Add dataset and identity contract foundations

This commit is contained in:
jb
2026-09-19 10:01:47 -04:00
parent 6af610d9ce
commit 348aa420ec
5 changed files with 1272 additions and 3 deletions
+351
View File
@@ -2,8 +2,18 @@ import type {
Action,
Capability,
CapabilityGate,
ConsentBinding,
ConsentCallbackResult,
CompleteReportInputPin,
ConsentResourceRequest,
DatasetJob,
DatasetKind,
DatasetSyncState,
MissingRequirement,
Requirement,
ResourceAuthorization,
ResourcePrincipal,
SelectedResource,
UserId,
} from './model';
@@ -12,12 +22,238 @@ export const CAPABILITY_REQUIREMENTS: Readonly<Record<Capability, readonly Requi
asset_search: ['assets'],
asset_valuation: ['assets', 'market_data'],
manufacturing_opportunities: ['assets', 'blueprints', 'skills', 'market_data'],
inventory_query: ['assets', 'reference_data'],
wallet_balance: ['wallet_balance'],
};
/** Assets scope verified against the 2026-08-18 ESI OpenAPI contract. */
export const ESI_ASSETS_SCOPE = 'esi-assets.read_assets.v1';
export const ESI_WALLET_SCOPE = 'esi-wallet.read_character_wallet.v1';
export const ESI_BLUEPRINTS_SCOPE = 'esi-characters.read_blueprints.v1';
export const ESI_SKILLS_SCOPE = 'esi-skills.read_skills.v1';
export const ESI_SKILL_QUEUE_SCOPE = 'esi-skills.read_skillqueue.v1';
export const ASSETS_SCOPE = ESI_ASSETS_SCOPE;
export const REQUIRED_ASSETS_SCOPE = ESI_ASSETS_SCOPE;
export const WALLET_BALANCE_SCOPE = ESI_WALLET_SCOPE;
export type CapabilityDefinition = {
capability: Capability;
datasets: readonly DatasetKind[];
requiredScopes: readonly string[];
supportsResourceSelection: boolean;
allowedPrincipalKinds: readonly ResourcePrincipal['kind'][];
};
/**
* The allowlisted capability surface is declarative. Consumers may use this to
* construct consent requests, but it does not itself authorize a principal.
*/
export const CAPABILITY_DEFINITIONS: Readonly<Record<Capability, CapabilityDefinition>> = {
asset_search: {
capability: 'asset_search', datasets: ['assets'], requiredScopes: [ESI_ASSETS_SCOPE], supportsResourceSelection: true, allowedPrincipalKinds: ['character'],
},
asset_valuation: {
capability: 'asset_valuation', datasets: ['assets', 'market_data'], requiredScopes: [ESI_ASSETS_SCOPE], supportsResourceSelection: true, allowedPrincipalKinds: ['character'],
},
manufacturing_opportunities: {
capability: 'manufacturing_opportunities',
datasets: ['assets', 'blueprints', 'skills', 'market_data'],
requiredScopes: [ESI_ASSETS_SCOPE, ESI_BLUEPRINTS_SCOPE, ESI_SKILLS_SCOPE, ESI_SKILL_QUEUE_SCOPE],
supportsResourceSelection: true,
allowedPrincipalKinds: ['character'],
},
inventory_query: {
capability: 'inventory_query', datasets: ['assets', 'reference_data'], requiredScopes: [ESI_ASSETS_SCOPE], supportsResourceSelection: true, allowedPrincipalKinds: ['character'],
},
wallet_balance: {
capability: 'wallet_balance', datasets: ['wallet_balance'], requiredScopes: [ESI_WALLET_SCOPE], supportsResourceSelection: true, allowedPrincipalKinds: ['character'],
},
};
export const SERVER_SCOPE_ALLOWLIST: readonly string[] = [
ESI_ASSETS_SCOPE, ESI_WALLET_SCOPE, ESI_BLUEPRINTS_SCOPE, ESI_SKILLS_SCOPE, ESI_SKILL_QUEUE_SCOPE,
];
const SYSTEM_JOB_DATASETS: Readonly<Record<'reference' | 'public_feed' | 'maintenance', readonly DatasetKind[]>> = {
reference: ['reference_data'],
public_feed: ['market_data'],
maintenance: ['assets', 'blueprints', 'skills', 'market_data', 'reference_data', 'wallet_balance', 'wallet_journal', 'wallet_transactions'],
};
export type CompleteReportPinValidation =
| { valid: true }
| { valid: false; reason: 'missing_pin' | 'unexpected_pin' | 'duplicate_pin' | 'principal_mismatch' | 'revision_mismatch' | 'incomplete_coverage' };
export type DatasetJobFenceValidation =
| { valid: true }
| { valid: false; reason: 'job_mismatch' | 'empty_user_fence' | 'system_resource_fence' | 'owner_mismatch' };
export type ConsentCallbackValidation =
| { valid: true }
| { valid: false; reason: 'binding_mismatch' | 'resource_mismatch' | 'scope_mismatch' | 'revision_mismatch' | 'duplicate_resource' };
export type ResourceCapabilityScopeValidation = CapabilityScopeValidation & {
principal: ResourcePrincipal;
};
export type CapabilityScopeValidation = {
valid: boolean;
requiredScopes: readonly string[];
allowlistedScopes: readonly string[];
preservedScopes: readonly string[];
missingScopes: readonly string[];
unexpectedScopes: readonly string[];
};
/**
* Validate a consent request against required capability scopes plus an
* explicitly server-approved set of existing grants. Callers cannot turn an
* arbitrary requested scope into a preserved grant.
*/
export function validateCapabilityScopes(
capability: Capability,
requestedScopes: readonly string[],
approvedExistingScopes: readonly string[] = [],
): CapabilityScopeValidation {
return validateScopeLists(CAPABILITY_DEFINITIONS[capability].requiredScopes, requestedScopes, approvedExistingScopes);
}
function validateScopeLists(
requiredScopes: readonly string[],
requestedScopes: readonly string[],
approvedExistingScopes: readonly string[],
): CapabilityScopeValidation {
const serverAllowlist = new Set(SERVER_SCOPE_ALLOWLIST);
const preservedScopes = approvedExistingScopes.filter((scope) => serverAllowlist.has(scope));
const allowlistedScopes = [...new Set([...requiredScopes, ...preservedScopes])];
const allowlisted = new Set(allowlistedScopes);
const missingScopes = requiredScopes.filter((scope) => !requestedScopes.includes(scope));
const unexpectedScopes = requestedScopes.filter((scope) => !allowlisted.has(scope) || !serverAllowlist.has(scope));
return {
valid: missingScopes.length === 0 && unexpectedScopes.length === 0,
requiredScopes: [...requiredScopes],
allowlistedScopes,
preservedScopes: preservedScopes.filter((scope) => requestedScopes.includes(scope)),
missingScopes,
unexpectedScopes,
};
}
/** Keyed wrapper used by consent flows; the approved scopes belong to this resource. */
export function validateResourceCapabilityScopes(
capability: Capability,
request: ConsentResourceRequest,
): ResourceCapabilityScopeValidation {
const requiredScopes = [...new Set([...CAPABILITY_DEFINITIONS[capability].requiredScopes, ...request.resource.requiredScopes])];
const validated = validateScopeLists(requiredScopes, request.requestedScopes, request.approvedExistingScopes);
return { principal: request.resource.principal, ...validated };
}
/** Validate callback correspondence before any credential or grant is persisted. */
export function validateConsentCallback(binding: ConsentBinding, callback: ConsentCallbackResult, now = Date.now()): ConsentCallbackValidation {
if (callback.bindingId !== binding.bindingId) return { valid: false, reason: 'binding_mismatch' };
if (callback.status === 'recovery_required' || callback.status === 'denied') return { valid: true };
if (binding.state !== 'pending' || !Number.isFinite(now) || !Number.isFinite(binding.createdAt) || !Number.isFinite(binding.expiresAt)
|| binding.expiresAt <= binding.createdAt || now < binding.createdAt || now >= binding.expiresAt) return { valid: false, reason: 'binding_mismatch' };
if (callback.resourceGrants.length !== binding.resourceRequests.length) return { valid: false, reason: 'resource_mismatch' };
const seen = new Set<string>();
for (const grant of callback.resourceGrants) {
const key = principalFingerprint(grant.request.resource.principal);
if (seen.has(key)) return { valid: false, reason: 'duplicate_resource' };
seen.add(key);
const bound = binding.resourceRequests.find((request) => samePrincipal(request.resource.principal, grant.request.resource.principal));
if (!bound || !sameResourceRequest(bound, grant.request)) return { valid: false, reason: 'resource_mismatch' };
const scopes = validateResourceCapabilityScopes(binding.capability, { ...grant.request, requestedScopes: grant.grantedScopes });
if (!scopes.valid) return { valid: false, reason: 'scope_mismatch' };
if (!validCounter(bound.resource.authorizationEpoch) || !validCounter(bound.resource.authorizationRevision)
|| !validCounter(grant.authorizationEpoch) || !validCounter(grant.grantRevision) || !validCounter(grant.credentialVersion)
|| grant.authorizationEpoch !== bound.resource.authorizationEpoch || grant.grantRevision < bound.resource.authorizationRevision) {
return { valid: false, reason: 'revision_mismatch' };
}
}
return seen.size === binding.resourceRequests.length ? { valid: true } : { valid: false, reason: 'resource_mismatch' };
}
function sameResourceRequest(left: ConsentResourceRequest, right: ConsentResourceRequest): boolean {
return samePrincipal(left.resource.principal, right.resource.principal)
&& left.resource.authorizationRevision === right.resource.authorizationRevision
&& left.resource.authorizationEpoch === right.resource.authorizationEpoch
&& sameStrings(left.resource.requiredScopes, right.resource.requiredScopes)
&& sameStrings(left.requestedScopes, right.requestedScopes)
&& sameStrings(left.approvedExistingScopes, right.approvedExistingScopes);
}
function validCounter(value: number): boolean {
return Number.isSafeInteger(value) && value >= 0;
}
function samePrincipal(left: ResourcePrincipal, right: ResourcePrincipal): boolean {
if (left.kind !== right.kind || left.id !== right.id) return false;
if (left.kind === 'character' || left.kind === 'corporation') {
return right.kind === left.kind && right.ownerUserId === left.ownerUserId;
}
return true;
}
function principalFingerprint(principal: ResourcePrincipal): string {
return JSON.stringify(principal);
}
function sameStrings(left: readonly string[], right: readonly string[]): boolean {
return left.length === right.length && left.every((value) => right.includes(value));
}
/** Validate the exact selected-resource × required-dataset pin set. */
export function validateCompleteReportPins(input: CompleteReportInputPin): CompleteReportPinValidation {
if (input.pinSet !== 'exact_selected_resource_dataset_set') return { valid: false, reason: 'unexpected_pin' };
const expected = new Set(input.selectedResources.flatMap((resource) => input.requiredDatasetKinds.map((dataset) => `${principalFingerprint(resource.principal)}|${dataset}`)));
const seen = new Set<string>();
for (const pin of input.datasetPins) {
const key = `${principalFingerprint(pin.selectedResource.principal)}|${pin.dataset}`;
if (!samePrincipal(pin.principal, pin.selectedResource.principal)) return { valid: false, reason: 'principal_mismatch' };
if (!sameSelectedResource(input.selectedResources, pin.selectedResource)
|| pin.authorizationRevision !== pin.selectedResource.authorizationRevision
|| pin.authorizationEpoch !== pin.selectedResource.authorizationEpoch) return { valid: false, reason: 'revision_mismatch' };
if (!expected.has(key)) return { valid: false, reason: 'unexpected_pin' };
if (seen.has(key)) return { valid: false, reason: 'duplicate_pin' };
seen.add(key);
if (!pin.coverage.complete) return { valid: false, reason: 'incomplete_coverage' };
}
return seen.size === expected.size ? { valid: true } : { valid: false, reason: 'missing_pin' };
}
function sameSelectedResource(resources: readonly SelectedResource[], candidate: SelectedResource): boolean {
return resources.some((resource) => samePrincipal(resource.principal, candidate.principal)
&& resource.authorizationRevision === candidate.authorizationRevision
&& resource.authorizationEpoch === candidate.authorizationEpoch
&& sameStrings(resource.requiredScopes, candidate.requiredScopes));
}
/** Validate duplicated job/fence identity before a worker can publish. */
export function validateDatasetJobFence(job: DatasetJob): DatasetJobFenceValidation {
const fence = job.publicationFence;
if (fence.jobId !== job.id || fence.dataset !== job.dataset) return { valid: false, reason: 'job_mismatch' };
if (job.authority === 'system') {
const service = 'service' in job.owner ? job.owner.service : null;
const allowedDatasets = service === null ? [] : SYSTEM_JOB_DATASETS[service];
return fence.resources.length === 0 && fence.principal.kind === 'system' && allowedDatasets.includes(job.dataset)
? { valid: true }
: { valid: false, reason: 'system_resource_fence' };
}
if (fence.resources.length === 0 || fence.principal.kind === 'system') return { valid: false, reason: 'empty_user_fence' };
const owner = job.owner as { kind: 'user'; userId: UserId } | { kind: 'corporation'; principal: ResourcePrincipal };
if (owner.kind === 'user') {
return principalBelongsToUser(fence.principal, owner.userId)
&& fence.resources.every((resource) => principalBelongsToUser(resource.principal, owner.userId))
? { valid: true }
: { valid: false, reason: 'owner_mismatch' };
}
if (owner.kind === 'corporation') {
return samePrincipal(fence.principal, owner.principal) && fence.resources.every((resource) => samePrincipal(resource.principal, owner.principal))
? { valid: true }
: { valid: false, reason: 'owner_mismatch' };
}
return { valid: false, reason: 'owner_mismatch' };
}
export const CONNECT_ACTION: Action = {
kind: 'connect_character',
@@ -58,6 +294,8 @@ export function requirementsMissing(
/**
* Gate a capability using only deterministic application state. In particular,
* this function never delegates an authorization decision to an AI model.
* This compatibility helper accepts precomputed missing requirements; callers
* at an authorization boundary should use gateCapabilityWithAuthorization.
*/
export function gateCapability(
capability: Capability,
@@ -70,6 +308,119 @@ export function gateCapability(
return { status: 'available' };
}
export type CapabilityAuthorizationContext = {
userId: UserId | null;
/** Server-resolved grant records, one per selected resource; never global user scopes. */
resourceAuthorizations: readonly ResourceAuthorization[];
selectedResources: readonly SelectedResource[];
datasets: readonly DatasetSyncState[];
};
const REQUIREMENT_DATASET: Partial<Record<Requirement, DatasetKind>> = {
assets: 'assets',
wallet_balance: 'wallet_balance',
market_data: 'market_data',
reference_data: 'reference_data',
blueprints: 'blueprints',
skills: 'skills',
};
const SCOPE_REQUIREMENT: Readonly<Record<string, Requirement>> = {
[ESI_ASSETS_SCOPE]: 'assets',
[ESI_WALLET_SCOPE]: 'wallet_balance',
[ESI_BLUEPRINTS_SCOPE]: 'blueprints',
[ESI_SKILLS_SCOPE]: 'skills',
[ESI_SKILL_QUEUE_SCOPE]: 'skills',
};
function principalBelongsToUser(principal: ResourcePrincipal, userId: UserId): boolean {
if (principal.kind === 'user') return principal.id === userId;
if (principal.kind === 'character' || principal.kind === 'corporation') return principal.ownerUserId === userId;
return false;
}
function addMissing(missing: MissingRequirement[], entry: MissingRequirement): void {
if (!missing.some((current) => current.requirement === entry.requirement && current.reason === entry.reason && current.characterId === entry.characterId)) {
missing.push(entry);
}
}
/**
* Authoritative contract gate: derives missing scopes/resources/dataset state
* instead of treating a non-empty user ID as authorization.
*/
export function gateCapabilityWithAuthorization(
capability: Capability,
context: CapabilityAuthorizationContext,
actions: readonly Action[] = [],
): CapabilityGate {
if (typeof context.userId !== 'string' || context.userId.length === 0) return authenticationRequired(capability);
const userId = context.userId;
const definition = CAPABILITY_DEFINITIONS[capability];
const missing: MissingRequirement[] = [];
const selectedKeys = new Set<string>();
for (const resource of context.selectedResources) {
selectedKeys.add(principalFingerprint(resource.principal));
const authorization = context.resourceAuthorizations.find((entry) => samePrincipal(entry.principal, resource.principal));
if (!authorization || authorization.userId !== userId || authorization.status !== 'active') {
addMissing(missing, { requirement: CAPABILITY_REQUIREMENTS[capability][0]!, reason: 'authorization' });
continue;
}
if (authorization.grantRevision !== resource.authorizationRevision || authorization.authorizationEpoch !== resource.authorizationEpoch) {
addMissing(missing, { requirement: CAPABILITY_REQUIREMENTS[capability][0]!, reason: 'stale' });
}
if (!definition.allowedPrincipalKinds.includes(resource.principal.kind)) {
addMissing(missing, { requirement: CAPABILITY_REQUIREMENTS[capability][0]!, reason: 'authorization' });
continue;
}
if (!principalBelongsToUser(resource.principal, userId)) {
addMissing(missing, { requirement: CAPABILITY_REQUIREMENTS[capability][0]!, reason: 'authorization' });
}
for (const scope of [...definition.requiredScopes, ...resource.requiredScopes]) {
if (!authorization.grantedScopes.includes(scope)) {
addMissing(missing, { requirement: SCOPE_REQUIREMENT[scope] ?? CAPABILITY_REQUIREMENTS[capability][0]!, reason: 'authorization' });
}
}
}
if (definition.supportsResourceSelection && context.selectedResources.length === 0 && definition.requiredScopes.length > 0) {
addMissing(missing, { requirement: CAPABILITY_REQUIREMENTS[capability][0]!, reason: 'authorization' });
}
for (const requirement of CAPABILITY_REQUIREMENTS[capability]) {
const dataset = REQUIREMENT_DATASET[requirement];
if (!dataset) continue;
const allStates = context.datasets.filter((state) => state.dataset === dataset);
// Public reference/market state may be system-owned; private datasets must
// match one of the already-validated selected principals exactly.
const isPublicDataset = dataset === 'reference_data' || dataset === 'market_data';
const states = isPublicDataset
? allStates.filter((state) => state.authorizationStatus === 'active' && state.principal.kind === 'system')
: allStates.filter((state) => {
const selected = context.selectedResources.find((resource) => samePrincipal(resource.principal, state.principal));
return selected !== undefined
&& state.authorizationStatus === 'active'
&& state.grantRevision === selected.authorizationRevision
&& state.authorizationEpoch === selected.authorizationEpoch
&& principalBelongsToUser(state.principal, userId);
});
const selectedStateKeys = new Set(states.map((state) => principalFingerprint(state.principal)));
const missingSelectedState = !isPublicDataset && [...selectedKeys].some((key) => !selectedStateKeys.has(key));
if (states.length === 0 || missingSelectedState) {
addMissing(missing, { requirement, reason: 'synchronization' });
continue;
}
if (states.some((state) => state.status === 'stale')) {
addMissing(missing, { requirement, reason: 'stale' });
} else if (states.some((state) => state.status !== 'ready')) {
addMissing(missing, { requirement, reason: 'synchronization' });
}
}
if (missing.length > 0) return requirementsMissing(capability, missing, actions);
return { status: 'available' };
}
/**
* Manufacturing is intentionally a visible, typed not-yet-implemented gate.
* Skills and blueprints are separate requirements so a caller cannot mistake
+431 -3
View File
@@ -15,9 +15,18 @@ export type Asset = {
locationId: string;
locationType: string;
locationFlag: string;
/** ESI is_singleton; absent on legacy rows and therefore unknown. */
isSingleton?: boolean;
isBlueprintCopy?: boolean;
};
export type InventoryType = { id: number; name: string; categoryId: number; volume?: number };
export type InventoryType = {
id: number;
name: string;
categoryId: number;
/** Legacy reference rows may omit group identity until refreshed. */
groupId?: number;
volume?: number;
};
/** Integer cents (0.01 ISK), serialized as decimal text. No floating-point accumulation. */
export type Isk = string;
export type ValuationMethod = 'reference_estimate' | 'immediate_liquidation' | 'expected_sell_order' | 'replacement';
@@ -39,8 +48,8 @@ export interface ReferenceRepository {
export interface MarketEstimates {
estimates(typeIds: number[]): Promise<PriceSnapshot>;
}
export type Requirement = 'assets' | 'blueprints' | 'skills' | 'market_data';
export type Capability = 'asset_search' | 'asset_valuation' | 'manufacturing_opportunities';
export type Requirement = 'assets' | 'blueprints' | 'skills' | 'market_data' | 'wallet_balance' | 'reference_data';
export type Capability = 'asset_search' | 'asset_valuation' | 'manufacturing_opportunities' | 'inventory_query' | 'wallet_balance';
export type Action = { kind: 'connect_character' | 'synchronize'; label: string; href: string };
export type MissingRequirement = { requirement: Requirement; characterId?: number; reason: 'authorization' | 'synchronization' | 'stale' | 'not_implemented' };
export type CapabilityGate =
@@ -56,6 +65,8 @@ export type AssetReport = {
referenceVersion: string;
prices: Omit<PriceSnapshot, 'prices'>;
warnings: string[];
/** Added for pinned downstream reports; optional for legacy asset consumers. */
provenance?: ReportProvenance;
};
export type AssetOperation =
| Exclude<CapabilityGate, { status: 'available' }>
@@ -92,3 +103,420 @@ export interface CapitalRouter {
export interface OrderBook {
buyOrders(typeId: number, locationId: string): Promise<{ orderId: string; price: Isk; remaining: number; minimumVolume: number; range: string }[]>;
}
/**
* Cross-feature contracts for dataset authorization and publication. These are
* data-only ports: persistence, OAuth callbacks, workers and schema migrations
* deliberately remain in their owning milestones.
*/
export type DecimalId = string;
export type DatasetKind =
| 'assets'
| 'wallet_balance'
| 'wallet_journal'
| 'wallet_transactions'
| 'market_data'
| 'reference_data'
| 'blueprints'
| 'skills';
/** These are semantically distinct counters even when their numeric values coincide. */
export type GrantRevision = number;
export type AuthorizationRevision = GrantRevision;
export type AuthorizationEpoch = number;
export type CredentialVersion = number;
export type SnapshotGeneration = string;
export type AppendCheckpoint = string;
export type ReferenceVersion = string;
export type PriceVersion = string;
/** A principal is an authorization subject, never a browser-provided selector. */
export type ResourcePrincipal =
| { kind: 'user'; id: UserId }
| { kind: 'character'; id: DecimalId; ownerUserId: UserId }
| { kind: 'corporation'; id: DecimalId; ownerUserId: UserId }
| { kind: 'system'; id: string };
export type SelectedResource = {
principal: ResourcePrincipal;
requiredScopes: readonly string[];
authorizationRevision: AuthorizationRevision;
authorizationEpoch: AuthorizationEpoch;
};
/** Snapshot and append datasets cannot be represented by one freshness field. */
export type AppendCoverageGap = {
from: AppendCheckpoint | null;
to: AppendCheckpoint | null;
reason: string;
};
export type DatasetCoverage =
| { kind: 'snapshot'; generation: SnapshotGeneration; complete: true }
| { kind: 'snapshot'; generation: null; complete: false }
| {
kind: 'append';
checkpoint: AppendCheckpoint | null;
sourceIdentity: string;
windowStart: number | null;
windowEnd: number | null;
gaps: readonly AppendCoverageGap[];
complete: boolean;
};
export type DatasetSyncStatus =
| 'never'
| 'queued'
| 'syncing'
| 'ready'
| 'partial'
| 'stale'
| 'blocked'
| 'error'
| 'quarantined';
/** A ready state cannot claim incomplete or missing coverage. */
export type CompleteDatasetCoverage =
| { kind: 'snapshot'; generation: SnapshotGeneration; complete: true }
| { kind: 'append'; checkpoint: AppendCheckpoint; sourceIdentity: string; windowStart: number; windowEnd: number; gaps: readonly []; complete: true };
/** Upstream effective time is null when ESI does not provide one. */
type DatasetSyncStateBase = {
principal: ResourcePrincipal;
dataset: DatasetKind;
/** Current authority snapshot; ready data from another revision/epoch is not readable. */
authorizationStatus: AuthorizationStatus;
authorizationEpoch: AuthorizationEpoch;
grantRevision: GrantRevision;
credentialVersion: CredentialVersion | null;
fetchedAt: number | null;
upstreamEffectiveAt: number | null;
failure?: { category: string; retryable: boolean };
};
export type DatasetSyncState =
| (DatasetSyncStateBase & { status: 'ready'; coverage: CompleteDatasetCoverage })
| (DatasetSyncStateBase & { status: Exclude<DatasetSyncStatus, 'ready'>; coverage: DatasetCoverage });
export type AuthorizationStatus = 'active' | 'revocation_pending' | 'quarantined' | 'revoked' | 'transferred';
/**
* A missing upstream revocation/transfer observation is intentionally not
* collapsed into a chosen stale-authority grace period. Operators must approve
* that window before a worker can promise one.
*/
export type StaleAuthorityObservation = {
state: 'current' | 'stale' | 'unknown';
observedAt: number | null;
maximumWindowMs: number | null;
};
export type ResourceAuthorization = {
principal: ResourcePrincipal;
userId: UserId;
status: AuthorizationStatus;
grantRevision: GrantRevision;
credentialVersion: CredentialVersion | null;
grantedScopes: readonly string[];
observedAt: number;
staleAuthority: StaleAuthorityObservation;
authorizationEpoch: AuthorizationEpoch;
};
export type ConsentIntent = 'initial_login' | 'link_character' | 'upgrade_dataset' | 'reconnect';
/** Scopes and revisions are keyed to one selected resource, never one global user list. */
export type ConsentResourceRequest = {
resource: SelectedResource;
requestedScopes: readonly string[];
approvedExistingScopes: readonly string[];
};
export type ConsentResourceGrant = {
/** Echoes the exact bound request; the callback cannot return an unbound principal. */
request: ConsentResourceRequest;
grantedScopes: readonly string[];
grantRevision: GrantRevision;
credentialVersion: CredentialVersion;
authorizationEpoch: AuthorizationEpoch;
};
/** One OAuth/session binding is consumed at most once and carries its scope intent. */
export type ConsentBinding = {
bindingId: string;
sessionId: string;
userId: UserId | null;
intent: ConsentIntent;
capability: Capability;
resourceRequests: readonly [ConsentResourceRequest, ...ConsentResourceRequest[]];
createdAt: number;
expiresAt: number;
state: 'pending' | 'consumed' | 'failed' | 'expired';
};
export type ConsentCallbackResult =
| { status: 'accepted'; bindingId: string; resourceGrants: readonly [ConsentResourceGrant, ...ConsentResourceGrant[]] }
| { status: 'denied'; bindingId: string; resourceGrants: readonly [] }
| { status: 'recovery_required'; bindingId: string; codeOutcome: 'unknown' | 'consumed'; retry: 'recover' | 'new_binding' };
/** Refresh CAS and OAuth replay are separate concerns; unknown rotation is actionable. */
export type CredentialRefreshResult =
| { status: 'updated'; credentialVersion: CredentialVersion; grantRevision: GrantRevision }
| { status: 'unchanged'; credentialVersion: CredentialVersion; grantRevision: GrantRevision }
| { status: 'uncertain'; credentialVersion: CredentialVersion; grantRevision: GrantRevision; action: 'reauthorize' | 'recover' };
export type EntityResolution<T> =
| { status: 'resolved'; value: T }
| { status: 'ambiguous'; candidates: readonly T[] }
| { status: 'unknown'; reason: string };
export type JobOwner =
| { kind: 'user'; userId: UserId }
| { kind: 'system'; service: 'reference' | 'public_feed' | 'maintenance' }
| { kind: 'corporation'; principal: Extract<ResourcePrincipal, { kind: 'corporation' }> };
export type JobAuthority = 'user' | 'system';
export type SyncJobStatus = 'queued' | 'running' | 'succeeded' | 'failed' | 'cancelled' | 'quarantined';
export type PublicationOutcome = 'success' | 'failure';
export type PublicationResourceFence = SelectedResource & {
/** Monotonic revocation/quarantine epoch, distinct from grant and credential versions. */
credentialVersion: CredentialVersion | null;
};
/** A publication fence binds all selected resources and both write outcomes. */
type NonEmptyReadonly<T> = readonly [T, ...T[]];
type PublicationFenceBase = {
jobId: string;
/** Job principal is retained for routing; resources are the authorization fence. */
principal: ResourcePrincipal;
resources: readonly PublicationResourceFence[];
dataset: DatasetKind;
};
export type PublicationFence =
| (PublicationFenceBase & {
coverageKind: 'snapshot';
generation: SnapshotGeneration;
checkpoint: null;
})
| (PublicationFenceBase & {
coverageKind: 'append';
generation: null;
checkpoint: AppendCheckpoint | null;
});
type UserPublicationPrincipal = Exclude<ResourcePrincipal, { kind: 'system' }>;
export type UserPublicationFence = PublicationFence & { principal: UserPublicationPrincipal; resources: NonEmptyReadonly<PublicationResourceFence> };
export type SystemPublicationFence = PublicationFence & { principal: Extract<ResourcePrincipal, { kind: 'system' }>; resources: readonly [] };
type DatasetJobBase = {
id: string;
dataset: DatasetKind;
status: SyncJobStatus;
attempts: number;
dueAt: number;
leaseExpiresAt: number | null;
};
/** Authority is discriminated with ownership; callers cannot promote a job by changing a flag. */
export type DatasetJob = DatasetJobBase &
(
| { owner: Extract<JobOwner, { kind: 'user' }>; authority: 'user'; publicationFence: UserPublicationFence }
| { owner: Extract<JobOwner, { kind: 'corporation' }>; authority: 'user'; publicationFence: UserPublicationFence }
| { owner: Extract<JobOwner, { kind: 'system' }>; authority: 'system'; publicationFence: SystemPublicationFence }
);
export type PublicationAttempt = {
fence: PublicationFence;
outcome: PublicationOutcome;
observedAt: number;
accepted: boolean;
rejectionReason?: 'stale_generation' | 'stale_grant' | 'stale_credential' | 'stale_authorization_epoch' | 'authorization_revoked' | 'quarantined';
};
export type ReportDatasetPin = {
/** The selected resource this dataset pin belongs to; principal is retained as a compatibility index. */
selectedResource: SelectedResource;
principal: ResourcePrincipal;
dataset: DatasetKind;
coverage: DatasetCoverage;
authorizationRevision: AuthorizationRevision;
authorizationEpoch: AuthorizationEpoch;
fetchedAt: number | null;
upstreamEffectiveAt: number | null;
};
export type ReportInputPin = {
selectedResources: readonly SelectedResource[];
requiredDatasetKinds: readonly [DatasetKind, ...DatasetKind[]];
/** One pin per selected resource/dataset; maps below are compatibility indexes. */
datasetPins: readonly ReportDatasetPin[];
authorizationRevisions: Readonly<Record<string, AuthorizationRevision>>;
authorizationEpochs: Readonly<Record<string, AuthorizationEpoch>>;
snapshotGenerations: Readonly<Record<string, SnapshotGeneration>>;
appendCheckpoints: Readonly<Record<string, AppendCheckpoint>>;
referenceVersion: ReferenceVersion | null;
priceVersion: PriceVersion | null;
};
export type ReportProvenance = {
reportId: string;
generatedAt: number;
fetchedAt: Readonly<Record<string, number | null>>;
upstreamEffectiveAt: Readonly<Record<string, number | null>>;
input: ReportInputPin;
sourceSkew: boolean;
assumptions: readonly string[];
};
export type CompleteReportDatasetPin = ReportDatasetPin & { coverage: CompleteDatasetCoverage };
export type CompleteReportInputPin = Omit<ReportInputPin, 'datasetPins'> & {
/** Set identity is validated against selectedResources × requiredDatasetKinds. */
pinSet: 'exact_selected_resource_dataset_set';
datasetPins: readonly [CompleteReportDatasetPin, ...CompleteReportDatasetPin[]];
};
export type CompleteReportProvenance = Omit<ReportProvenance, 'input'> & { input: CompleteReportInputPin };
export type InventoryBlueprintState = 'all' | 'original' | 'copy' | 'unknown';
export type InventorySortField = 'character' | 'name' | 'quantity' | 'location' | 'estimated_value';
export type InventorySortDirection = 'ascending' | 'descending';
export type InventoryFilters = {
characterIds?: readonly DecimalId[];
typeIds?: readonly number[];
customName?: string;
categoryIds?: readonly number[];
groupIds?: readonly number[];
minQuantity?: string;
maxQuantity?: string;
locationIds?: readonly DecimalId[];
systemIds?: readonly number[];
regionIds?: readonly number[];
blueprintState?: InventoryBlueprintState;
};
/** Item ID is the canonical unique tie-breaker for every inventory ordering. */
export type InventorySort = { field: InventorySortField; direction: InventorySortDirection; tieBreaker: 'item_id' };
/** Cursor tokens are opaque; the unsigned pin is retained server-side and revalidated on use. */
export type InventoryCursorPin = {
callerUserId: UserId;
queryFingerprint: string;
selectedResources: readonly SelectedResource[];
authorizationRevisions: Readonly<Record<string, AuthorizationRevision>>;
authorizationEpochs: Readonly<Record<string, AuthorizationEpoch>>;
snapshotGenerations: Readonly<Record<string, SnapshotGeneration>>;
referenceVersion: ReferenceVersion | null;
priceVersion: PriceVersion | null;
expiresAt: number;
};
export type InventoryCursorPosition = { offset: number; lastItemId: DecimalId | null };
export type InventoryCursor = { token: string };
export type InventoryCursorState = { token: string; pin: InventoryCursorPin; status: 'active' | 'exhausted'; position: InventoryCursorPosition };
export type InventoryQuery = {
/** Resolved from the verified session; never accepted as caller authority. */
callerUserId: UserId;
/** Resolved by the authorization service from caller-owned resources. */
selectedResources: readonly SelectedResource[];
authorizationRevisions: Readonly<Record<string, AuthorizationRevision>>;
authorizationEpochs: Readonly<Record<string, AuthorizationEpoch>>;
filters: InventoryFilters;
sort: InventorySort;
pageSize: number;
cursor: InventoryCursor | null;
};
export type InventoryRow = Omit<Asset, 'isSingleton'> & {
name: string;
categoryId: number | null;
groupId: number | null;
customName: string | null;
systemId: number | null;
regionId: number | null;
isSingleton: boolean | null;
estimatedValue: Isk | null;
};
export type InventorySummary = {
matchingRows: number;
matchingQuantity: string;
estimatedValue: Isk | null;
scope: 'all_matching_rows';
};
export type InventoryPageState = 'initial' | 'continuation' | 'after_end';
export type InventoryQueryResult =
| { status: 'ready'; rows: readonly InventoryRow[]; summary: InventorySummary; nextCursor: InventoryCursor | null; pageState: 'initial' | 'continuation'; provenance: CompleteReportProvenance }
| { status: 'ready'; rows: readonly InventoryRow[]; summary: InventorySummary; nextCursor: null; pageState: 'after_end'; provenance: CompleteReportProvenance }
| { status: 'partial' | 'stale' | 'authorization_required' | 'unavailable'; rows: readonly InventoryRow[]; summary: InventorySummary | null; reason: string; provenance?: ReportProvenance };
export type ReferenceCategoryRecord = { id: number; name: string; published?: boolean };
export type ReferenceGroupRecord = { id: number; categoryId: number; name: string; published?: boolean };
export type ReferenceTypeRecord = { id: number; name: string; categoryId: number; groupId: number; published?: boolean; volume?: number };
export type ReferenceRegionRecord = { id: number; name: string | null };
export type ReferenceConstellationRecord = { id: number; name: string | null; regionId: number | null };
export type ReferenceSystemRecord = {
id: number; name: string; constellationId: number; regionId: number | null;
x: number; y: number; z: number; securityStatus: number; securityClass: string | null;
};
export type ReferenceStationRecord = {
id: number; systemId: number; systemName: string | null; regionId: number | null;
name: string | null; typeId: number | null; ownerId: number | null;
x: number | null; y: number | null; z: number | null;
};
export type ReferenceSearchOptions = { query: string; categoryId?: number; groupId?: number; published?: boolean; limit?: number; offset?: number };
export type ReferenceListOptions = { ids?: number[]; query?: string; limit?: number; offset?: number };
export type ReferenceIdSelection = number[] | ReferenceListOptions | undefined;
export type ReferenceGroupSelection = number[] | (ReferenceListOptions & { categoryId?: number }) | undefined;
export type ReferenceSystemSelection = number[] | (ReferenceListOptions & { constellationId?: number; regionId?: number }) | undefined;
export type ReferenceStationSelection = number[] | (ReferenceListOptions & { systemId?: number }) | undefined;
/** Structural mirror of T03's indexed reader; records stay immutable/version-pinned. */
export interface ReferenceReader extends ReferenceRepository {
searchTypes(options: ReferenceSearchOptions): Promise<ReferenceTypeRecord[]>;
searchTypesByName(query: string, options?: Omit<ReferenceSearchOptions, 'query'>): Promise<ReferenceTypeRecord[]>;
categories(selection?: ReferenceIdSelection): Promise<ReferenceCategoryRecord[]>;
groups(selection?: ReferenceGroupSelection): Promise<ReferenceGroupRecord[]>;
regions(selection?: ReferenceIdSelection): Promise<ReferenceRegionRecord[]>;
constellations(selection?: ReferenceIdSelection): Promise<ReferenceConstellationRecord[]>;
systems(selection?: ReferenceSystemSelection): Promise<ReferenceSystemRecord[]>;
stations(selection?: ReferenceStationSelection): Promise<ReferenceStationRecord[]>;
}
export interface ReferenceReaderHandle extends ReferenceReader {
readonly pinnedVersion: ReferenceVersion;
close(): void;
}
/** Main reader owns a manifest-pinned handle; callers use one handle across calls. */
export interface ReferenceReaderPort extends ReferenceReader {
pin(): ReferenceReaderHandle;
}
export type LocationFact = {
kind: 'region' | 'system' | 'station' | 'unknown';
id: DecimalId;
name: string | null;
parentId: DecimalId | null;
referenceVersion: ReferenceVersion;
};
/** Narrow adapter over a pinned T03 reader; unknown IDs remain explicit. */
export interface LocationReader {
location(id: DecimalId): Promise<LocationFact>;
}
export type WalletBalanceComponent = {
component: 'wallet_balance';
principal: ResourcePrincipal;
amount: Isk;
fetchedAt: number;
upstreamEffectiveAt: number | null;
coverage: DatasetCoverage;
authorizationRevision: AuthorizationRevision;
credentialVersion: CredentialVersion | null;
};
export type WalletBalanceResult =
| { status: 'ready'; components: readonly WalletBalanceComponent[]; provenance: CompleteReportProvenance }
| { status: 'partial'; components: readonly WalletBalanceComponent[]; missing: readonly ResourcePrincipal[]; provenance: ReportProvenance }
| { status: 'blocked' | 'unavailable'; reason: string };
/**
* Approved signed-decimal policy contract. This is intentionally a fixed
* policy, not a selectable rounding-mode API. T04 owns the parser; T02 only
* records the boundary and resource limits.
*/
export type SignedMoneyNormalizationPolicy = {
input: 'decimal_text';
scale: 2;
rounding: 'half_away_from_zero';
grammar: 'json_number_ascii_decimal';
maxRawInputCharacters: 8192;
maxCoefficientDigits: 4096;
exponentRange: readonly [-1000, 1000];
centsShiftRange: readonly [-1000, 1000];
rejectWhitespace: true;
canonicalizeNegativeZero: true;
};