/** * In-memory Personal Server + gateway that enforce the Write API contract the * way `personal-server-ts` does (routes/write-session.ts, api-auth.ts, * write/attribution.ts, contracts/binary.ts), so the SDK client is tested * against the real rules rather than a permissive stub: * * - handshake: Web3Signed proof, grantId claim required, grant must carry * `write:` entries, signer must be the grantee, proof single-use * - write: bearer must resolve to a live session, scope must be covered by * the session's write patterns, X-Vana-Write-Signature must recover to * the session builder over the STORED representation (body for JSON, * the `$binary` record for anything else), carry the session grantId as * a signed claim, JSON bodies must be compact, reserved keys rejected, * proof single-use; `lineage` is the body's top-level field (JSON) or * the metadata object's field (binary), validated per * docs/derivative-data-api.md and mirrored to `$lineage` * - derivative questions (`/v1/derivatives/questions`): the same builder * credential as a write (bearer + X-Vana-Write-Signature over the whole * request TARGET, query string INCLUDED and compared in canonical form), * the optional `nonce` claim as the replay key when present, authorized * against `write:`, compact JSON bodies, the consent rule * (every source scope read-granted to the builder), the cycle guard, the * 404 for a question this builder did not register AND for an unknown id * presented with a live write session, and the 400 * `DERIVATIVE_DERIVED_SCOPE_REQUIRED` for a builder list with no * `?derivedScope=` * - lineage reads on both the Personal Server and the gateway: Web3Signed * over the bare path (`/lineage[/:version]`, the version is a path * segment, any query is 400), grant view from the signed `grantId` claim * only, 401 for a missing / invalid gateway signature, a uniform 404 for * an unknown id and for a signer the gateway will not serve; answering * the `{ data, proof }` envelope with redaction for nodes the caller's * grant does not cover * * The binary representation is a verbatim port of the Personal Server's * `buildBinaryEnvelopeData` / `parseMetadataHeader` (Web Crypto + btoa), so * it is an independent oracle for the SDK's `binaryWriteSignedBytes`. */ import { type Address, type Hex } from "viem"; export interface MockGrant { id: string; grantorAddress: Address; granteeId: Address; scopes: string[]; revokedAt?: string | null; } export interface MockStoredRecord { scope: string; collectedAt: string; data: Record; } export interface MockLineageSource { dataPointId: Hex; scope: string; version: string; deletedAt: string | null; } export interface MockPersonalServerOptions { origin: string; owner: Address; grants: MockGrant[]; /** Data points a lineage source may reference (id -> node). */ knownDataPoints?: MockLineageSource[]; /** Fixed ingest status. */ status?: "stored" | "syncing"; sessionTtlSeconds?: number; now?: () => number; /** Answer every `/v1/derivatives` route 503, as a server with no compute. */ computeUnavailable?: boolean; } /** A question registration the mock server holds. */ export interface MockQuestion { questionId: string; derivedScope: string; sourceScopes: string[]; question: string; model: string | null; registeredBy: { kind: "owner"; } | { kind: "builder"; builder: Address; grantId: string; }; status: "pending" | "ready" | "failed" | "stale"; error: string | null; /** The coarse failure class the status route serves; null unless failed. */ errorCode: "inference_unavailable" | "source_missing" | "grant_invalid" | "internal" | null; /** * What the status route reports as the next automatic retry. The mock has * no scheduler, so a test sets it through `settleQuestion`. */ retryAfterSeconds: number | null; createdAt: string; updatedAt: string; lastComputedAt: string | null; derivedVersion: number | null; derivedCollectedAt: string | null; } export interface MockRequestLog { method: string; path: string; headers: Record; body: Uint8Array; } export interface MockPersonalServer { fetch: typeof fetch; origin: string; records: MockStoredRecord[]; requests: MockRequestLog[]; /** Handshake and write proofs consumed so far (sha-256 hex of the header). */ proofsSeen: Set; /** Make the next `n` fetches throw (transport failure) before answering. */ failNext(n: number, error?: Error): void; /** Force the next response (any route). */ respondNextWith(status: number, body: unknown): void; /** Sessions minted (token -> record). */ sessions: Map; /** Question registrations (id -> row), in registration order. */ questions: Map; /** * Forget every minted session, the way a restarted Personal Server does: * the next call with an old bearer answers 401. */ dropSessions(): void; /** * Settle a question the way a compute would: set its status and, for a * `ready` one, store the derived record the builder then reads. */ settleQuestion(questionId: string, outcome: { status: "ready"; data?: Record; } | { status: "failed"; error: string; errorCode?: MockQuestion["errorCode"]; retryAfterSeconds?: number | null; } | { status: "stale" | "pending"; }): void; } export interface MockSession { token: string; builderAddress: Address; grantId: string; writeScopes: string[]; expiresAtMs: number; } /** The Personal Server's `binaryWriteSignedBytes`, ported for the oracle. */ export declare function personalServerBinaryWriteSignedBytes(input: { bytes: Uint8Array; contentType: string; filename?: string; metadataHeader?: string; }): Promise; /** A Personal Server that enforces the Write API contract, as a `fetch`. */ export declare function createMockPersonalServer(options: MockPersonalServerOptions): MockPersonalServer; export interface MockGatewayOptions { origin: string; /** Lineage views by data point id (lowercase). */ graphs: Record; /** Builder address -> grant ids it holds (lowercase), for the 404 rule. */ grants?: Record; /** Wrap answers in the gateway `{ data, proof }` envelope (default true). */ envelope?: boolean; now?: () => number; } export interface MockGatewayView { dataPointId: Hex; ownerAddress?: Address; scope: string; version: string; deletedAt: string | null; sources: unknown[]; derivatives: unknown[]; derivativesTruncated?: boolean; } export interface MockGateway { fetch: typeof fetch; requests: MockRequestLog[]; } /** * A gateway answering `GET /v1/data/:id/lineage[/:version]`: the version is * a path segment and any query string is 400; the request must carry a * Web3Signed header whose `uri` is that bare path (401 * LINEAGE_SIGNATURE_REQUIRED / LINEAGE_SIGNATURE_INVALID otherwise); the * grant view is the signed `grantId` claim; an unknown id and a signer that * holds no such grant both answer a uniform 404. */ export declare function createMockGateway(options: MockGatewayOptions): MockGateway;