Files
eden/src/lib/domain/model.ts
T

534 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** Domain values are JSON-safe. EVE item/location IDs are decimal strings, not JS floats. */
export type UserId = string;
export type CharacterDatasetStatus = {
dataset: string;
status: 'never' | 'queued' | 'syncing' | 'ready' | 'partial' | 'stale' | 'blocked' | 'error' | 'quarantined';
grantRevision: number;
authorizationEpoch: number;
credentialVersion: number;
generation?: string;
syncedAt?: number;
invalidatedAt?: number;
error?: string;
};
export type Character = {
id: number;
userId: UserId;
name: string;
scopes: string[];
/** Exactly one active primary may authenticate while signed out. */
loginRole: 'primary' | 'data_only';
authorizationStatus: 'active' | 'revocation_pending' | 'quarantined' | 'revoked' | 'transferred' | 'reconnect_required';
grantRevision: number;
credentialVersion: number;
authorizationEpoch: number;
datasets: CharacterDatasetStatus[];
assets: { status: 'never' | 'queued' | 'syncing' | 'ready' | 'error'; syncedAt?: number; expiresAt?: number; error?: string };
};
export type Asset = {
itemId: string;
characterId: number;
typeId: number;
quantity: number;
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;
/** Legacy reference rows may omit group identity until refreshed. */
groupId?: number;
volume?: number;
};
export type {
LocationFact,
LocationReader,
ReferenceCategoryRecord,
ReferenceConstellationRecord,
ReferenceGroupRecord,
ReferenceIdSelection,
ReferenceListOptions,
ReferenceReader,
ReferenceReaderHandle,
ReferenceReaderPort,
ReferenceRegionRecord,
ReferenceRepository,
ReferenceSearchOptions,
ReferenceStationEvidence,
ReferenceStationRecord,
ReferenceStationSelection,
ReferenceSystemRecord,
ReferenceSystemSelection,
ReferenceTypeRecord,
} from './reference';
/** 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';
export type PriceEstimate = { typeId: number; unitPrice: Isk };
export type PriceSnapshot = {
prices: PriceEstimate[];
source: string;
asOf: number;
expiresAt: number;
};
export interface CharacterAssetRepository {
characters(userId: UserId): Promise<Character[]>;
assets(userId: UserId, characterId: number): Promise<Asset[]>;
}
export interface MarketEstimates {
estimates(typeIds: number[]): Promise<PriceSnapshot>;
}
export type Requirement = 'assets' | 'blueprints' | 'skills' | 'market_data' | 'wallet_balance' | 'reference_data';
export type Capability = 'asset_search' | 'asset_valuation' | 'manufacturing_opportunities' | 'inventory_query' | 'inventory_locations' | '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 =
| { status: 'available' }
| { status: 'authentication_required' | 'requirements_missing'; capability: Capability; missing: MissingRequirement[]; actions: Action[] };
export type ValuedAsset = Asset & { name: string; unitPrice: Isk | null; estimatedValue: Isk | null; exclusion?: 'missing_price' | 'blueprint_copy' | 'unknown_type' };
export type AssetReport = {
method: 'reference_estimate';
total: Isk;
items: ValuedAsset[];
characters: { id: number; name: string; syncedAt: number }[];
unpricedItems: number;
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' }>
| { status: 'ready'; report: AssetReport }
| { status: 'unavailable'; reason: string };
// Planned domains: contracts only. These are not implemented features.
export type IndustryEconomics = {
expectedRevenue: Isk;
additionalIskRequired: Isk;
ownedMaterialOpportunityValue: Isk;
totalEconomicCost: Isk;
cashFlowGain: Isk;
economicProfit: Isk;
economicMargin: number | null;
productionSeconds: number;
iskPerHour: Isk | null;
};
export type SystemCoordinates = { systemId: number; x: number; y: number; z: number };
export type DockingEvidence = {
structureId: string;
characterId: number;
status: 'unknown' | 'allowed' | 'denied';
source: 'visibility_only' | 'esi_structure_access' | 'acl_evaluation' | 'observed_docking' | 'user_report';
observedAt: number;
expiresAt: number;
limitations: string[];
};
export interface StructureEvidenceRepository { docking(characterId: number, structureId: string): Promise<DockingEvidence>; }
export interface BlueprintRepository { manufacturingMaterials(blueprintTypeId: number): Promise<{ typeId: number; quantity: number }[]>; }
export interface CapitalRouter {
route(request: { from: number; to: number; shipTypeId: number; characterId: number; calibrationLevel: number; avoidSystems: number[]; avoidRegions: number[]; preferDockableFortizars: boolean }): Promise<{ systems: number[]; evidence: DockingEvidence[]; weight: number }>;
}
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 SyncJobDataset = 'assets';
export type SafeSyncJob = {
id: string;
dataset: SyncJobDataset;
characterId: number;
status: SyncJobStatus;
attempts: number;
dueAt: number;
createdAt: number;
updatedAt: number;
leaseExpiresAt: number | null;
cancelRequested: boolean;
error?: string;
};
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 };
/** Server-resolved values supplied on every continuation lookup; no caller signature is required. */
export type InventoryCursorLookup = {
token: string;
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;
now: number;
};
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 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;
};