Align shared reference and opaque cursor contracts

This commit is contained in:
jb
2026-09-19 10:01:47 -04:00
parent 348aa420ec
commit 240e16a258
6 changed files with 191 additions and 57 deletions
+31
View File
@@ -9,6 +9,8 @@ import type {
DatasetJob,
DatasetKind,
DatasetSyncState,
InventoryCursorLookup,
InventoryCursorState,
MissingRequirement,
Requirement,
ResourceAuthorization,
@@ -79,6 +81,10 @@ const SYSTEM_JOB_DATASETS: Readonly<Record<'reference' | 'public_feed' | 'mainte
maintenance: ['assets', 'blueprints', 'skills', 'market_data', 'reference_data', 'wallet_balance', 'wallet_journal', 'wallet_transactions'],
};
export type InventoryCursorValidation =
| { valid: true }
| { valid: false; reason: 'token_mismatch' | 'expired' | 'exhausted' | 'caller_mismatch' | 'query_mismatch' | 'resource_mismatch' | 'authorization_mismatch' | 'input_version_mismatch' };
export type CompleteReportPinValidation =
| { valid: true }
| { valid: false; reason: 'missing_pin' | 'unexpected_pin' | 'duplicate_pin' | 'principal_mismatch' | 'revision_mismatch' | 'incomplete_coverage' };
@@ -202,6 +208,31 @@ function sameStrings(left: readonly string[], right: readonly string[]): boolean
return left.length === right.length && left.every((value) => right.includes(value));
}
/** Validate every server-resolved value before consuming an opaque cursor. */
export function validateInventoryCursorUse(state: InventoryCursorState, lookup: InventoryCursorLookup): InventoryCursorValidation {
if (state.token !== lookup.token) return { valid: false, reason: 'token_mismatch' };
if (state.status === 'exhausted') return { valid: false, reason: 'exhausted' };
if (!Number.isFinite(lookup.now) || !Number.isFinite(state.pin.expiresAt) || lookup.now >= state.pin.expiresAt) return { valid: false, reason: 'expired' };
if (state.pin.callerUserId !== lookup.callerUserId) return { valid: false, reason: 'caller_mismatch' };
if (state.pin.queryFingerprint !== lookup.queryFingerprint) return { valid: false, reason: 'query_mismatch' };
if (!sameSelectedResourceList(state.pin.selectedResources, lookup.selectedResources)) return { valid: false, reason: 'resource_mismatch' };
if (!sameRecord(state.pin.authorizationRevisions, lookup.authorizationRevisions)
|| !sameRecord(state.pin.authorizationEpochs, lookup.authorizationEpochs)) return { valid: false, reason: 'authorization_mismatch' };
if (!sameRecord(state.pin.snapshotGenerations, lookup.snapshotGenerations)
|| state.pin.referenceVersion !== lookup.referenceVersion || state.pin.priceVersion !== lookup.priceVersion) return { valid: false, reason: 'input_version_mismatch' };
return { valid: true };
}
function sameSelectedResourceList(left: readonly SelectedResource[], right: readonly SelectedResource[]): boolean {
return left.length === right.length && left.every((resource) => right.some((candidate) => sameSelectedResource([resource], candidate)));
}
function sameRecord<T extends string | number | null>(left: Readonly<Record<string, T>>, right: Readonly<Record<string, T>>): boolean {
const leftKeys = Object.keys(left);
const rightKeys = Object.keys(right);
return leftKeys.length === rightKeys.length && leftKeys.every((key) => right[key] === left[key]);
}
/** 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' };
+34 -55
View File
@@ -27,6 +27,27 @@ export type InventoryType = {
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';
@@ -41,10 +62,6 @@ export interface CharacterAssetRepository {
characters(userId: UserId): Promise<Character[]>;
assets(userId: UserId, characterId: number): Promise<Asset[]>;
}
export interface ReferenceRepository {
types(ids: number[]): Promise<InventoryType[]>;
version(): Promise<string>;
}
export interface MarketEstimates {
estimates(typeIds: number[]): Promise<PriceSnapshot>;
}
@@ -401,6 +418,19 @@ export type InventoryCursorPin = {
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;
@@ -436,57 +466,6 @@ export type InventoryQueryResult =
| { 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';
+74
View File
@@ -0,0 +1,74 @@
import type { DecimalId, InventoryType, ReferenceVersion } from './model';
/** Narrow compatibility boundary retained for existing asset consumers. */
export interface ReferenceRepository {
types(ids: number[]): Promise<InventoryType[]>;
version(): Promise<string>;
}
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 ReferenceStationEvidence = 'verified' | 'system_listed';
export type ReferenceStationRecord = {
id: number; systemId: number; systemName: string | null; regionId: number | null;
name: string | null; typeId: number | null; ownerId: number | null;
/** Evidence distinguishes authoritative station rows from system-list-only IDs. */
evidence: ReferenceStationEvidence;
/** Compatibility projection; true exactly when evidence is `verified`. */
sourceVerified: boolean;
x: number | null; y: number | null; z: number | null;
};
/** T03-compatible bounded offset search; inventory cursors remain service-level. */
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;
snapshot(): ReferenceReaderHandle;
}
export type LocationFact = {
kind: 'region' | 'system' | 'station' | 'unknown';
id: DecimalId;
name: string | null;
parentId: DecimalId | null;
/** Null means the ID is absent; otherwise it preserves station source evidence. */
sourceVerified: boolean | null;
referenceVersion: ReferenceVersion;
};
/** Narrow service adapter; unknown IDs are explicit and names are never synthesized. */
export interface LocationReader {
location(id: DecimalId): Promise<LocationFact>;
}