@opendatalabs/vana-sdk 3.23.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -0
- package/dist/auth/errors.cjs +1 -1
- package/dist/auth/web3-signed.cjs +1 -1
- package/dist/crypto/envelope/job-stream.test.d.ts +1 -0
- package/dist/crypto/envelope/job.cjs +593 -38
- package/dist/crypto/envelope/job.cjs.map +1 -1
- package/dist/crypto/envelope/job.d.ts +44 -27
- package/dist/crypto/envelope/job.js +589 -37
- package/dist/crypto/envelope/job.js.map +1 -1
- package/dist/direct/access-request-client.cjs +1 -1
- package/dist/direct/connect-flow.cjs +43 -1
- package/dist/direct/connect-flow.cjs.map +1 -1
- package/dist/direct/connect-flow.d.ts +12 -0
- package/dist/direct/connect-flow.js +43 -1
- package/dist/direct/connect-flow.js.map +1 -1
- package/dist/direct/controller.cjs +1 -1
- package/dist/direct/errors.cjs +1 -1
- package/dist/direct/escrow-payment.cjs.map +1 -1
- package/dist/direct/escrow-payment.d.ts +2 -2
- package/dist/direct/escrow-payment.js.map +1 -1
- package/dist/direct/personal-server-read.cjs +2 -2
- package/dist/direct/use-direct-vana-connect.cjs +2 -1
- package/dist/direct/use-direct-vana-connect.cjs.map +1 -1
- package/dist/direct/use-direct-vana-connect.d.ts +3 -1
- package/dist/direct/use-direct-vana-connect.js +2 -1
- package/dist/direct/use-direct-vana-connect.js.map +1 -1
- package/dist/error-entry-points.test.d.ts +1 -0
- package/dist/errors.cjs +7 -0
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.d.ts +10 -0
- package/dist/errors.js +6 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.browser.d.ts +4 -4
- package/dist/index.browser.js +882 -518
- package/dist/index.browser.js.map +4 -4
- package/dist/index.node.cjs +1035 -705
- package/dist/index.node.cjs.map +4 -4
- package/dist/index.node.d.ts +4 -4
- package/dist/index.node.js +989 -567
- package/dist/index.node.js.map +4 -4
- package/dist/protocol/data-point-deletion.cjs +1 -1
- package/dist/protocol/derivative-questions.cjs +1 -1
- package/dist/protocol/derivative-status.cjs +1 -1
- package/dist/protocol/eip712.cjs +28 -2
- package/dist/protocol/eip712.cjs.map +1 -1
- package/dist/protocol/eip712.d.ts +70 -0
- package/dist/protocol/eip712.js +24 -1
- package/dist/protocol/eip712.js.map +1 -1
- package/dist/protocol/escrow.cjs +154 -2
- package/dist/protocol/escrow.cjs.map +1 -1
- package/dist/protocol/escrow.d.ts +145 -2
- package/dist/protocol/escrow.js +152 -1
- package/dist/protocol/escrow.js.map +1 -1
- package/dist/protocol/fixtures/moksha-identity.json +20 -0
- package/dist/protocol/gateway.cjs +1 -1
- package/dist/protocol/gateway.cjs.map +1 -1
- package/dist/protocol/gateway.d.ts +8 -38
- package/dist/protocol/gateway.js.map +1 -1
- package/dist/protocol/identity.cjs +7 -2
- package/dist/protocol/identity.cjs.map +1 -1
- package/dist/protocol/identity.js +7 -2
- package/dist/protocol/identity.js.map +1 -1
- package/dist/protocol/jobs-client.cjs +73 -21
- package/dist/protocol/jobs-client.cjs.map +1 -1
- package/dist/protocol/jobs-client.d.ts +7 -5
- package/dist/protocol/jobs-client.js +74 -21
- package/dist/protocol/jobs-client.js.map +1 -1
- package/dist/protocol/jobs.cjs +0 -3
- package/dist/protocol/jobs.cjs.map +1 -1
- package/dist/protocol/jobs.d.ts +21 -13
- package/dist/protocol/jobs.js +0 -2
- package/dist/protocol/jobs.js.map +1 -1
- package/dist/protocol/lineage.cjs +1 -1
- package/dist/protocol/personal-server-data.cjs +1 -1
- package/dist/protocol/personal-server-write.cjs +1 -1
- package/dist/protocol/write-request.cjs +1 -1
- package/dist/protocol/write-signer.cjs +1 -1
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js.map +1 -1
- package/dist/session-relay/client.cjs +1 -1
- package/dist/session-relay/errors.cjs +1 -1
- package/dist/session-relay/index.cjs +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/protocol/gateway.ts"],"sourcesContent":["import { tryGrantPermissions, type GrantPermission } from \"./scope-actions\";\nimport {\n DataPointDeletedError,\n DataPointNotFoundError,\n DataPointVersionConflictError,\n} from \"../errors\";\nimport {\n isDataPointTombstone,\n tombstoneDeletedAt,\n} from \"./data-point-deletion\";\nimport { deriveDataPointId } from \"./lineage\";\nimport {\n isPlainObject,\n readJsonObject,\n readJsonValue,\n} from \"../utils/response-body\";\n\nexport interface GatewayEnvelope<T> {\n data: T;\n proof: GatewayProof;\n /**\n * Cursor-based pagination metadata, present on list endpoints (e.g.\n * `GET /v1/data`). A sibling of `data`, not nested inside it — so callers\n * that need it must read the full envelope rather than going through\n * `unwrapEnvelope`, which intentionally returns only `data`.\n */\n pagination?: GatewayPagination;\n}\n\nexport interface GatewayPagination {\n limit: number;\n hasMore: boolean;\n /**\n * Opaque cursor for the NEXT page; pass back as the `cursor` query param.\n * Null when there are no further pages.\n */\n nextCursor: string | null;\n}\n\nexport interface GatewayProof {\n signature: string;\n timestamp: string;\n gatewayAddress: string;\n requestHash: string;\n responseHash: string;\n userSignature: string;\n status: string;\n chainBlockHeight: number;\n}\n\nexport interface Builder {\n id: string;\n ownerAddress: string;\n granteeAddress: string;\n publicKey: string;\n appUrl: string;\n addedAt: string;\n}\n\nexport interface Schema {\n id: string;\n ownerAddress: string;\n name: string;\n definitionUrl: string;\n scope: string;\n addedAt: string;\n}\n\n// Row shape returned by `GET /v1/servers?owner=…`. A discovery-only list —\n// the endpoint carries no per-item gateway attestation (use\n// `getServer(serverAddress)` if you need a verifiable single-server proof).\n// `status` is the gateway's chain-sync state ('pending' | 'confirmed' |\n// 'finalized' | 'reorged'); kept as an open string since the gateway may add\n// states without a SDK bump.\nexport interface OwnerServerRecord {\n id: string;\n ownerAddress: string;\n serverAddress: string;\n publicKey: string;\n serverUrl: string;\n status: string;\n chainBlockHeight: string | null;\n // ISO timestamp of the gateway-side upsert.\n addedAt: string;\n // ISO timestamp when the grantor deregistered this server. Always null for\n // entries in the `active` list, always non-null for `revoked`.\n revokedAt: string | null;\n}\n\n// Response from `GET /v1/servers?owner=…`. Split into two arrays; each is\n// ordered newest-first (`addedAt DESC`, `id ASC` tiebreaker) so `active[0]`\n// is unambiguously the newest active server — the sensible default when the\n// caller just needs one URL. The gateway caps the total across both arrays\n// at 200 — per-owner counts are tiny in practice, so no cursor.\nexport interface OwnerServersResult {\n active: OwnerServerRecord[];\n revoked: OwnerServerRecord[];\n count: number;\n}\n\nexport interface ServerInfo {\n id: string;\n ownerAddress: string;\n serverAddress: string;\n publicKey: string;\n serverUrl: string;\n addedAt: string;\n // ISO timestamp when the grantor deregistered this server, null while\n // active. The gateway returns this on /v1/servers/:address GETs since\n // covering revocation in the response keeps the attestation hash\n // authoritative for both states.\n revokedAt: string | null;\n}\n\n// Fee annotation surfaced on every GET grant response. Amounts are decimal\n// uint256 strings to match `/v1/escrow/pay`'s wire format. totalDue is a\n// snapshot — the gateway re-resolves fees at pay time, so clients shouldn't\n// cache this across requests.\nexport interface GatewayGrantFee {\n asset: string;\n registrationFee: string;\n dataAccessFee: string;\n totalDue: string;\n}\n\n// Lifecycle of a grant's on-chain settlement, tracked separately from the\n// fee-payment lifecycle (paymentStatus). Driven by POST /v1/settle:\n// pending — nothing on-chain yet\n// submitting — settle tx broadcast but receipt not yet observed\n// confirmed — settle tx mined successfully\n// finalized — finalized tip past the settle block, reorg-safe\n// reorged — finalized observation reverted; back to 'pending' on next settle\nexport type GatewayGrantStatus =\n | \"pending\"\n | \"submitting\"\n | \"confirmed\"\n | \"finalized\"\n | \"reorged\";\n\nexport interface GatewayGrantResponse {\n id: string;\n grantorAddress: string;\n granteeId: string;\n // The signed scope entries, verbatim (`[operation:]scope`, bare = read).\n // This is the wire form: it is what the grantor signed and what the\n // gateway stores. Render `permissions` instead of parsing these.\n scopes: string[];\n // Derived by the SDK at read time from `scopes` via grantPermissions():\n // one `{ scope, actions }` row per scope pattern, canonically ordered.\n // Never sent to the gateway and never part of the signed payload. Absent\n // when `scopes` carries an entry this SDK version cannot interpret (an\n // operation introduced after this release) - fall back to `scopes` and\n // do not assume such a grant is read-only.\n permissions?: GrantPermission[];\n status: GatewayGrantStatus;\n addedAt: string;\n // Grantor-signed deadline. null = perpetual grant (signed value was 0).\n expiresAt: string | null;\n // Derived at read time from expiresAt vs the gateway's clock — a snapshot,\n // not a cached truth. Re-check against expiresAt locally if you care.\n expired: boolean;\n revokedAt: string | null;\n revocationSignature: string | null;\n // 'pending' until the grant registration fee is settled via /v1/escrow/pay.\n paymentStatus: \"pending\" | \"paid\";\n paidAt: string | null;\n paidBy: string | null;\n // Decimal-string uint256 monotonic nonce; advances on every state change.\n grantVersion: string;\n // Settle metadata — populated as the grant progresses through the chain\n // lifecycle. Null while `status === 'pending'`.\n settleTxHash: string | null;\n settleSubmittedAt: string | null;\n // Revocation metadata — populated independently when the grantor signs\n // and the gateway pushes a deregister tx.\n revocationTxHash: string | null;\n revocationSubmittedAt: string | null;\n fee: GatewayGrantFee;\n}\n\nexport type GrantListItem = GatewayGrantResponse;\n\n// Mirror of a DataRegistryV2 row as the gateway exposes it. `id` is the\n// deterministic `keccak256(abi.encode(owner, scope))` dataPointId — the same\n// value the contract uses as its primary key. `expectedVersion` is the latest\n// version the gateway has accepted; the on-chain row may be one version behind\n// while a settle is still pending. The gateway response intentionally drops\n// `status` from the body — read it from the on-chain contract when you need\n// the canonical lifecycle state.\nexport interface DataPointRecord {\n id: string;\n ownerAddress: string;\n scope: string;\n dataHash: string;\n metadataHash: string;\n // Decimal-string uint256.\n expectedVersion: string;\n // ISO 8601 timestamp of the most recent gateway-side upsert.\n addedAt: string;\n // ISO 8601 timestamp of the deletion tombstone, null/absent while live.\n // Only present on reads that opted in via `includeDeleted`; a plain\n // `getDataPoint` of a deleted row throws `DataPointDeletedError` instead.\n deletedAt?: string | null;\n}\n\nexport interface GetDataPointOptions {\n /**\n * Return the row even after it was deleted (with `deletedAt` set and the\n * tombstone hash pair) instead of throwing `DataPointDeletedError`.\n */\n includeDeleted?: boolean;\n}\n\nexport interface DataPointListResult {\n dataPoints: DataPointRecord[];\n cursor: string | null;\n}\n\nexport interface ListDataPointsOptions {\n /**\n * Only return rows added at or after this ISO 8601 timestamp. Used by sync\n * loops that want incremental tails — pass the last seen `addedAt`.\n */\n since?: string;\n /** Page size. Capped at 1000 by the gateway. */\n limit?: number;\n /**\n * Include deleted (tombstoned) rows, each carrying `deletedAt`. Off by\n * default; without it the SDK also drops any tombstone the gateway leaks.\n */\n includeDeleted?: boolean;\n}\n\n// grantVersion and expiresAt are decimal-string uint256s — same wire format\n// the gateway expects. The caller is responsible for converting their bigint\n// to a decimal string and for signing GRANT_REGISTRATION_TYPES with matching\n// bigint values.\nexport interface CreateGrantParams {\n grantorAddress: string;\n granteeId: string;\n scopes: string[];\n grantVersion: string;\n expiresAt: string;\n signature: string;\n}\n\nexport interface RevokeGrantParams {\n grantId: string;\n grantorAddress: string;\n grantVersion: string;\n signature: string;\n}\n\nexport interface RegisterServerParams {\n ownerAddress: string;\n serverAddress: string;\n publicKey: string;\n serverUrl: string;\n signature: string;\n}\n\nexport interface RegisterServerResult {\n serverId?: string;\n alreadyRegistered: boolean;\n}\n\nexport interface RegisterBuilderParams {\n ownerAddress: string;\n // Wallet the builder authenticates to the Personal Server with. The\n // builderId is deterministically derived from (owner, grantee, publicKey,\n // appUrl) so this triple pins the on-chain identity.\n granteeAddress: string;\n publicKey: string;\n appUrl: string;\n signature: string;\n}\n\nexport interface RegisterBuilderResult {\n builderId?: string;\n alreadyRegistered: boolean;\n}\n\n// AddData on DataRegistryV2. dataHash + metadataHash are bytes32 commitments\n// to the off-chain payload + its metadata. expectedVersion is a CAS knob —\n// the gateway/contract rejects on 409 if a higher version is already stored,\n// and the error body surfaces `currentExpectedVersion` so callers can re-sign.\nexport interface RegisterDataPointParams {\n ownerAddress: string;\n scope: string;\n dataHash: string;\n metadataHash: string;\n expectedVersion: string;\n signature: string;\n}\n\nexport interface RegisterDataPointResult {\n dataPointId?: string;\n expectedVersion?: string;\n}\n\n// Deletion = an owner-signed AddData for version current+1 carrying the\n// tombstone hash pair (see protocol/data-point-deletion.ts). The gateway\n// reconstructs the AddData from the body plus the tombstone constants and\n// recovers the signer, so the hashes are NOT on the wire. Used at\n// DELETE /v1/data/:dataPointId. expectedVersion is the tombstone's version\n// as a decimal uint256 string; the signature is the raw EIP-712 hex.\nexport interface DeleteDataPointParams {\n ownerAddress: string;\n scope: string;\n expectedVersion: string;\n signature: string;\n}\n\n// The tombstone version record the gateway returns on 200. Fields are\n// optional because, like RegisterDataPointResult, the SDK tolerates either\n// a bare body or an enveloped `{data}`; `deletedAt` is the durable marker.\nexport interface DeleteDataPointResult {\n dataPointId?: string;\n ownerAddress?: string;\n scope?: string;\n dataHash?: string;\n metadataHash?: string;\n expectedVersion?: string;\n deletedAt?: string | null;\n}\n\n// ── Escrow / data-access payment path ───────────────────────────────────────\n// /v1/escrow/pay debits the payer's escrow balance for a payable op. For a\n// Legacy grants use opType='grant' and opId=grantId. Standalone receipt-bound\n// reads use opType='data_access' and opId=accessRecord.recordId. amount and\n// paymentNonce are decimal uint256 strings on the wire. The signature is the\n// raw EIP-712 hex of GENERIC_PAYMENT_TYPES against escrowPaymentDomain.\n\n// A server-signed delivery receipt attached to a data-access payment. The\n// signature is over RECORD_DATA_ACCESS_TYPES against dataRegistryDomain; the\n// signer must be a personal server the data point's owner has registered as\n// trusted. Shape validation in clients does not verify this signature; the\n// gateway verifies it before re-using it on-chain in the next /v1/settle pass\n// via DataRegistryV2.recordDataAccess, where `recordId` dedupes via\n// `_usedRecordIds`.\nexport interface AccessRecord {\n dataPointId: string;\n // Decimal-string uint256 — the data point version being attested to.\n version: string;\n // Must equal the enclosing payment's payerAddress (the gateway enforces).\n accessor: string;\n recordId: string;\n signature: string;\n}\n\nexport interface PayForOperationParams {\n payerAddress: string;\n opType: string;\n opId: string;\n asset: string;\n amount: string;\n // Per-payer monotonic; (payer, nonce, kind) must be unique. The gateway\n // returns 409 if reused — bump and re-sign.\n paymentNonce: string;\n signature: string;\n // Optional: attach a server-signed access record so the next /v1/settle\n // pass submits a recordDataAccess tx alongside the payment settlement.\n // Required for data-access payments (the second-and-onward payments per\n // grant) that want their on-chain `totalAccesses` counter to advance.\n accessRecord?: AccessRecord;\n}\n\nexport interface PayForOperationResult {\n opType: string;\n opId: string;\n payerAddress: string;\n asset: string;\n amount: string;\n // Echoes how the gateway split this payment. `registrationPaid` is true on\n // the first payment for a grant (which bundles both fees) and false on\n // subsequent data-access-only payments. Off-chain ledger state only — the\n // on-chain settlement of the registration is tracked by the grant's\n // `status` field, not this flag.\n breakdown: {\n registrationFee: string;\n dataAccessFee: string;\n registrationPaid: boolean;\n };\n paymentNonce: string;\n paidAt: string;\n}\n\n// ── Settle / reconcile ──────────────────────────────────────────────────────\n// POST /v1/settle drains pending-on-chain rows (grants, servers, builders,\n// data points, data-point statuses, access records) to the relayer, then\n// promotes 'submitting' → 'confirmed' and 'confirmed' → 'finalized' for\n// previously-submitted rows. One call does all three; the response surfaces\n// each phase's outcomes.\n\n// The op-types the settle endpoint emits in `items[]`. Kept as a union so\n// callers can narrow inside the discriminated SettleItem shape. Mirrors the\n// gateway's drain phases (drainGrants/drainServers/drainBuilders/\n// drainDataPoints/drainDataPointStatuses/drainAccessRecords) — keep in sync\n// when the gateway adds a drain.\nexport type SettleOpType =\n | \"grant\"\n | \"server\"\n | \"data\"\n | \"access\"\n | \"builder\"\n | \"data-status\";\n\nexport type SettleItem =\n | {\n opType: SettleOpType;\n opId: string;\n // 'confirmed' when the submit function waited for the receipt and it\n // mined (registerAndSettle path); 'submitting' when only the tx was\n // sent (no receipt wait).\n status: \"submitting\" | \"confirmed\";\n settleTxHash: string | null;\n settleSubmittedAt: string | null;\n // Block height the tx mined in; only set when status === 'confirmed'.\n chainBlockHeight: string | null;\n revocationTxHash: string | null;\n revocationSubmittedAt: string | null;\n // True while lib/settle.ts is in placeholder mode for this row's pass.\n placeholder: boolean;\n }\n | {\n opType: SettleOpType;\n opId: string;\n status: \"skipped\";\n reason: string;\n }\n | {\n opType: SettleOpType;\n opId: string;\n status: \"failed\";\n error: string;\n };\n\n// Outcome of the housekeeping pass that retries earlier `submitting` rows\n// whose receipt arrived after the prior /v1/settle's wait budget elapsed.\nexport interface SettlePromoteResult {\n opType: SettleOpType;\n opId: string;\n status: \"confirmed\" | \"failed\" | \"pending\" | \"skipped\";\n txHash: string;\n chainBlockHeight: string | null;\n reason?: string;\n}\n\n// Outcome of the reconcile pass that advances 'confirmed' → 'finalized' once\n// the chain's finalized tip catches up past the tx's block (or reverts to\n// 'pending' on reorg detection).\nexport interface SettleReconcileItem {\n opId: string;\n status: \"finalized\" | \"reorged\" | \"unchanged\";\n chainBlockHeight: string | null;\n settleTxHash: string | null;\n reason?: string;\n}\n\nexport interface SettleParams {\n // Per-phase cap. Bounded by MAX_LIMIT on the gateway side; omit to use\n // the gateway's default BATCH_LIMIT.\n limit?: number;\n}\n\nexport interface SettleResult {\n scanned: number;\n submitted: number;\n confirmed: number;\n skipped: number;\n failed: number;\n items: SettleItem[];\n promoted: { count: number; items: SettlePromoteResult[] };\n reconciled: {\n scanned: number;\n finalized: number;\n reorged: number;\n unchanged: number;\n items: SettleReconcileItem[];\n };\n // Present only when the gateway is configured for paced submission —\n // spreads work across several blocks within one /v1/settle invocation.\n paced?: { iterations: number };\n}\n\n// /v1/escrow/balance?account=... — pure read. Returns finalized balances by\n// asset, plus the lifecycle breakdown of deposits.\nexport interface EscrowBalanceEntry {\n asset: string;\n // Gross credited deposits for (account, asset). Decremented only when the\n // reconcile pass marks a payment finalized — NOT on /v1/escrow/pay.\n balance: string;\n // Sum of claimedAmount for deposits still in 'submitted' status — surfaced\n // separately so clients don't conflate \"credited\" with \"deposit announced\n // but not yet confirmed.\"\n pendingAmount: string;\n // Sum of payments.amount for (account, asset) regardless of settled status —\n // mirrors the /v1/escrow/pay handler's soft-lock counter. Subtract from\n // `balance` to see how much the payer can still authorise.\n authorizedAmount: string;\n // `max(balance − authorizedAmount, 0)`. The headroom a payer has against\n // the soft-lock before /v1/escrow/pay starts returning 402.\n availableAmount: string;\n updatedAt: string | null;\n}\n\nexport interface EscrowDepositSubmitted {\n txHash: string;\n submittedAt: string;\n claimedAsset: string;\n claimedAmount: string;\n}\n\nexport interface EscrowDepositFinalized {\n txHash: string;\n finalizedAt: string | null;\n blockNumber: string | null;\n claimedAsset: string;\n claimedAmount: string;\n}\n\nexport interface EscrowDepositFailed {\n txHash: string;\n submittedAt: string;\n claimedAsset: string;\n claimedAmount: string;\n lastError: string | null;\n}\n\nexport interface EscrowBalance {\n account: string;\n balances: EscrowBalanceEntry[];\n deposits: {\n submitted: EscrowDepositSubmitted[];\n finalized: EscrowDepositFinalized[];\n failed: EscrowDepositFailed[];\n };\n}\n\n// /v1/escrow/deposit announces an on-chain deposit tx so the gateway can\n// reconcile it into the payer's balance. The gateway extracts the credited\n// account from calldata — no off-chain claim about who paid.\nexport interface SubmitDepositParams {\n txHash: string;\n}\n\nexport interface DepositState {\n txHash: string;\n account: string;\n // 'submitted' | 'finalized' | 'failed' — kept open since the gateway adds\n // states (e.g. 'orphaned') as the deposit flow evolves.\n status: string;\n blockNumber: string | null;\n submittedAt: string;\n finalizedAt: string | null;\n lastError: string | null;\n}\n\nexport interface GatewayClient {\n isRegisteredBuilder(address: string): Promise<boolean>;\n getBuilder(address: string): Promise<Builder | null>;\n getGrant(grantId: string): Promise<GatewayGrantResponse | null>;\n listGrantsByUser(userAddress: string): Promise<GrantListItem[]>;\n getSchemaForScope(scope: string): Promise<Schema | null>;\n getServer(address: string): Promise<ServerInfo | null>;\n /**\n * List every personal server an owner has registered — split into `active`\n * (currently trusted) and `revoked` (deregistered). Each list is ordered\n * newest-first; `active[0]` is the sensible default when the caller just\n * needs one URL for the owner. Empty owner returns `{active: [], revoked: [], count: 0}`.\n * Discovery-only endpoint — no per-server attestation; use `getServer` for that.\n */\n listServersByOwner(owner: string): Promise<OwnerServersResult>;\n /**\n * Fetch a single data point by its deterministic id (keccak256 of (owner, scope)).\n * Returns null on 404. Throws `DataPointDeletedError` on 410, or when the row is a\n * tombstone, unless `options.includeDeleted` is set. The gateway omits `status` from\n * the response body -- read it from the on-chain DataRegistryV2 contract when you need\n * the canonical lifecycle state.\n */\n getDataPoint(\n dataPointId: string,\n options?: GetDataPointOptions,\n ): Promise<DataPointRecord | null>;\n /**\n * Page through an owner's data points. Cursor is opaque; pass `null` for the first\n * page and feed back `result.cursor` until it returns null.\n * Deleted rows are excluded unless `options.includeDeleted` is set.\n */\n listDataPointsByOwner(\n owner: string,\n cursor: string | null,\n options?: ListDataPointsOptions,\n ): Promise<DataPointListResult>;\n getSchema(schemaId: string): Promise<Schema | null>;\n registerServer(params: RegisterServerParams): Promise<RegisterServerResult>;\n registerBuilder(\n params: RegisterBuilderParams,\n ): Promise<RegisterBuilderResult>;\n registerDataPoint(\n params: RegisterDataPointParams,\n ): Promise<RegisterDataPointResult>;\n /**\n * Tombstone a data point: `DELETE /v1/data/:dataPointId` with the owner's\n * AddData signature for version `current + 1`. The dataPointId is derived\n * from (ownerAddress, scope). Throws `DataPointVersionConflictError` on\n * 409, `DataPointDeletedError` on 410 (already deleted),\n * `DataPointNotFoundError` on 404.\n */\n deleteDataPoint(\n params: DeleteDataPointParams,\n ): Promise<DeleteDataPointResult>;\n createGrant(params: CreateGrantParams): Promise<{ grantId?: string }>;\n revokeGrant(params: RevokeGrantParams): Promise<void>;\n getEscrowBalance(account: string): Promise<EscrowBalance>;\n submitEscrowDeposit(params: SubmitDepositParams): Promise<DepositState>;\n payForOperation(\n params: PayForOperationParams,\n ): Promise<PayForOperationResult>;\n settle(params?: SettleParams): Promise<SettleResult>;\n}\n\n// Attach the derived `permissions` view to a grant record read back from the\n// gateway. The wire `scopes` are left untouched.\nfunction withGrantPermissions<T extends GatewayGrantResponse>(grant: T): T {\n // Gateway responses are untrusted runtime data despite their type. The\n // `permissions` view is SDK-owned: drop whatever the body carried under\n // that name, then derive only from a well-formed string[] and leave the\n // field absent otherwise, so a spoofed or stale value can never be\n // rendered as this grant's authority.\n const stripped = { ...grant };\n delete stripped.permissions;\n const scopes: unknown = stripped.scopes;\n if (!Array.isArray(scopes)) {\n return stripped;\n }\n const permissions = tryGrantPermissions(scopes as string[]);\n return permissions === undefined ? stripped : { ...stripped, permissions };\n}\n\nexport function createGatewayClient(baseUrl: string): GatewayClient {\n const base = baseUrl.replace(/\\/+$/, \"\");\n\n function malformedBody(res: Response): Error {\n return new Error(\n `Gateway error: ${res.status} malformed response body (expected a JSON envelope)`,\n );\n }\n\n // GET responses are `{ data, proof, pagination? }`. A 200 whose body is\n // empty, not JSON, or not an object with `data` is a gateway bug, not a\n // record: fail with a clear error instead of returning undefined.\n async function readEnvelope<T>(\n res: Response,\n ): Promise<Record<string, unknown> & { data: T }> {\n const envelope = await readJsonValue(res);\n if (!isPlainObject(envelope) || !(\"data\" in envelope)) {\n throw malformedBody(res);\n }\n return envelope as Record<string, unknown> & { data: T };\n }\n\n async function unwrapEnvelope<T>(res: Response): Promise<T> {\n return (await readEnvelope<T>(res)).data;\n }\n\n function getMutationId(body: unknown, key: string): string | undefined {\n if (!isPlainObject(body)) return undefined;\n const value = body[key] ?? body[\"id\"];\n return typeof value === \"string\" ? value : undefined;\n }\n\n // Mutation responses are bare JSON; GET responses are enveloped. Accept\n // either so a gateway that wraps the tombstone record still round-trips.\n // Any body that is not a plain object (empty 204, `null`, an array, an\n // HTML error page) reads as `{}` so the status-code mapping still wins.\n async function readBody(res: Response): Promise<Record<string, unknown>> {\n const raw = await readJsonObject(res);\n const data = raw[\"data\"];\n if (isPlainObject(data) && \"proof\" in raw) {\n return data;\n }\n return raw;\n }\n\n function stringOrUndefined(value: unknown): string | undefined {\n return typeof value === \"string\" ? value : undefined;\n }\n\n async function deletedError(\n res: Response,\n details: { dataPointId?: string; scope?: string; ownerAddress?: string },\n ): Promise<DataPointDeletedError> {\n const body = await readBody(res);\n return new DataPointDeletedError(\n `Data point ${details.dataPointId ?? details.scope ?? \"\"} has been deleted`,\n { ...details, deletedAt: tombstoneDeletedAt(body) },\n );\n }\n\n return {\n async isRegisteredBuilder(address: string): Promise<boolean> {\n const builder = await this.getBuilder(address);\n return builder !== null;\n },\n\n async getBuilder(address: string): Promise<Builder | null> {\n const res = await fetch(`${base}/v1/builders/${address}`);\n if (res.status === 404) return null;\n if (!res.ok) {\n throw new Error(`Gateway error: ${res.status} ${res.statusText}`);\n }\n return unwrapEnvelope<Builder>(res);\n },\n\n async getGrant(grantId: string): Promise<GatewayGrantResponse | null> {\n const res = await fetch(`${base}/v1/grants/${grantId}`);\n if (res.status === 404) return null;\n if (!res.ok) {\n throw new Error(`Gateway error: ${res.status} ${res.statusText}`);\n }\n return withGrantPermissions(\n await unwrapEnvelope<GatewayGrantResponse>(res),\n );\n },\n\n async listGrantsByUser(userAddress: string): Promise<GrantListItem[]> {\n const res = await fetch(`${base}/v1/grants?user=${userAddress}`);\n if (res.status === 404) return [];\n if (!res.ok) {\n throw new Error(`Gateway error: ${res.status} ${res.statusText}`);\n }\n const grants = await unwrapEnvelope<GrantListItem[]>(res);\n return grants.map(withGrantPermissions);\n },\n\n async getSchemaForScope(scope: string): Promise<Schema | null> {\n const res = await fetch(`${base}/v1/schemas?scope=${scope}`);\n if (res.status === 404) return null;\n if (!res.ok) {\n throw new Error(`Gateway error: ${res.status} ${res.statusText}`);\n }\n return unwrapEnvelope<Schema>(res);\n },\n\n async getServer(address: string): Promise<ServerInfo | null> {\n const res = await fetch(`${base}/v1/servers/${address}`);\n if (res.status === 404) return null;\n if (!res.ok) {\n throw new Error(`Gateway error: ${res.status} ${res.statusText}`);\n }\n return unwrapEnvelope<ServerInfo>(res);\n },\n\n async listServersByOwner(owner: string): Promise<OwnerServersResult> {\n // URLSearchParams encodes the owner so a malformed value (e.g.\n // \"0xabc&foo=bar\") can't inject extra query params. Same pattern as\n // listDataPointsByOwner.\n const params = new URLSearchParams({ owner });\n const res = await fetch(`${base}/v1/servers?${params.toString()}`);\n if (!res.ok) {\n throw new Error(`Gateway error: ${res.status} ${res.statusText}`);\n }\n // Unlike /v1/servers/:address, the list endpoint returns the body\n // directly — no GatewayEnvelope, no attestation (discovery-only).\n return (await res.json()) as OwnerServersResult;\n },\n\n async getDataPoint(\n dataPointId: string,\n options?: GetDataPointOptions,\n ): Promise<DataPointRecord | null> {\n const query = options?.includeDeleted ? \"?includeDeleted=true\" : \"\";\n const res = await fetch(`${base}/v1/data/${dataPointId}${query}`);\n if (res.status === 404) return null;\n if (res.status === 410) {\n throw await deletedError(res, { dataPointId });\n }\n if (!res.ok) {\n throw new Error(`Gateway error: ${res.status} ${res.statusText}`);\n }\n const record = await unwrapEnvelope<DataPointRecord>(res);\n // Never hand a tombstone back as data unless the caller opted in --\n // guards against a gateway that returns 200 + deletedAt on plain reads.\n if (!options?.includeDeleted && isDataPointTombstone(record)) {\n throw new DataPointDeletedError(\n `Data point ${dataPointId} has been deleted`,\n {\n dataPointId,\n scope: record.scope,\n ownerAddress: record.ownerAddress,\n deletedAt: tombstoneDeletedAt(record),\n },\n );\n }\n return record;\n },\n\n async listDataPointsByOwner(\n owner: string,\n cursor: string | null,\n options?: ListDataPointsOptions,\n ): Promise<DataPointListResult> {\n const params = new URLSearchParams({ user: owner });\n if (cursor !== null) {\n params.set(\"cursor\", cursor);\n }\n if (options?.since) {\n params.set(\"since\", options.since);\n }\n if (options?.limit !== undefined) {\n params.set(\"limit\", String(options.limit));\n }\n if (options?.includeDeleted) {\n params.set(\"includeDeleted\", \"true\");\n }\n const res = await fetch(`${base}/v1/data?${params.toString()}`);\n if (!res.ok) {\n throw new Error(`Gateway error: ${res.status} ${res.statusText}`);\n }\n // Next-page cursor lives in the envelope's `pagination.nextCursor`\n // (a sibling of `data`), so read the full envelope rather than going\n // through `unwrapEnvelope`, which returns only `data`.\n const envelope = await readEnvelope<unknown>(res);\n if (\n !isPlainObject(envelope.data) ||\n !Array.isArray(envelope.data[\"dataPoints\"])\n ) {\n throw malformedBody(res);\n }\n const rows = envelope.data[\"dataPoints\"] as DataPointRecord[];\n const pagination = isPlainObject(envelope[\"pagination\"])\n ? envelope[\"pagination\"]\n : undefined;\n const rawCursor = pagination?.[\"nextCursor\"];\n const nextCursor =\n pagination?.[\"hasMore\"] === false || typeof rawCursor !== \"string\"\n ? null\n : rawCursor;\n // Tombstones only surface when asked for; drop any the gateway leaks\n // on a plain list so sync loops never ingest one as data.\n const dataPoints = options?.includeDeleted\n ? rows\n : rows.filter((row) => !isDataPointTombstone(row));\n return {\n dataPoints,\n cursor: nextCursor,\n };\n },\n\n async getSchema(schemaId: string): Promise<Schema | null> {\n const res = await fetch(`${base}/v1/schemas/${schemaId}`);\n if (res.status === 404) return null;\n if (!res.ok) {\n throw new Error(`Gateway error: ${res.status} ${res.statusText}`);\n }\n return unwrapEnvelope<Schema>(res);\n },\n\n async registerServer(\n params: RegisterServerParams,\n ): Promise<RegisterServerResult> {\n const res = await fetch(`${base}/v1/servers`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Web3Signed ${params.signature}`,\n },\n body: JSON.stringify({\n ownerAddress: params.ownerAddress,\n serverAddress: params.serverAddress,\n publicKey: params.publicKey,\n serverUrl: params.serverUrl,\n }),\n });\n if (res.status === 409) {\n const body = await readJsonObject(res);\n return {\n serverId: getMutationId(body, \"serverId\"),\n alreadyRegistered: true,\n };\n }\n if (!res.ok) {\n throw new Error(`Gateway error: ${res.status} ${res.statusText}`);\n }\n const body = await readJsonObject(res);\n return {\n serverId: getMutationId(body, \"serverId\"),\n alreadyRegistered: false,\n };\n },\n\n async registerBuilder(\n params: RegisterBuilderParams,\n ): Promise<RegisterBuilderResult> {\n const res = await fetch(`${base}/v1/builders`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Web3Signed ${params.signature}`,\n },\n body: JSON.stringify({\n ownerAddress: params.ownerAddress,\n granteeAddress: params.granteeAddress,\n publicKey: params.publicKey,\n appUrl: params.appUrl,\n }),\n });\n // 409 is idempotent — the gateway's current 409 body doesn't include\n // the builderId, but we tolerate it in case that changes (mirrors the\n // registerServer / createGrant shape).\n if (res.status === 409) {\n const body = await readJsonObject(res);\n return {\n builderId: getMutationId(body, \"builderId\"),\n alreadyRegistered: true,\n };\n }\n if (!res.ok) {\n throw new Error(`Gateway error: ${res.status} ${res.statusText}`);\n }\n const body = await readJsonObject(res);\n return {\n builderId: getMutationId(body, \"builderId\"),\n alreadyRegistered: false,\n };\n },\n\n async registerDataPoint(\n params: RegisterDataPointParams,\n ): Promise<RegisterDataPointResult> {\n const res = await fetch(`${base}/v1/data`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Web3Signed ${params.signature}`,\n },\n body: JSON.stringify({\n ownerAddress: params.ownerAddress,\n scope: params.scope,\n dataHash: params.dataHash,\n metadataHash: params.metadataHash,\n expectedVersion: params.expectedVersion,\n }),\n });\n // 409 is a real failure here (stale CAS), not an idempotent replay —\n // surface the gateway's error message verbatim so the caller knows\n // what `currentExpectedVersion` to re-sign against.\n if (!res.ok) {\n const body = await readJsonObject(res);\n const detail = stringOrUndefined(body[\"error\"]) ?? res.statusText;\n throw new Error(`Gateway error: ${res.status} ${detail}`);\n }\n const body = await readJsonObject(res);\n return {\n dataPointId: getMutationId(body, \"dataPointId\"),\n expectedVersion: stringOrUndefined(body[\"expectedVersion\"]),\n };\n },\n\n async deleteDataPoint(\n params: DeleteDataPointParams,\n ): Promise<DeleteDataPointResult> {\n // The gateway keys the row on keccak256(abi.encode(owner, scope)) --\n // derive it locally rather than asking the caller for it, so the path\n // can never disagree with the signed (owner, scope).\n const dataPointId = deriveDataPointId(\n params.ownerAddress as `0x${string}`,\n params.scope,\n );\n const details = {\n dataPointId,\n scope: params.scope,\n ownerAddress: params.ownerAddress,\n };\n // Signature rides both in the body (the DELETE contract) and as the\n // Web3Signed Authorization header every other /v1 mutation uses.\n const res = await fetch(`${base}/v1/data/${dataPointId}`, {\n method: \"DELETE\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Web3Signed ${params.signature}`,\n },\n body: JSON.stringify({\n ownerAddress: params.ownerAddress,\n scope: params.scope,\n expectedVersion: params.expectedVersion,\n signature: params.signature,\n }),\n });\n if (res.status === 404) {\n throw new DataPointNotFoundError(\n `Data point ${dataPointId} (scope '${params.scope}') is not registered`,\n details,\n );\n }\n if (res.status === 409) {\n const body = await readBody(res);\n const currentExpectedVersion = stringOrUndefined(\n body[\"currentExpectedVersion\"],\n );\n const detail = stringOrUndefined(body[\"error\"]) ?? res.statusText;\n throw new DataPointVersionConflictError(\n `Gateway error: 409 ${detail}`,\n {\n ...details,\n expectedVersion: params.expectedVersion,\n currentExpectedVersion,\n },\n );\n }\n if (res.status === 410) {\n throw await deletedError(res, details);\n }\n if (!res.ok) {\n // 400 = the recovered signer/hashes do not match the tombstone\n // AddData. Surface the gateway's message verbatim, like\n // registerDataPoint does.\n const body = await readBody(res);\n const detail = stringOrUndefined(body[\"error\"]) ?? res.statusText;\n throw new Error(`Gateway error: ${res.status} ${detail}`);\n }\n const body = await readBody(res);\n return {\n dataPointId: getMutationId(body, \"dataPointId\") ?? dataPointId,\n ownerAddress: stringOrUndefined(body[\"ownerAddress\"]),\n scope: stringOrUndefined(body[\"scope\"]),\n dataHash: stringOrUndefined(body[\"dataHash\"]),\n metadataHash: stringOrUndefined(body[\"metadataHash\"]),\n expectedVersion: stringOrUndefined(body[\"expectedVersion\"]),\n deletedAt: tombstoneDeletedAt(body),\n };\n },\n\n async createGrant(\n params: CreateGrantParams,\n ): Promise<{ grantId?: string }> {\n const res = await fetch(`${base}/v1/grants`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Web3Signed ${params.signature}`,\n },\n body: JSON.stringify({\n grantorAddress: params.grantorAddress,\n granteeId: params.granteeId,\n scopes: params.scopes,\n grantVersion: params.grantVersion,\n expiresAt: params.expiresAt,\n }),\n });\n if (res.status === 409) {\n const body = await readJsonObject(res);\n return { grantId: getMutationId(body, \"grantId\") };\n }\n if (!res.ok) {\n throw new Error(`Gateway error: ${res.status} ${res.statusText}`);\n }\n const body = await readJsonObject(res);\n return { grantId: getMutationId(body, \"grantId\") };\n },\n\n async revokeGrant(params: RevokeGrantParams): Promise<void> {\n const res = await fetch(`${base}/v1/grants/${params.grantId}`, {\n method: \"DELETE\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Web3Signed ${params.signature}`,\n },\n body: JSON.stringify({\n grantorAddress: params.grantorAddress,\n grantVersion: params.grantVersion,\n }),\n });\n if (res.status === 409) return;\n if (!res.ok) {\n throw new Error(`Gateway error: ${res.status} ${res.statusText}`);\n }\n },\n\n async getEscrowBalance(account: string): Promise<EscrowBalance> {\n const res = await fetch(`${base}/v1/escrow/balance?account=${account}`);\n if (!res.ok) {\n throw new Error(`Gateway error: ${res.status} ${res.statusText}`);\n }\n // Unlike the rest of /v1, the balance endpoint returns the body\n // directly (no GatewayEnvelope wrap) — it's a pure read with no\n // gateway-signed attestation. See data-gateway api/v1/escrow/balance.ts.\n return (await res.json()) as EscrowBalance;\n },\n\n async submitEscrowDeposit(\n params: SubmitDepositParams,\n ): Promise<DepositState> {\n const res = await fetch(`${base}/v1/escrow/deposit`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ txHash: params.txHash }),\n });\n // The gateway returns 202 for \"accepted (still confirming)\" and 200 for\n // duplicate idempotent replays. Both carry the deposit's current state.\n if (res.status !== 200 && res.status !== 202) {\n throw new Error(`Gateway error: ${res.status} ${res.statusText}`);\n }\n return (await res.json()) as DepositState;\n },\n\n async payForOperation(\n params: PayForOperationParams,\n ): Promise<PayForOperationResult> {\n // Build the body without the accessRecord key when absent so the\n // gateway's \"missing optional\" branch matches the no-receipt case\n // exactly (an explicit `accessRecord: undefined` would JSON-serialize\n // to nothing — same result — but keeping it conditional makes wire\n // traces easier to read).\n const body: Record<string, unknown> = {\n payerAddress: params.payerAddress,\n opType: params.opType,\n opId: params.opId,\n asset: params.asset,\n amount: params.amount,\n paymentNonce: params.paymentNonce,\n };\n if (params.accessRecord) {\n body[\"accessRecord\"] = params.accessRecord;\n }\n const res = await fetch(`${base}/v1/escrow/pay`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Web3Signed ${params.signature}`,\n },\n body: JSON.stringify(body),\n });\n if (!res.ok) {\n throw new Error(`Gateway error: ${res.status} ${res.statusText}`);\n }\n return (await res.json()) as PayForOperationResult;\n },\n\n async settle(params?: SettleParams): Promise<SettleResult> {\n const res = await fetch(`${base}/v1/settle`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n // The gateway accepts an empty body; only `limit` is recognised.\n // Always send a JSON body so the gateway's req.body shape parse\n // doesn't have to deal with an undefined.\n body: JSON.stringify(params ?? {}),\n });\n if (!res.ok) {\n throw new Error(`Gateway error: ${res.status} ${res.statusText}`);\n }\n return (await res.json()) as SettleResult;\n },\n };\n}\n"],"mappings":"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","names":["body"]}
|
|
1
|
+
{"version":3,"sources":["../../src/protocol/gateway.ts"],"sourcesContent":["import { tryGrantPermissions, type GrantPermission } from \"./scope-actions\";\nimport {\n DataPointDeletedError,\n DataPointNotFoundError,\n DataPointVersionConflictError,\n} from \"../errors\";\nimport {\n isDataPointTombstone,\n tombstoneDeletedAt,\n} from \"./data-point-deletion\";\nimport { deriveDataPointId } from \"./lineage\";\nimport {\n isPlainObject,\n readJsonObject,\n readJsonValue,\n} from \"../utils/response-body\";\nimport type { EscrowBalanceResult } from \"./escrow\";\n\nexport type {\n EscrowBalanceEntry,\n EscrowBalanceResult,\n FailedDepositEntry as EscrowDepositFailed,\n FinalizedDepositEntry as EscrowDepositFinalized,\n SubmittedDepositEntry as EscrowDepositSubmitted,\n} from \"./escrow\";\n\nexport interface GatewayEnvelope<T> {\n data: T;\n proof: GatewayProof;\n /**\n * Cursor-based pagination metadata, present on list endpoints (e.g.\n * `GET /v1/data`). A sibling of `data`, not nested inside it — so callers\n * that need it must read the full envelope rather than going through\n * `unwrapEnvelope`, which intentionally returns only `data`.\n */\n pagination?: GatewayPagination;\n}\n\nexport interface GatewayPagination {\n limit: number;\n hasMore: boolean;\n /**\n * Opaque cursor for the NEXT page; pass back as the `cursor` query param.\n * Null when there are no further pages.\n */\n nextCursor: string | null;\n}\n\nexport interface GatewayProof {\n signature: string;\n timestamp: string;\n gatewayAddress: string;\n requestHash: string;\n responseHash: string;\n userSignature: string;\n status: string;\n chainBlockHeight: number;\n}\n\nexport interface Builder {\n id: string;\n ownerAddress: string;\n granteeAddress: string;\n publicKey: string;\n appUrl: string;\n addedAt: string;\n}\n\nexport interface Schema {\n id: string;\n ownerAddress: string;\n name: string;\n definitionUrl: string;\n scope: string;\n addedAt: string;\n}\n\n// Row shape returned by `GET /v1/servers?owner=…`. A discovery-only list —\n// the endpoint carries no per-item gateway attestation (use\n// `getServer(serverAddress)` if you need a verifiable single-server proof).\n// `status` is the gateway's chain-sync state ('pending' | 'confirmed' |\n// 'finalized' | 'reorged'); kept as an open string since the gateway may add\n// states without a SDK bump.\nexport interface OwnerServerRecord {\n id: string;\n ownerAddress: string;\n serverAddress: string;\n publicKey: string;\n serverUrl: string;\n status: string;\n chainBlockHeight: string | null;\n // ISO timestamp of the gateway-side upsert.\n addedAt: string;\n // ISO timestamp when the grantor deregistered this server. Always null for\n // entries in the `active` list, always non-null for `revoked`.\n revokedAt: string | null;\n}\n\n// Response from `GET /v1/servers?owner=…`. Split into two arrays; each is\n// ordered newest-first (`addedAt DESC`, `id ASC` tiebreaker) so `active[0]`\n// is unambiguously the newest active server — the sensible default when the\n// caller just needs one URL. The gateway caps the total across both arrays\n// at 200 — per-owner counts are tiny in practice, so no cursor.\nexport interface OwnerServersResult {\n active: OwnerServerRecord[];\n revoked: OwnerServerRecord[];\n count: number;\n}\n\nexport interface ServerInfo {\n id: string;\n ownerAddress: string;\n serverAddress: string;\n publicKey: string;\n serverUrl: string;\n addedAt: string;\n // ISO timestamp when the grantor deregistered this server, null while\n // active. The gateway returns this on /v1/servers/:address GETs since\n // covering revocation in the response keeps the attestation hash\n // authoritative for both states.\n revokedAt: string | null;\n}\n\n// Fee annotation surfaced on every GET grant response. Amounts are decimal\n// uint256 strings to match `/v1/escrow/pay`'s wire format. totalDue is a\n// snapshot — the gateway re-resolves fees at pay time, so clients shouldn't\n// cache this across requests.\nexport interface GatewayGrantFee {\n asset: string;\n registrationFee: string;\n dataAccessFee: string;\n totalDue: string;\n}\n\n// Lifecycle of a grant's on-chain settlement, tracked separately from the\n// fee-payment lifecycle (paymentStatus). Driven by POST /v1/settle:\n// pending — nothing on-chain yet\n// submitting — settle tx broadcast but receipt not yet observed\n// confirmed — settle tx mined successfully\n// finalized — finalized tip past the settle block, reorg-safe\n// reorged — finalized observation reverted; back to 'pending' on next settle\nexport type GatewayGrantStatus =\n | \"pending\"\n | \"submitting\"\n | \"confirmed\"\n | \"finalized\"\n | \"reorged\";\n\nexport interface GatewayGrantResponse {\n id: string;\n grantorAddress: string;\n granteeId: string;\n // The signed scope entries, verbatim (`[operation:]scope`, bare = read).\n // This is the wire form: it is what the grantor signed and what the\n // gateway stores. Render `permissions` instead of parsing these.\n scopes: string[];\n // Derived by the SDK at read time from `scopes` via grantPermissions():\n // one `{ scope, actions }` row per scope pattern, canonically ordered.\n // Never sent to the gateway and never part of the signed payload. Absent\n // when `scopes` carries an entry this SDK version cannot interpret (an\n // operation introduced after this release) - fall back to `scopes` and\n // do not assume such a grant is read-only.\n permissions?: GrantPermission[];\n status: GatewayGrantStatus;\n addedAt: string;\n // Grantor-signed deadline. null = perpetual grant (signed value was 0).\n expiresAt: string | null;\n // Derived at read time from expiresAt vs the gateway's clock — a snapshot,\n // not a cached truth. Re-check against expiresAt locally if you care.\n expired: boolean;\n revokedAt: string | null;\n revocationSignature: string | null;\n // 'pending' until the grant registration fee is settled via /v1/escrow/pay.\n paymentStatus: \"pending\" | \"paid\";\n paidAt: string | null;\n paidBy: string | null;\n // Decimal-string uint256 monotonic nonce; advances on every state change.\n grantVersion: string;\n // Settle metadata — populated as the grant progresses through the chain\n // lifecycle. Null while `status === 'pending'`.\n settleTxHash: string | null;\n settleSubmittedAt: string | null;\n // Revocation metadata — populated independently when the grantor signs\n // and the gateway pushes a deregister tx.\n revocationTxHash: string | null;\n revocationSubmittedAt: string | null;\n fee: GatewayGrantFee;\n}\n\nexport type GrantListItem = GatewayGrantResponse;\n\n// Mirror of a DataRegistryV2 row as the gateway exposes it. `id` is the\n// deterministic `keccak256(abi.encode(owner, scope))` dataPointId — the same\n// value the contract uses as its primary key. `expectedVersion` is the latest\n// version the gateway has accepted; the on-chain row may be one version behind\n// while a settle is still pending. The gateway response intentionally drops\n// `status` from the body — read it from the on-chain contract when you need\n// the canonical lifecycle state.\nexport interface DataPointRecord {\n id: string;\n ownerAddress: string;\n scope: string;\n dataHash: string;\n metadataHash: string;\n // Decimal-string uint256.\n expectedVersion: string;\n // ISO 8601 timestamp of the most recent gateway-side upsert.\n addedAt: string;\n // ISO 8601 timestamp of the deletion tombstone, null/absent while live.\n // Only present on reads that opted in via `includeDeleted`; a plain\n // `getDataPoint` of a deleted row throws `DataPointDeletedError` instead.\n deletedAt?: string | null;\n}\n\nexport interface GetDataPointOptions {\n /**\n * Return the row even after it was deleted (with `deletedAt` set and the\n * tombstone hash pair) instead of throwing `DataPointDeletedError`.\n */\n includeDeleted?: boolean;\n}\n\nexport interface DataPointListResult {\n dataPoints: DataPointRecord[];\n cursor: string | null;\n}\n\nexport interface ListDataPointsOptions {\n /**\n * Only return rows added at or after this ISO 8601 timestamp. Used by sync\n * loops that want incremental tails — pass the last seen `addedAt`.\n */\n since?: string;\n /** Page size. Capped at 1000 by the gateway. */\n limit?: number;\n /**\n * Include deleted (tombstoned) rows, each carrying `deletedAt`. Off by\n * default; without it the SDK also drops any tombstone the gateway leaks.\n */\n includeDeleted?: boolean;\n}\n\n// grantVersion and expiresAt are decimal-string uint256s — same wire format\n// the gateway expects. The caller is responsible for converting their bigint\n// to a decimal string and for signing GRANT_REGISTRATION_TYPES with matching\n// bigint values.\nexport interface CreateGrantParams {\n grantorAddress: string;\n granteeId: string;\n scopes: string[];\n grantVersion: string;\n expiresAt: string;\n signature: string;\n}\n\nexport interface RevokeGrantParams {\n grantId: string;\n grantorAddress: string;\n grantVersion: string;\n signature: string;\n}\n\nexport interface RegisterServerParams {\n ownerAddress: string;\n serverAddress: string;\n publicKey: string;\n serverUrl: string;\n signature: string;\n}\n\nexport interface RegisterServerResult {\n serverId?: string;\n alreadyRegistered: boolean;\n}\n\nexport interface RegisterBuilderParams {\n ownerAddress: string;\n // Wallet the builder authenticates to the Personal Server with. The\n // builderId is deterministically derived from (owner, grantee, publicKey,\n // appUrl) so this triple pins the on-chain identity.\n granteeAddress: string;\n publicKey: string;\n appUrl: string;\n signature: string;\n}\n\nexport interface RegisterBuilderResult {\n builderId?: string;\n alreadyRegistered: boolean;\n}\n\n// AddData on DataRegistryV2. dataHash + metadataHash are bytes32 commitments\n// to the off-chain payload + its metadata. expectedVersion is a CAS knob —\n// the gateway/contract rejects on 409 if a higher version is already stored,\n// and the error body surfaces `currentExpectedVersion` so callers can re-sign.\nexport interface RegisterDataPointParams {\n ownerAddress: string;\n scope: string;\n dataHash: string;\n metadataHash: string;\n expectedVersion: string;\n signature: string;\n}\n\nexport interface RegisterDataPointResult {\n dataPointId?: string;\n expectedVersion?: string;\n}\n\n// Deletion = an owner-signed AddData for version current+1 carrying the\n// tombstone hash pair (see protocol/data-point-deletion.ts). The gateway\n// reconstructs the AddData from the body plus the tombstone constants and\n// recovers the signer, so the hashes are NOT on the wire. Used at\n// DELETE /v1/data/:dataPointId. expectedVersion is the tombstone's version\n// as a decimal uint256 string; the signature is the raw EIP-712 hex.\nexport interface DeleteDataPointParams {\n ownerAddress: string;\n scope: string;\n expectedVersion: string;\n signature: string;\n}\n\n// The tombstone version record the gateway returns on 200. Fields are\n// optional because, like RegisterDataPointResult, the SDK tolerates either\n// a bare body or an enveloped `{data}`; `deletedAt` is the durable marker.\nexport interface DeleteDataPointResult {\n dataPointId?: string;\n ownerAddress?: string;\n scope?: string;\n dataHash?: string;\n metadataHash?: string;\n expectedVersion?: string;\n deletedAt?: string | null;\n}\n\n// ── Escrow / data-access payment path ───────────────────────────────────────\n// /v1/escrow/pay debits the payer's escrow balance for a payable op. For a\n// Legacy grants use opType='grant' and opId=grantId. Standalone receipt-bound\n// reads use opType='data_access' and opId=accessRecord.recordId. amount and\n// paymentNonce are decimal uint256 strings on the wire. The signature is the\n// raw EIP-712 hex of GENERIC_PAYMENT_TYPES against escrowPaymentDomain.\n\n// A server-signed delivery receipt attached to a data-access payment. The\n// signature is over RECORD_DATA_ACCESS_TYPES against dataRegistryDomain; the\n// signer must be a personal server the data point's owner has registered as\n// trusted. Shape validation in clients does not verify this signature; the\n// gateway verifies it before re-using it on-chain in the next /v1/settle pass\n// via DataRegistryV2.recordDataAccess, where `recordId` dedupes via\n// `_usedRecordIds`.\nexport interface AccessRecord {\n dataPointId: string;\n // Decimal-string uint256 — the data point version being attested to.\n version: string;\n // Must equal the enclosing payment's payerAddress (the gateway enforces).\n accessor: string;\n recordId: string;\n signature: string;\n}\n\nexport interface PayForOperationParams {\n payerAddress: string;\n opType: string;\n opId: string;\n asset: string;\n amount: string;\n // Per-payer monotonic; (payer, nonce, kind) must be unique. The gateway\n // returns 409 if reused — bump and re-sign.\n paymentNonce: string;\n signature: string;\n // Optional: attach a server-signed access record so the next /v1/settle\n // pass submits a recordDataAccess tx alongside the payment settlement.\n // Required for data-access payments (the second-and-onward payments per\n // grant) that want their on-chain `totalAccesses` counter to advance.\n accessRecord?: AccessRecord;\n}\n\nexport interface PayForOperationResult {\n opType: string;\n opId: string;\n payerAddress: string;\n asset: string;\n amount: string;\n // Echoes how the gateway split this payment. `registrationPaid` is true on\n // the first payment for a grant (which bundles both fees) and false on\n // subsequent data-access-only payments. Off-chain ledger state only — the\n // on-chain settlement of the registration is tracked by the grant's\n // `status` field, not this flag.\n breakdown: {\n registrationFee: string;\n dataAccessFee: string;\n registrationPaid: boolean;\n };\n paymentNonce: string;\n paidAt: string;\n}\n\n// ── Settle / reconcile ──────────────────────────────────────────────────────\n// POST /v1/settle drains pending-on-chain rows (grants, servers, builders,\n// data points, data-point statuses, access records) to the relayer, then\n// promotes 'submitting' → 'confirmed' and 'confirmed' → 'finalized' for\n// previously-submitted rows. One call does all three; the response surfaces\n// each phase's outcomes.\n\n// The op-types the settle endpoint emits in `items[]`. Kept as a union so\n// callers can narrow inside the discriminated SettleItem shape. Mirrors the\n// gateway's drain phases (drainGrants/drainServers/drainBuilders/\n// drainDataPoints/drainDataPointStatuses/drainAccessRecords) — keep in sync\n// when the gateway adds a drain.\nexport type SettleOpType =\n | \"grant\"\n | \"server\"\n | \"data\"\n | \"access\"\n | \"builder\"\n | \"data-status\";\n\nexport type SettleItem =\n | {\n opType: SettleOpType;\n opId: string;\n // 'confirmed' when the submit function waited for the receipt and it\n // mined (registerAndSettle path); 'submitting' when only the tx was\n // sent (no receipt wait).\n status: \"submitting\" | \"confirmed\";\n settleTxHash: string | null;\n settleSubmittedAt: string | null;\n // Block height the tx mined in; only set when status === 'confirmed'.\n chainBlockHeight: string | null;\n revocationTxHash: string | null;\n revocationSubmittedAt: string | null;\n // True while lib/settle.ts is in placeholder mode for this row's pass.\n placeholder: boolean;\n }\n | {\n opType: SettleOpType;\n opId: string;\n status: \"skipped\";\n reason: string;\n }\n | {\n opType: SettleOpType;\n opId: string;\n status: \"failed\";\n error: string;\n };\n\n// Outcome of the housekeeping pass that retries earlier `submitting` rows\n// whose receipt arrived after the prior /v1/settle's wait budget elapsed.\nexport interface SettlePromoteResult {\n opType: SettleOpType;\n opId: string;\n status: \"confirmed\" | \"failed\" | \"pending\" | \"skipped\";\n txHash: string;\n chainBlockHeight: string | null;\n reason?: string;\n}\n\n// Outcome of the reconcile pass that advances 'confirmed' → 'finalized' once\n// the chain's finalized tip catches up past the tx's block (or reverts to\n// 'pending' on reorg detection).\nexport interface SettleReconcileItem {\n opId: string;\n status: \"finalized\" | \"reorged\" | \"unchanged\";\n chainBlockHeight: string | null;\n settleTxHash: string | null;\n reason?: string;\n}\n\nexport interface SettleParams {\n // Per-phase cap. Bounded by MAX_LIMIT on the gateway side; omit to use\n // the gateway's default BATCH_LIMIT.\n limit?: number;\n}\n\nexport interface SettleResult {\n scanned: number;\n submitted: number;\n confirmed: number;\n skipped: number;\n failed: number;\n items: SettleItem[];\n promoted: { count: number; items: SettlePromoteResult[] };\n reconciled: {\n scanned: number;\n finalized: number;\n reorged: number;\n unchanged: number;\n items: SettleReconcileItem[];\n };\n // Present only when the gateway is configured for paced submission —\n // spreads work across several blocks within one /v1/settle invocation.\n paced?: { iterations: number };\n}\n\n/**\n * Legacy `GatewayClient` name for the canonical `/v1/escrow/balance` response.\n * `availableAmount` is `max(balance − authorizedAmount − withdrawingAmount, 0)`.\n */\nexport type EscrowBalance = EscrowBalanceResult;\n\n// /v1/escrow/deposit announces an on-chain deposit tx so the gateway can\n// reconcile it into the payer's balance. The gateway extracts the credited\n// account from calldata — no off-chain claim about who paid.\nexport interface SubmitDepositParams {\n txHash: string;\n}\n\nexport interface DepositState {\n txHash: string;\n account: string;\n // 'submitted' | 'finalized' | 'failed' — kept open since the gateway adds\n // states (e.g. 'orphaned') as the deposit flow evolves.\n status: string;\n blockNumber: string | null;\n submittedAt: string;\n finalizedAt: string | null;\n lastError: string | null;\n}\n\nexport interface GatewayClient {\n isRegisteredBuilder(address: string): Promise<boolean>;\n getBuilder(address: string): Promise<Builder | null>;\n getGrant(grantId: string): Promise<GatewayGrantResponse | null>;\n listGrantsByUser(userAddress: string): Promise<GrantListItem[]>;\n getSchemaForScope(scope: string): Promise<Schema | null>;\n getServer(address: string): Promise<ServerInfo | null>;\n /**\n * List every personal server an owner has registered — split into `active`\n * (currently trusted) and `revoked` (deregistered). Each list is ordered\n * newest-first; `active[0]` is the sensible default when the caller just\n * needs one URL for the owner. Empty owner returns `{active: [], revoked: [], count: 0}`.\n * Discovery-only endpoint — no per-server attestation; use `getServer` for that.\n */\n listServersByOwner(owner: string): Promise<OwnerServersResult>;\n /**\n * Fetch a single data point by its deterministic id (keccak256 of (owner, scope)).\n * Returns null on 404. Throws `DataPointDeletedError` on 410, or when the row is a\n * tombstone, unless `options.includeDeleted` is set. The gateway omits `status` from\n * the response body -- read it from the on-chain DataRegistryV2 contract when you need\n * the canonical lifecycle state.\n */\n getDataPoint(\n dataPointId: string,\n options?: GetDataPointOptions,\n ): Promise<DataPointRecord | null>;\n /**\n * Page through an owner's data points. Cursor is opaque; pass `null` for the first\n * page and feed back `result.cursor` until it returns null.\n * Deleted rows are excluded unless `options.includeDeleted` is set.\n */\n listDataPointsByOwner(\n owner: string,\n cursor: string | null,\n options?: ListDataPointsOptions,\n ): Promise<DataPointListResult>;\n getSchema(schemaId: string): Promise<Schema | null>;\n registerServer(params: RegisterServerParams): Promise<RegisterServerResult>;\n registerBuilder(\n params: RegisterBuilderParams,\n ): Promise<RegisterBuilderResult>;\n registerDataPoint(\n params: RegisterDataPointParams,\n ): Promise<RegisterDataPointResult>;\n /**\n * Tombstone a data point: `DELETE /v1/data/:dataPointId` with the owner's\n * AddData signature for version `current + 1`. The dataPointId is derived\n * from (ownerAddress, scope). Throws `DataPointVersionConflictError` on\n * 409, `DataPointDeletedError` on 410 (already deleted),\n * `DataPointNotFoundError` on 404.\n */\n deleteDataPoint(\n params: DeleteDataPointParams,\n ): Promise<DeleteDataPointResult>;\n createGrant(params: CreateGrantParams): Promise<{ grantId?: string }>;\n revokeGrant(params: RevokeGrantParams): Promise<void>;\n getEscrowBalance(account: string): Promise<EscrowBalanceResult>;\n submitEscrowDeposit(params: SubmitDepositParams): Promise<DepositState>;\n payForOperation(\n params: PayForOperationParams,\n ): Promise<PayForOperationResult>;\n settle(params?: SettleParams): Promise<SettleResult>;\n}\n\n// Attach the derived `permissions` view to a grant record read back from the\n// gateway. The wire `scopes` are left untouched.\nfunction withGrantPermissions<T extends GatewayGrantResponse>(grant: T): T {\n // Gateway responses are untrusted runtime data despite their type. The\n // `permissions` view is SDK-owned: drop whatever the body carried under\n // that name, then derive only from a well-formed string[] and leave the\n // field absent otherwise, so a spoofed or stale value can never be\n // rendered as this grant's authority.\n const stripped = { ...grant };\n delete stripped.permissions;\n const scopes: unknown = stripped.scopes;\n if (!Array.isArray(scopes)) {\n return stripped;\n }\n const permissions = tryGrantPermissions(scopes as string[]);\n return permissions === undefined ? stripped : { ...stripped, permissions };\n}\n\nexport function createGatewayClient(baseUrl: string): GatewayClient {\n const base = baseUrl.replace(/\\/+$/, \"\");\n\n function malformedBody(res: Response): Error {\n return new Error(\n `Gateway error: ${res.status} malformed response body (expected a JSON envelope)`,\n );\n }\n\n // GET responses are `{ data, proof, pagination? }`. A 200 whose body is\n // empty, not JSON, or not an object with `data` is a gateway bug, not a\n // record: fail with a clear error instead of returning undefined.\n async function readEnvelope<T>(\n res: Response,\n ): Promise<Record<string, unknown> & { data: T }> {\n const envelope = await readJsonValue(res);\n if (!isPlainObject(envelope) || !(\"data\" in envelope)) {\n throw malformedBody(res);\n }\n return envelope as Record<string, unknown> & { data: T };\n }\n\n async function unwrapEnvelope<T>(res: Response): Promise<T> {\n return (await readEnvelope<T>(res)).data;\n }\n\n function getMutationId(body: unknown, key: string): string | undefined {\n if (!isPlainObject(body)) return undefined;\n const value = body[key] ?? body[\"id\"];\n return typeof value === \"string\" ? value : undefined;\n }\n\n // Mutation responses are bare JSON; GET responses are enveloped. Accept\n // either so a gateway that wraps the tombstone record still round-trips.\n // Any body that is not a plain object (empty 204, `null`, an array, an\n // HTML error page) reads as `{}` so the status-code mapping still wins.\n async function readBody(res: Response): Promise<Record<string, unknown>> {\n const raw = await readJsonObject(res);\n const data = raw[\"data\"];\n if (isPlainObject(data) && \"proof\" in raw) {\n return data;\n }\n return raw;\n }\n\n function stringOrUndefined(value: unknown): string | undefined {\n return typeof value === \"string\" ? value : undefined;\n }\n\n async function deletedError(\n res: Response,\n details: { dataPointId?: string; scope?: string; ownerAddress?: string },\n ): Promise<DataPointDeletedError> {\n const body = await readBody(res);\n return new DataPointDeletedError(\n `Data point ${details.dataPointId ?? details.scope ?? \"\"} has been deleted`,\n { ...details, deletedAt: tombstoneDeletedAt(body) },\n );\n }\n\n return {\n async isRegisteredBuilder(address: string): Promise<boolean> {\n const builder = await this.getBuilder(address);\n return builder !== null;\n },\n\n async getBuilder(address: string): Promise<Builder | null> {\n const res = await fetch(`${base}/v1/builders/${address}`);\n if (res.status === 404) return null;\n if (!res.ok) {\n throw new Error(`Gateway error: ${res.status} ${res.statusText}`);\n }\n return unwrapEnvelope<Builder>(res);\n },\n\n async getGrant(grantId: string): Promise<GatewayGrantResponse | null> {\n const res = await fetch(`${base}/v1/grants/${grantId}`);\n if (res.status === 404) return null;\n if (!res.ok) {\n throw new Error(`Gateway error: ${res.status} ${res.statusText}`);\n }\n return withGrantPermissions(\n await unwrapEnvelope<GatewayGrantResponse>(res),\n );\n },\n\n async listGrantsByUser(userAddress: string): Promise<GrantListItem[]> {\n const res = await fetch(`${base}/v1/grants?user=${userAddress}`);\n if (res.status === 404) return [];\n if (!res.ok) {\n throw new Error(`Gateway error: ${res.status} ${res.statusText}`);\n }\n const grants = await unwrapEnvelope<GrantListItem[]>(res);\n return grants.map(withGrantPermissions);\n },\n\n async getSchemaForScope(scope: string): Promise<Schema | null> {\n const res = await fetch(`${base}/v1/schemas?scope=${scope}`);\n if (res.status === 404) return null;\n if (!res.ok) {\n throw new Error(`Gateway error: ${res.status} ${res.statusText}`);\n }\n return unwrapEnvelope<Schema>(res);\n },\n\n async getServer(address: string): Promise<ServerInfo | null> {\n const res = await fetch(`${base}/v1/servers/${address}`);\n if (res.status === 404) return null;\n if (!res.ok) {\n throw new Error(`Gateway error: ${res.status} ${res.statusText}`);\n }\n return unwrapEnvelope<ServerInfo>(res);\n },\n\n async listServersByOwner(owner: string): Promise<OwnerServersResult> {\n // URLSearchParams encodes the owner so a malformed value (e.g.\n // \"0xabc&foo=bar\") can't inject extra query params. Same pattern as\n // listDataPointsByOwner.\n const params = new URLSearchParams({ owner });\n const res = await fetch(`${base}/v1/servers?${params.toString()}`);\n if (!res.ok) {\n throw new Error(`Gateway error: ${res.status} ${res.statusText}`);\n }\n // Unlike /v1/servers/:address, the list endpoint returns the body\n // directly — no GatewayEnvelope, no attestation (discovery-only).\n return (await res.json()) as OwnerServersResult;\n },\n\n async getDataPoint(\n dataPointId: string,\n options?: GetDataPointOptions,\n ): Promise<DataPointRecord | null> {\n const query = options?.includeDeleted ? \"?includeDeleted=true\" : \"\";\n const res = await fetch(`${base}/v1/data/${dataPointId}${query}`);\n if (res.status === 404) return null;\n if (res.status === 410) {\n throw await deletedError(res, { dataPointId });\n }\n if (!res.ok) {\n throw new Error(`Gateway error: ${res.status} ${res.statusText}`);\n }\n const record = await unwrapEnvelope<DataPointRecord>(res);\n // Never hand a tombstone back as data unless the caller opted in --\n // guards against a gateway that returns 200 + deletedAt on plain reads.\n if (!options?.includeDeleted && isDataPointTombstone(record)) {\n throw new DataPointDeletedError(\n `Data point ${dataPointId} has been deleted`,\n {\n dataPointId,\n scope: record.scope,\n ownerAddress: record.ownerAddress,\n deletedAt: tombstoneDeletedAt(record),\n },\n );\n }\n return record;\n },\n\n async listDataPointsByOwner(\n owner: string,\n cursor: string | null,\n options?: ListDataPointsOptions,\n ): Promise<DataPointListResult> {\n const params = new URLSearchParams({ user: owner });\n if (cursor !== null) {\n params.set(\"cursor\", cursor);\n }\n if (options?.since) {\n params.set(\"since\", options.since);\n }\n if (options?.limit !== undefined) {\n params.set(\"limit\", String(options.limit));\n }\n if (options?.includeDeleted) {\n params.set(\"includeDeleted\", \"true\");\n }\n const res = await fetch(`${base}/v1/data?${params.toString()}`);\n if (!res.ok) {\n throw new Error(`Gateway error: ${res.status} ${res.statusText}`);\n }\n // Next-page cursor lives in the envelope's `pagination.nextCursor`\n // (a sibling of `data`), so read the full envelope rather than going\n // through `unwrapEnvelope`, which returns only `data`.\n const envelope = await readEnvelope<unknown>(res);\n if (\n !isPlainObject(envelope.data) ||\n !Array.isArray(envelope.data[\"dataPoints\"])\n ) {\n throw malformedBody(res);\n }\n const rows = envelope.data[\"dataPoints\"] as DataPointRecord[];\n const pagination = isPlainObject(envelope[\"pagination\"])\n ? envelope[\"pagination\"]\n : undefined;\n const rawCursor = pagination?.[\"nextCursor\"];\n const nextCursor =\n pagination?.[\"hasMore\"] === false || typeof rawCursor !== \"string\"\n ? null\n : rawCursor;\n // Tombstones only surface when asked for; drop any the gateway leaks\n // on a plain list so sync loops never ingest one as data.\n const dataPoints = options?.includeDeleted\n ? rows\n : rows.filter((row) => !isDataPointTombstone(row));\n return {\n dataPoints,\n cursor: nextCursor,\n };\n },\n\n async getSchema(schemaId: string): Promise<Schema | null> {\n const res = await fetch(`${base}/v1/schemas/${schemaId}`);\n if (res.status === 404) return null;\n if (!res.ok) {\n throw new Error(`Gateway error: ${res.status} ${res.statusText}`);\n }\n return unwrapEnvelope<Schema>(res);\n },\n\n async registerServer(\n params: RegisterServerParams,\n ): Promise<RegisterServerResult> {\n const res = await fetch(`${base}/v1/servers`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Web3Signed ${params.signature}`,\n },\n body: JSON.stringify({\n ownerAddress: params.ownerAddress,\n serverAddress: params.serverAddress,\n publicKey: params.publicKey,\n serverUrl: params.serverUrl,\n }),\n });\n if (res.status === 409) {\n const body = await readJsonObject(res);\n return {\n serverId: getMutationId(body, \"serverId\"),\n alreadyRegistered: true,\n };\n }\n if (!res.ok) {\n throw new Error(`Gateway error: ${res.status} ${res.statusText}`);\n }\n const body = await readJsonObject(res);\n return {\n serverId: getMutationId(body, \"serverId\"),\n alreadyRegistered: false,\n };\n },\n\n async registerBuilder(\n params: RegisterBuilderParams,\n ): Promise<RegisterBuilderResult> {\n const res = await fetch(`${base}/v1/builders`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Web3Signed ${params.signature}`,\n },\n body: JSON.stringify({\n ownerAddress: params.ownerAddress,\n granteeAddress: params.granteeAddress,\n publicKey: params.publicKey,\n appUrl: params.appUrl,\n }),\n });\n // 409 is idempotent — the gateway's current 409 body doesn't include\n // the builderId, but we tolerate it in case that changes (mirrors the\n // registerServer / createGrant shape).\n if (res.status === 409) {\n const body = await readJsonObject(res);\n return {\n builderId: getMutationId(body, \"builderId\"),\n alreadyRegistered: true,\n };\n }\n if (!res.ok) {\n throw new Error(`Gateway error: ${res.status} ${res.statusText}`);\n }\n const body = await readJsonObject(res);\n return {\n builderId: getMutationId(body, \"builderId\"),\n alreadyRegistered: false,\n };\n },\n\n async registerDataPoint(\n params: RegisterDataPointParams,\n ): Promise<RegisterDataPointResult> {\n const res = await fetch(`${base}/v1/data`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Web3Signed ${params.signature}`,\n },\n body: JSON.stringify({\n ownerAddress: params.ownerAddress,\n scope: params.scope,\n dataHash: params.dataHash,\n metadataHash: params.metadataHash,\n expectedVersion: params.expectedVersion,\n }),\n });\n // 409 is a real failure here (stale CAS), not an idempotent replay —\n // surface the gateway's error message verbatim so the caller knows\n // what `currentExpectedVersion` to re-sign against.\n if (!res.ok) {\n const body = await readJsonObject(res);\n const detail = stringOrUndefined(body[\"error\"]) ?? res.statusText;\n throw new Error(`Gateway error: ${res.status} ${detail}`);\n }\n const body = await readJsonObject(res);\n return {\n dataPointId: getMutationId(body, \"dataPointId\"),\n expectedVersion: stringOrUndefined(body[\"expectedVersion\"]),\n };\n },\n\n async deleteDataPoint(\n params: DeleteDataPointParams,\n ): Promise<DeleteDataPointResult> {\n // The gateway keys the row on keccak256(abi.encode(owner, scope)) --\n // derive it locally rather than asking the caller for it, so the path\n // can never disagree with the signed (owner, scope).\n const dataPointId = deriveDataPointId(\n params.ownerAddress as `0x${string}`,\n params.scope,\n );\n const details = {\n dataPointId,\n scope: params.scope,\n ownerAddress: params.ownerAddress,\n };\n // Signature rides both in the body (the DELETE contract) and as the\n // Web3Signed Authorization header every other /v1 mutation uses.\n const res = await fetch(`${base}/v1/data/${dataPointId}`, {\n method: \"DELETE\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Web3Signed ${params.signature}`,\n },\n body: JSON.stringify({\n ownerAddress: params.ownerAddress,\n scope: params.scope,\n expectedVersion: params.expectedVersion,\n signature: params.signature,\n }),\n });\n if (res.status === 404) {\n throw new DataPointNotFoundError(\n `Data point ${dataPointId} (scope '${params.scope}') is not registered`,\n details,\n );\n }\n if (res.status === 409) {\n const body = await readBody(res);\n const currentExpectedVersion = stringOrUndefined(\n body[\"currentExpectedVersion\"],\n );\n const detail = stringOrUndefined(body[\"error\"]) ?? res.statusText;\n throw new DataPointVersionConflictError(\n `Gateway error: 409 ${detail}`,\n {\n ...details,\n expectedVersion: params.expectedVersion,\n currentExpectedVersion,\n },\n );\n }\n if (res.status === 410) {\n throw await deletedError(res, details);\n }\n if (!res.ok) {\n // 400 = the recovered signer/hashes do not match the tombstone\n // AddData. Surface the gateway's message verbatim, like\n // registerDataPoint does.\n const body = await readBody(res);\n const detail = stringOrUndefined(body[\"error\"]) ?? res.statusText;\n throw new Error(`Gateway error: ${res.status} ${detail}`);\n }\n const body = await readBody(res);\n return {\n dataPointId: getMutationId(body, \"dataPointId\") ?? dataPointId,\n ownerAddress: stringOrUndefined(body[\"ownerAddress\"]),\n scope: stringOrUndefined(body[\"scope\"]),\n dataHash: stringOrUndefined(body[\"dataHash\"]),\n metadataHash: stringOrUndefined(body[\"metadataHash\"]),\n expectedVersion: stringOrUndefined(body[\"expectedVersion\"]),\n deletedAt: tombstoneDeletedAt(body),\n };\n },\n\n async createGrant(\n params: CreateGrantParams,\n ): Promise<{ grantId?: string }> {\n const res = await fetch(`${base}/v1/grants`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Web3Signed ${params.signature}`,\n },\n body: JSON.stringify({\n grantorAddress: params.grantorAddress,\n granteeId: params.granteeId,\n scopes: params.scopes,\n grantVersion: params.grantVersion,\n expiresAt: params.expiresAt,\n }),\n });\n if (res.status === 409) {\n const body = await readJsonObject(res);\n return { grantId: getMutationId(body, \"grantId\") };\n }\n if (!res.ok) {\n throw new Error(`Gateway error: ${res.status} ${res.statusText}`);\n }\n const body = await readJsonObject(res);\n return { grantId: getMutationId(body, \"grantId\") };\n },\n\n async revokeGrant(params: RevokeGrantParams): Promise<void> {\n const res = await fetch(`${base}/v1/grants/${params.grantId}`, {\n method: \"DELETE\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Web3Signed ${params.signature}`,\n },\n body: JSON.stringify({\n grantorAddress: params.grantorAddress,\n grantVersion: params.grantVersion,\n }),\n });\n if (res.status === 409) return;\n if (!res.ok) {\n throw new Error(`Gateway error: ${res.status} ${res.statusText}`);\n }\n },\n\n async getEscrowBalance(account: string): Promise<EscrowBalanceResult> {\n const res = await fetch(`${base}/v1/escrow/balance?account=${account}`);\n if (!res.ok) {\n throw new Error(`Gateway error: ${res.status} ${res.statusText}`);\n }\n // Unlike the rest of /v1, the balance endpoint returns the body\n // directly (no GatewayEnvelope wrap) — it's a pure read with no\n // gateway-signed attestation. See data-gateway api/v1/escrow/balance.ts.\n return (await res.json()) as EscrowBalanceResult;\n },\n\n async submitEscrowDeposit(\n params: SubmitDepositParams,\n ): Promise<DepositState> {\n const res = await fetch(`${base}/v1/escrow/deposit`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ txHash: params.txHash }),\n });\n // The gateway returns 202 for \"accepted (still confirming)\" and 200 for\n // duplicate idempotent replays. Both carry the deposit's current state.\n if (res.status !== 200 && res.status !== 202) {\n throw new Error(`Gateway error: ${res.status} ${res.statusText}`);\n }\n return (await res.json()) as DepositState;\n },\n\n async payForOperation(\n params: PayForOperationParams,\n ): Promise<PayForOperationResult> {\n // Build the body without the accessRecord key when absent so the\n // gateway's \"missing optional\" branch matches the no-receipt case\n // exactly (an explicit `accessRecord: undefined` would JSON-serialize\n // to nothing — same result — but keeping it conditional makes wire\n // traces easier to read).\n const body: Record<string, unknown> = {\n payerAddress: params.payerAddress,\n opType: params.opType,\n opId: params.opId,\n asset: params.asset,\n amount: params.amount,\n paymentNonce: params.paymentNonce,\n };\n if (params.accessRecord) {\n body[\"accessRecord\"] = params.accessRecord;\n }\n const res = await fetch(`${base}/v1/escrow/pay`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Web3Signed ${params.signature}`,\n },\n body: JSON.stringify(body),\n });\n if (!res.ok) {\n throw new Error(`Gateway error: ${res.status} ${res.statusText}`);\n }\n return (await res.json()) as PayForOperationResult;\n },\n\n async settle(params?: SettleParams): Promise<SettleResult> {\n const res = await fetch(`${base}/v1/settle`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n // The gateway accepts an empty body; only `limit` is recognised.\n // Always send a JSON body so the gateway's req.body shape parse\n // doesn't have to deal with an undefined.\n body: JSON.stringify(params ?? {}),\n });\n if (!res.ok) {\n throw new Error(`Gateway error: ${res.status} ${res.statusText}`);\n }\n return (await res.json()) as SettleResult;\n },\n };\n}\n"],"mappings":"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","names":["body"]}
|
|
@@ -70,8 +70,13 @@ function emptyAnchor() {
|
|
|
70
70
|
const ENCLAVE_TRUST_ANCHORS = Object.freeze({
|
|
71
71
|
// filled at fleet provisioning; verify fails closed while empty
|
|
72
72
|
[VANA_MAINNET_CHAIN_ID]: emptyAnchor(),
|
|
73
|
-
//
|
|
74
|
-
[MOKSHA_CHAIN_ID]:
|
|
73
|
+
// Moksha workers share this dstack app and KMS root; the controller does not derive owner keys.
|
|
74
|
+
[MOKSHA_CHAIN_ID]: Object.freeze({
|
|
75
|
+
kmsRootPubkey: "0x0434c76e0c3f52ec64cbf9bbf5c910c272330166fd656c0a86bb330963e46910e1a0e6fa51bec74be2f9129342707636060d85856d102cee8290185409ecf7422f",
|
|
76
|
+
appIds: Object.freeze([
|
|
77
|
+
"0xec9a39de98c760e1ded9f1e97016dc5f0e357cf2"
|
|
78
|
+
])
|
|
79
|
+
})
|
|
75
80
|
});
|
|
76
81
|
function userPsId(chainId, ownerAddress) {
|
|
77
82
|
const packed = (0, import_viem.encodePacked)(
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/protocol/identity.ts"],"sourcesContent":["/**\n * Enclave identity and consent primitives for Personal Servers.\n *\n * Owner/Web -> Gateway -> Agent(CVM): derive wallet and quote\n * Web verifies evidence; Account authenticates and encrypts the EIP-191 master\n * signature to the enclave key; Gateway relays it blind; Agent decrypts/seals.\n *\n * @category Protocol\n */\n\nimport * as secp256k1 from \"@noble/secp256k1\";\nimport {\n concat,\n encodePacked,\n fromHex,\n getAddress,\n isAddressEqual,\n keccak256,\n recoverPublicKey,\n toBytes,\n toHex,\n type Address,\n type Hex,\n} from \"viem\";\nimport { publicKeyToAddress } from \"viem/accounts\";\nimport { serializeECIES, type ECIESProvider } from \"../crypto/ecies/interface\";\nimport { deriveMasterKey, recoverServerOwner } from \"../crypto/keys/derive\";\nimport type { ServerRegistrationMessage } from \"./eip712\";\n\nexport const ENCLAVE_IDENTITY_EVIDENCE_VERSION = 1;\nexport const USER_PS_ID_DOMAIN = \"vana.ps-enclave.v1\";\nexport const ENCLAVE_WALLET_PURPOSE = \"vana.ps-enclave.wallet.v1\";\nexport const MASTER_SIGNATURE_DELIVERY_VERSION = \"vana.ps-enclave.delivery.v1\";\n/** Agent rejects deliveries when `|now - issuedAt|` exceeds this value. */\nexport const MASTER_SIGNATURE_DELIVERY_MAX_AGE_SECONDS = 600;\nexport const SEALED_ENVELOPE_VERSION = 1;\n\nconst VANA_MAINNET_CHAIN_ID = 1480;\nconst MOKSHA_CHAIN_ID = 14800;\nconst KMS_ISSUED_PREFIX = \"dstack-kms-issued\";\nconst PREIMAGE_SEPARATOR = \":\";\nconst UNCOMPRESSED_PUBLIC_KEY_BYTES = 65;\nconst UNCOMPRESSED_PUBLIC_KEY_PREFIX = \"04\";\nconst EMPTY_HEX = \"0x\";\n\n/** `keccak256(encodePacked([string,uint256,address], [USER_PS_ID_DOMAIN,chainId,owner]))`. */\nexport type UserPsId = Hex;\n\n/** Attested identity evidence returned by the enclave agent. */\nexport interface EnclaveIdentityEvidence {\n v: typeof ENCLAVE_IDENTITY_EVIDENCE_VERSION;\n userPsId: UserPsId;\n chainId: number;\n ownerAddress: Address;\n /** Path suffix `users/{id}/wallet/ethereum/secp256k1/v{epoch}`. */\n epoch: number;\n address: Address;\n /** 65-byte uncompressed `0x04..`; `publicKeyToAddress(publicKey) == address`. */\n publicKey: Hex;\n /** 20-byte dstack `app_id`. */\n appId: Hex;\n /** 32-byte compose hash. */\n composeHash: Hex;\n /** Omitted when the OS does not expose its image hash. */\n osImageHash?: Hex;\n /** Must equal `ENCLAVE_WALLET_PURPOSE`. */\n purpose: string;\n /** `[appRoot over link 0, kmsRoot over link 1]`; see `appRootPreimage` and `kmsIssuedPreimage`. */\n signatureChain: [Hex, Hex];\n /** Raw TDX quote with report_data `keccak256(userPsId || address)`; not parsed. */\n quote: Hex;\n eventLog?: string;\n /** `keccak256` of the uncompressed KMS root public key. */\n kmsRootFingerprint: Hex;\n}\n\n/** Identity values the caller expects the evidence to bind. */\nexport interface ExpectedIdentity {\n ownerAddress: Address;\n chainId: number;\n epoch: number;\n}\n\n/** Request for an owner-scoped enclave identity. */\nexport interface IdentityRequest {\n ownerAddress: Address;\n chainId: number;\n}\n\n/** Lifecycle state of an enclave identity. */\nexport type IdentityState = \"prepared\" | \"registered\" | \"sealed\" | \"retired\";\n\n/** Gateway response containing enclave identity state and registration data. */\nexport interface IdentityResponse {\n identity: EnclaveIdentityEvidence;\n state: IdentityState;\n created: boolean;\n /** Exact `ServerRegistration.serverUrl` the owner signs. */\n serverUrl: string;\n serverId?: Hex;\n serverStatus?:\n | \"pending\"\n | \"submitting\"\n | \"confirmed\"\n | \"finalized\"\n | \"failed\";\n sealed: boolean;\n}\n\n/** Versioned server-registration request for an enclave identity. */\nexport type IdentityRegistrationRequest =\n | { version: \"v2\"; message: ServerRegistrationMessage }\n | {\n version: \"v3\";\n message: ServerRegistrationMessage & { nonce: string; deadline: string };\n };\n\n/** Accepted identity-registration response. */\nexport interface IdentityRegistrationResponse {\n serverId: Hex;\n state: \"registered\";\n serverStatus: \"pending\";\n}\n\n/** Inner plaintext of the ECIES box. */\nexport interface MasterSignatureDelivery {\n v: typeof MASTER_SIGNATURE_DELIVERY_VERSION;\n userPsId: UserPsId;\n epoch: number;\n enclaveAddress: Address;\n ownerAddress: Address;\n /** 65-byte EIP-191 signature over `MASTER_KEY_MESSAGE`. */\n masterSignature: Hex;\n /** Unix seconds; see `MASTER_SIGNATURE_DELIVERY_MAX_AGE_SECONDS`. */\n issuedAt: number;\n}\n\n/** Enclave-bound ciphertext submitted for sealing. */\nexport interface SealedSecretSubmission {\n userPsId: UserPsId;\n epoch: number;\n enclaveAddress: Address;\n /** `iv(16) || ephemPub(65) || ct || mac(32)` to `evidence.publicKey`. */\n ciphertext: Hex;\n}\n\n/** Confirmation that the enclave sealed a submitted secret. */\nexport interface SealedSecretResponse {\n sealed: true;\n /** `sha256(ciphertext)`. */\n secretHash: Hex;\n sealedAt: string;\n}\n\n/** AES-GCM fields encoded as base64 strings. */\nexport interface AesGcmBox {\n /** Base64 initialization vector. */\n iv: string;\n /** Base64 ciphertext. */\n ciphertext: string;\n /** Base64 authentication tag. */\n tag: string;\n}\n\n/** Persisted enclave envelope; the Gateway treats it as opaque text. */\nexport interface SealedEnvelope extends AesGcmBox {\n v: typeof SEALED_ENVELOPE_VERSION;\n wrappedContentKey: AesGcmBox;\n}\n\n/** Fleet-pinned KMS root and allowed dstack application IDs. */\nexport interface EnclaveTrustAnchors {\n kmsRootPubkey: Hex;\n appIds: readonly Hex[];\n}\n\nfunction emptyAnchor(): Readonly<EnclaveTrustAnchors> {\n return Object.freeze({\n kmsRootPubkey: EMPTY_HEX,\n appIds: Object.freeze([] as Hex[]),\n });\n}\n\n/** Fleet-provisioned trust anchors keyed by Vana chain ID. */\nexport const ENCLAVE_TRUST_ANCHORS: Readonly<\n Record<number, Readonly<EnclaveTrustAnchors>>\n> = Object.freeze({\n // filled at fleet provisioning; verify fails closed while empty\n [VANA_MAINNET_CHAIN_ID]: emptyAnchor(),\n // filled at fleet provisioning; verify fails closed while empty\n [MOKSHA_CHAIN_ID]: emptyAnchor(),\n});\n\n/**\n * Derives the deterministic Personal Server ID used by enclave paths.\n *\n * @param chainId - Vana chain ID.\n * @param ownerAddress - Checksummed owner address.\n * @returns The path-compatible keccak256 digest.\n */\nexport function userPsId(chainId: number, ownerAddress: Address): UserPsId {\n const packed = encodePacked(\n [\"string\", \"uint256\", \"address\"],\n [USER_PS_ID_DOMAIN, BigInt(chainId), getAddress(ownerAddress)],\n );\n\n return keccak256(packed);\n}\n\nfunction compressPublicKey(publicKey: Hex): Uint8Array {\n return secp256k1.ProjectivePoint.fromHex(\n fromHex(publicKey, \"bytes\"),\n ).toRawBytes(true);\n}\n\nfunction sameKey(a: Hex, b: Hex): boolean {\n return toHex(compressPublicKey(a)) === toHex(compressPublicKey(b));\n}\n\nfunction assertUncompressedKey(publicKey: Hex): void {\n const hex = publicKey.slice(2);\n if (\n hex.length !== UNCOMPRESSED_PUBLIC_KEY_BYTES * 2 ||\n !hex.startsWith(UNCOMPRESSED_PUBLIC_KEY_PREFIX) ||\n !/^[0-9a-fA-F]+$/.test(hex)\n ) {\n throw new Error(\"Public key must be a 65-byte uncompressed secp256k1 key\");\n }\n}\n\n/**\n * Builds keccak256(utf8(purpose || \":\" || lowercase hex(compressed pubkey))).\n *\n * Matches `dstack/guest-agent/src/rpc_service.rs:612-628`.\n */\nexport function appRootPreimage(purpose: string, publicKey: Hex): Hex {\n const keyHex = toHex(compressPublicKey(publicKey)).slice(2);\n return keccak256(toBytes(`${purpose}${PREIMAGE_SEPARATOR}${keyHex}`));\n}\n\n/**\n * Hashes the KMS prefix, raw app ID bytes, and compressed app-root public key.\n *\n * Matches `dstack/kms/src/crypto.rs:23-40`; there is no separator between the\n * raw 20-byte app ID and the compressed key.\n */\nexport function kmsIssuedPreimage(appId: Hex, appRootPublicKey: Hex): Hex {\n const prefix = concat([\n toBytes(`${KMS_ISSUED_PREFIX}${PREIMAGE_SEPARATOR}`),\n fromHex(appId, \"bytes\"),\n ]);\n const compressed = compressPublicKey(appRootPublicKey);\n\n return keccak256(concat([prefix, compressed]));\n}\n\nasync function recoverChainKey(\n hash: Hex,\n signature: Hex,\n link: number,\n): Promise<Hex> {\n try {\n return (await recoverPublicKey({ hash, signature })).toLowerCase() as Hex;\n } catch {\n throw new Error(`Invalid enclave signature chain link ${link}`);\n }\n}\n\n/**\n * Verifies the enclave wallet binding against fleet-provisioned trust anchors.\n *\n * @param evidence - Identity evidence returned by the enclave agent.\n * @param anchors - Pinned KMS root and allowed dstack app IDs.\n * @param expected - Owner, chain, and epoch requested by the caller.\n *\n * @remarks\n * The dstack chain signs only `purpose || publicKey` and `appId || appRootPub`;\n * owner, chain and epoch are bound to the key by the TDX quote report_data\n * (`keccak256(userPsId || address)`), which v1 stores but does not verify\n * (DCAP is step 4). Until then the agent's derivation check is the backstop.\n * @throws When any identity binding or trust check fails.\n */\nexport async function verifyEnclaveIdentityEvidence(\n evidence: EnclaveIdentityEvidence,\n anchors: EnclaveTrustAnchors,\n expected: ExpectedIdentity,\n): Promise<void> {\n if (evidence.v !== ENCLAVE_IDENTITY_EVIDENCE_VERSION) {\n throw new Error(\"Unsupported enclave identity evidence version\");\n }\n\n if (!Number.isInteger(evidence.epoch) || evidence.epoch < 1) {\n throw new Error(\"Invalid enclave identity epoch\");\n }\n\n if (evidence.chainId !== expected.chainId) {\n throw new Error(\n \"Enclave identity chain ID does not match expected chain ID\",\n );\n }\n\n if (!isAddressEqual(evidence.ownerAddress, expected.ownerAddress)) {\n throw new Error(\"Enclave identity owner does not match expected owner\");\n }\n\n if (evidence.epoch !== expected.epoch) {\n throw new Error(\"Enclave identity epoch does not match expected epoch\");\n }\n\n const expectedUserPsId = userPsId(expected.chainId, expected.ownerAddress);\n if (evidence.userPsId.toLowerCase() !== expectedUserPsId.toLowerCase()) {\n throw new Error(\"Enclave userPsId does not match expected identity\");\n }\n\n if (evidence.purpose !== ENCLAVE_WALLET_PURPOSE) {\n throw new Error(\"Unexpected enclave wallet purpose\");\n }\n\n if (anchors.kmsRootPubkey === EMPTY_HEX) {\n throw new Error(\"KMS root trust anchor is not provisioned\");\n }\n\n assertUncompressedKey(evidence.publicKey);\n const derivedAddress = publicKeyToAddress(evidence.publicKey);\n\n if (getAddress(derivedAddress) !== getAddress(evidence.address)) {\n throw new Error(\"Enclave public key does not match its address\");\n }\n\n const appRootPublicKey = await recoverChainKey(\n appRootPreimage(evidence.purpose, evidence.publicKey),\n evidence.signatureChain[0],\n 0,\n );\n const kmsRootPublicKey = await recoverChainKey(\n kmsIssuedPreimage(evidence.appId, appRootPublicKey),\n evidence.signatureChain[1],\n 1,\n );\n\n let matchesAnchor: boolean;\n try {\n matchesAnchor = sameKey(kmsRootPublicKey, anchors.kmsRootPubkey);\n } catch {\n throw new Error(\"KMS root trust anchor is malformed\");\n }\n\n if (!matchesAnchor) {\n throw new Error(\"KMS root public key does not match the trust anchor\");\n }\n\n if (\n keccak256(kmsRootPublicKey) !== evidence.kmsRootFingerprint.toLowerCase()\n ) {\n throw new Error(\"KMS root fingerprint does not match the evidence\");\n }\n\n const appId = evidence.appId.toLowerCase();\n if (\n !anchors.appIds.some((allowedAppId) => allowedAppId.toLowerCase() === appId)\n ) {\n throw new Error(\"Enclave app ID is not trusted\");\n }\n}\n\n/**\n * Builds the authenticated master-signature plaintext delivered to an enclave.\n *\n * @param evidence - Verified enclave identity evidence.\n * @param masterSignature - EIP-191 signature over the master-key message.\n * @param now - Issuance time in Unix seconds.\n * @returns The authenticated plaintext delivery object.\n * @throws When the signature is malformed or does not belong to the owner.\n */\nexport async function buildMasterSignatureDelivery(\n evidence: EnclaveIdentityEvidence,\n masterSignature: Hex,\n now = Math.floor(Date.now() / 1000),\n): Promise<MasterSignatureDelivery> {\n // Validate signature length and hex encoding before recovery.\n deriveMasterKey(masterSignature);\n const signerAddress = await recoverServerOwner(masterSignature);\n\n if (!isAddressEqual(signerAddress, evidence.ownerAddress)) {\n throw new Error(\"Master signature signer does not match evidence owner\");\n }\n\n return {\n v: MASTER_SIGNATURE_DELIVERY_VERSION,\n userPsId: evidence.userPsId,\n epoch: evidence.epoch,\n enclaveAddress: evidence.address,\n ownerAddress: evidence.ownerAddress,\n masterSignature,\n issuedAt: now,\n };\n}\n\n/**\n * Encrypts a master-signature delivery in the SDK's ECIES wire format.\n *\n * @param delivery - Plaintext delivery object.\n * @param publicKey - Enclave 65-byte uncompressed public key.\n * @param ecies - Platform-specific ECIES provider.\n * @returns The 0x-prefixed serialized ciphertext.\n * @throws When the public key is malformed or belongs to another enclave.\n */\nexport async function encryptMasterSignatureDelivery(\n delivery: MasterSignatureDelivery,\n publicKey: Hex,\n ecies: ECIESProvider,\n): Promise<Hex> {\n assertUncompressedKey(publicKey);\n\n if (!isAddressEqual(publicKeyToAddress(publicKey), delivery.enclaveAddress)) {\n throw new Error(\n \"Public key does not belong to the delivery's enclave address\",\n );\n }\n\n const plaintext = toBytes(JSON.stringify(delivery));\n const encrypted = await ecies.encrypt(fromHex(publicKey, \"bytes\"), plaintext);\n\n return `0x${serializeECIES(encrypted)}`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUA,gBAA2B;AAC3B,kBAYO;AACP,sBAAmC;AACnC,uBAAmD;AACnD,oBAAoD;AAG7C,MAAM,oCAAoC;AAC1C,MAAM,oBAAoB;AAC1B,MAAM,yBAAyB;AAC/B,MAAM,oCAAoC;AAE1C,MAAM,4CAA4C;AAClD,MAAM,0BAA0B;AAEvC,MAAM,wBAAwB;AAC9B,MAAM,kBAAkB;AACxB,MAAM,oBAAoB;AAC1B,MAAM,qBAAqB;AAC3B,MAAM,gCAAgC;AACtC,MAAM,iCAAiC;AACvC,MAAM,YAAY;AAqIlB,SAAS,cAA6C;AACpD,SAAO,OAAO,OAAO;AAAA,IACnB,eAAe;AAAA,IACf,QAAQ,OAAO,OAAO,CAAC,CAAU;AAAA,EACnC,CAAC;AACH;AAGO,MAAM,wBAET,OAAO,OAAO;AAAA;AAAA,EAEhB,CAAC,qBAAqB,GAAG,YAAY;AAAA;AAAA,EAErC,CAAC,eAAe,GAAG,YAAY;AACjC,CAAC;AASM,SAAS,SAAS,SAAiB,cAAiC;AACzE,QAAM,aAAS;AAAA,IACb,CAAC,UAAU,WAAW,SAAS;AAAA,IAC/B,CAAC,mBAAmB,OAAO,OAAO,OAAG,wBAAW,YAAY,CAAC;AAAA,EAC/D;AAEA,aAAO,uBAAU,MAAM;AACzB;AAEA,SAAS,kBAAkB,WAA4B;AACrD,SAAO,UAAU,gBAAgB;AAAA,QAC/B,qBAAQ,WAAW,OAAO;AAAA,EAC5B,EAAE,WAAW,IAAI;AACnB;AAEA,SAAS,QAAQ,GAAQ,GAAiB;AACxC,aAAO,mBAAM,kBAAkB,CAAC,CAAC,UAAM,mBAAM,kBAAkB,CAAC,CAAC;AACnE;AAEA,SAAS,sBAAsB,WAAsB;AACnD,QAAM,MAAM,UAAU,MAAM,CAAC;AAC7B,MACE,IAAI,WAAW,gCAAgC,KAC/C,CAAC,IAAI,WAAW,8BAA8B,KAC9C,CAAC,iBAAiB,KAAK,GAAG,GAC1B;AACA,UAAM,IAAI,MAAM,yDAAyD;AAAA,EAC3E;AACF;AAOO,SAAS,gBAAgB,SAAiB,WAAqB;AACpE,QAAM,aAAS,mBAAM,kBAAkB,SAAS,CAAC,EAAE,MAAM,CAAC;AAC1D,aAAO,2BAAU,qBAAQ,GAAG,OAAO,GAAG,kBAAkB,GAAG,MAAM,EAAE,CAAC;AACtE;AAQO,SAAS,kBAAkB,OAAY,kBAA4B;AACxE,QAAM,aAAS,oBAAO;AAAA,QACpB,qBAAQ,GAAG,iBAAiB,GAAG,kBAAkB,EAAE;AAAA,QACnD,qBAAQ,OAAO,OAAO;AAAA,EACxB,CAAC;AACD,QAAM,aAAa,kBAAkB,gBAAgB;AAErD,aAAO,2BAAU,oBAAO,CAAC,QAAQ,UAAU,CAAC,CAAC;AAC/C;AAEA,eAAe,gBACb,MACA,WACA,MACc;AACd,MAAI;AACF,YAAQ,UAAM,8BAAiB,EAAE,MAAM,UAAU,CAAC,GAAG,YAAY;AAAA,EACnE,QAAQ;AACN,UAAM,IAAI,MAAM,wCAAwC,IAAI,EAAE;AAAA,EAChE;AACF;AAgBA,eAAsB,8BACpB,UACA,SACA,UACe;AACf,MAAI,SAAS,MAAM,mCAAmC;AACpD,UAAM,IAAI,MAAM,+CAA+C;AAAA,EACjE;AAEA,MAAI,CAAC,OAAO,UAAU,SAAS,KAAK,KAAK,SAAS,QAAQ,GAAG;AAC3D,UAAM,IAAI,MAAM,gCAAgC;AAAA,EAClD;AAEA,MAAI,SAAS,YAAY,SAAS,SAAS;AACzC,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAEA,MAAI,KAAC,4BAAe,SAAS,cAAc,SAAS,YAAY,GAAG;AACjE,UAAM,IAAI,MAAM,sDAAsD;AAAA,EACxE;AAEA,MAAI,SAAS,UAAU,SAAS,OAAO;AACrC,UAAM,IAAI,MAAM,sDAAsD;AAAA,EACxE;AAEA,QAAM,mBAAmB,SAAS,SAAS,SAAS,SAAS,YAAY;AACzE,MAAI,SAAS,SAAS,YAAY,MAAM,iBAAiB,YAAY,GAAG;AACtE,UAAM,IAAI,MAAM,mDAAmD;AAAA,EACrE;AAEA,MAAI,SAAS,YAAY,wBAAwB;AAC/C,UAAM,IAAI,MAAM,mCAAmC;AAAA,EACrD;AAEA,MAAI,QAAQ,kBAAkB,WAAW;AACvC,UAAM,IAAI,MAAM,0CAA0C;AAAA,EAC5D;AAEA,wBAAsB,SAAS,SAAS;AACxC,QAAM,qBAAiB,oCAAmB,SAAS,SAAS;AAE5D,UAAI,wBAAW,cAAc,UAAM,wBAAW,SAAS,OAAO,GAAG;AAC/D,UAAM,IAAI,MAAM,+CAA+C;AAAA,EACjE;AAEA,QAAM,mBAAmB,MAAM;AAAA,IAC7B,gBAAgB,SAAS,SAAS,SAAS,SAAS;AAAA,IACpD,SAAS,eAAe,CAAC;AAAA,IACzB;AAAA,EACF;AACA,QAAM,mBAAmB,MAAM;AAAA,IAC7B,kBAAkB,SAAS,OAAO,gBAAgB;AAAA,IAClD,SAAS,eAAe,CAAC;AAAA,IACzB;AAAA,EACF;AAEA,MAAI;AACJ,MAAI;AACF,oBAAgB,QAAQ,kBAAkB,QAAQ,aAAa;AAAA,EACjE,QAAQ;AACN,UAAM,IAAI,MAAM,oCAAoC;AAAA,EACtD;AAEA,MAAI,CAAC,eAAe;AAClB,UAAM,IAAI,MAAM,qDAAqD;AAAA,EACvE;AAEA,UACE,uBAAU,gBAAgB,MAAM,SAAS,mBAAmB,YAAY,GACxE;AACA,UAAM,IAAI,MAAM,kDAAkD;AAAA,EACpE;AAEA,QAAM,QAAQ,SAAS,MAAM,YAAY;AACzC,MACE,CAAC,QAAQ,OAAO,KAAK,CAAC,iBAAiB,aAAa,YAAY,MAAM,KAAK,GAC3E;AACA,UAAM,IAAI,MAAM,+BAA+B;AAAA,EACjD;AACF;AAWA,eAAsB,6BACpB,UACA,iBACA,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI,GACA;AAElC,qCAAgB,eAAe;AAC/B,QAAM,gBAAgB,UAAM,kCAAmB,eAAe;AAE9D,MAAI,KAAC,4BAAe,eAAe,SAAS,YAAY,GAAG;AACzD,UAAM,IAAI,MAAM,uDAAuD;AAAA,EACzE;AAEA,SAAO;AAAA,IACL,GAAG;AAAA,IACH,UAAU,SAAS;AAAA,IACnB,OAAO,SAAS;AAAA,IAChB,gBAAgB,SAAS;AAAA,IACzB,cAAc,SAAS;AAAA,IACvB;AAAA,IACA,UAAU;AAAA,EACZ;AACF;AAWA,eAAsB,+BACpB,UACA,WACA,OACc;AACd,wBAAsB,SAAS;AAE/B,MAAI,KAAC,gCAAe,oCAAmB,SAAS,GAAG,SAAS,cAAc,GAAG;AAC3E,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAEA,QAAM,gBAAY,qBAAQ,KAAK,UAAU,QAAQ,CAAC;AAClD,QAAM,YAAY,MAAM,MAAM,YAAQ,qBAAQ,WAAW,OAAO,GAAG,SAAS;AAE5E,SAAO,SAAK,iCAAe,SAAS,CAAC;AACvC;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/protocol/identity.ts"],"sourcesContent":["/**\n * Enclave identity and consent primitives for Personal Servers.\n *\n * Owner/Web -> Gateway -> Agent(CVM): derive wallet and quote\n * Web verifies evidence; Account authenticates and encrypts the EIP-191 master\n * signature to the enclave key; Gateway relays it blind; Agent decrypts/seals.\n *\n * @category Protocol\n */\n\nimport * as secp256k1 from \"@noble/secp256k1\";\nimport {\n concat,\n encodePacked,\n fromHex,\n getAddress,\n isAddressEqual,\n keccak256,\n recoverPublicKey,\n toBytes,\n toHex,\n type Address,\n type Hex,\n} from \"viem\";\nimport { publicKeyToAddress } from \"viem/accounts\";\nimport { serializeECIES, type ECIESProvider } from \"../crypto/ecies/interface\";\nimport { deriveMasterKey, recoverServerOwner } from \"../crypto/keys/derive\";\nimport type { ServerRegistrationMessage } from \"./eip712\";\n\nexport const ENCLAVE_IDENTITY_EVIDENCE_VERSION = 1;\nexport const USER_PS_ID_DOMAIN = \"vana.ps-enclave.v1\";\nexport const ENCLAVE_WALLET_PURPOSE = \"vana.ps-enclave.wallet.v1\";\nexport const MASTER_SIGNATURE_DELIVERY_VERSION = \"vana.ps-enclave.delivery.v1\";\n/** Agent rejects deliveries when `|now - issuedAt|` exceeds this value. */\nexport const MASTER_SIGNATURE_DELIVERY_MAX_AGE_SECONDS = 600;\nexport const SEALED_ENVELOPE_VERSION = 1;\n\nconst VANA_MAINNET_CHAIN_ID = 1480;\nconst MOKSHA_CHAIN_ID = 14800;\nconst KMS_ISSUED_PREFIX = \"dstack-kms-issued\";\nconst PREIMAGE_SEPARATOR = \":\";\nconst UNCOMPRESSED_PUBLIC_KEY_BYTES = 65;\nconst UNCOMPRESSED_PUBLIC_KEY_PREFIX = \"04\";\nconst EMPTY_HEX = \"0x\";\n\n/** `keccak256(encodePacked([string,uint256,address], [USER_PS_ID_DOMAIN,chainId,owner]))`. */\nexport type UserPsId = Hex;\n\n/** Attested identity evidence returned by the enclave agent. */\nexport interface EnclaveIdentityEvidence {\n v: typeof ENCLAVE_IDENTITY_EVIDENCE_VERSION;\n userPsId: UserPsId;\n chainId: number;\n ownerAddress: Address;\n /** Path suffix `users/{id}/wallet/ethereum/secp256k1/v{epoch}`. */\n epoch: number;\n address: Address;\n /** 65-byte uncompressed `0x04..`; `publicKeyToAddress(publicKey) == address`. */\n publicKey: Hex;\n /** 20-byte dstack `app_id`. */\n appId: Hex;\n /** 32-byte compose hash. */\n composeHash: Hex;\n /** Omitted when the OS does not expose its image hash. */\n osImageHash?: Hex;\n /** Must equal `ENCLAVE_WALLET_PURPOSE`. */\n purpose: string;\n /** `[appRoot over link 0, kmsRoot over link 1]`; see `appRootPreimage` and `kmsIssuedPreimage`. */\n signatureChain: [Hex, Hex];\n /** Raw TDX quote with report_data `keccak256(userPsId || address)`; not parsed. */\n quote: Hex;\n eventLog?: string;\n /** `keccak256` of the uncompressed KMS root public key. */\n kmsRootFingerprint: Hex;\n}\n\n/** Identity values the caller expects the evidence to bind. */\nexport interface ExpectedIdentity {\n ownerAddress: Address;\n chainId: number;\n epoch: number;\n}\n\n/** Request for an owner-scoped enclave identity. */\nexport interface IdentityRequest {\n ownerAddress: Address;\n chainId: number;\n}\n\n/** Lifecycle state of an enclave identity. */\nexport type IdentityState = \"prepared\" | \"registered\" | \"sealed\" | \"retired\";\n\n/** Gateway response containing enclave identity state and registration data. */\nexport interface IdentityResponse {\n identity: EnclaveIdentityEvidence;\n state: IdentityState;\n created: boolean;\n /** Exact `ServerRegistration.serverUrl` the owner signs. */\n serverUrl: string;\n serverId?: Hex;\n serverStatus?:\n | \"pending\"\n | \"submitting\"\n | \"confirmed\"\n | \"finalized\"\n | \"failed\";\n sealed: boolean;\n}\n\n/** Versioned server-registration request for an enclave identity. */\nexport type IdentityRegistrationRequest =\n | { version: \"v2\"; message: ServerRegistrationMessage }\n | {\n version: \"v3\";\n message: ServerRegistrationMessage & { nonce: string; deadline: string };\n };\n\n/** Accepted identity-registration response. */\nexport interface IdentityRegistrationResponse {\n serverId: Hex;\n state: \"registered\";\n serverStatus: \"pending\";\n}\n\n/** Inner plaintext of the ECIES box. */\nexport interface MasterSignatureDelivery {\n v: typeof MASTER_SIGNATURE_DELIVERY_VERSION;\n userPsId: UserPsId;\n epoch: number;\n enclaveAddress: Address;\n ownerAddress: Address;\n /** 65-byte EIP-191 signature over `MASTER_KEY_MESSAGE`. */\n masterSignature: Hex;\n /** Unix seconds; see `MASTER_SIGNATURE_DELIVERY_MAX_AGE_SECONDS`. */\n issuedAt: number;\n}\n\n/** Enclave-bound ciphertext submitted for sealing. */\nexport interface SealedSecretSubmission {\n userPsId: UserPsId;\n epoch: number;\n enclaveAddress: Address;\n /** `iv(16) || ephemPub(65) || ct || mac(32)` to `evidence.publicKey`. */\n ciphertext: Hex;\n}\n\n/** Confirmation that the enclave sealed a submitted secret. */\nexport interface SealedSecretResponse {\n sealed: true;\n /** `sha256(ciphertext)`. */\n secretHash: Hex;\n sealedAt: string;\n}\n\n/** AES-GCM fields encoded as base64 strings. */\nexport interface AesGcmBox {\n /** Base64 initialization vector. */\n iv: string;\n /** Base64 ciphertext. */\n ciphertext: string;\n /** Base64 authentication tag. */\n tag: string;\n}\n\n/** Persisted enclave envelope; the Gateway treats it as opaque text. */\nexport interface SealedEnvelope extends AesGcmBox {\n v: typeof SEALED_ENVELOPE_VERSION;\n wrappedContentKey: AesGcmBox;\n}\n\n/** Fleet-pinned KMS root and allowed dstack application IDs. */\nexport interface EnclaveTrustAnchors {\n kmsRootPubkey: Hex;\n appIds: readonly Hex[];\n}\n\nfunction emptyAnchor(): Readonly<EnclaveTrustAnchors> {\n return Object.freeze({\n kmsRootPubkey: EMPTY_HEX,\n appIds: Object.freeze([] as Hex[]),\n });\n}\n\n/** Fleet-provisioned trust anchors keyed by Vana chain ID. */\nexport const ENCLAVE_TRUST_ANCHORS: Readonly<\n Record<number, Readonly<EnclaveTrustAnchors>>\n> = Object.freeze({\n // filled at fleet provisioning; verify fails closed while empty\n [VANA_MAINNET_CHAIN_ID]: emptyAnchor(),\n // Moksha workers share this dstack app and KMS root; the controller does not derive owner keys.\n [MOKSHA_CHAIN_ID]: Object.freeze({\n kmsRootPubkey:\n \"0x0434c76e0c3f52ec64cbf9bbf5c910c272330166fd656c0a86bb330963e46910e1a0e6fa51bec74be2f9129342707636060d85856d102cee8290185409ecf7422f\",\n appIds: Object.freeze([\n \"0xec9a39de98c760e1ded9f1e97016dc5f0e357cf2\" as Hex,\n ]),\n }),\n});\n\n/**\n * Derives the deterministic Personal Server ID used by enclave paths.\n *\n * @param chainId - Vana chain ID.\n * @param ownerAddress - Checksummed owner address.\n * @returns The path-compatible keccak256 digest.\n */\nexport function userPsId(chainId: number, ownerAddress: Address): UserPsId {\n const packed = encodePacked(\n [\"string\", \"uint256\", \"address\"],\n [USER_PS_ID_DOMAIN, BigInt(chainId), getAddress(ownerAddress)],\n );\n\n return keccak256(packed);\n}\n\nfunction compressPublicKey(publicKey: Hex): Uint8Array {\n return secp256k1.ProjectivePoint.fromHex(\n fromHex(publicKey, \"bytes\"),\n ).toRawBytes(true);\n}\n\nfunction sameKey(a: Hex, b: Hex): boolean {\n return toHex(compressPublicKey(a)) === toHex(compressPublicKey(b));\n}\n\nfunction assertUncompressedKey(publicKey: Hex): void {\n const hex = publicKey.slice(2);\n if (\n hex.length !== UNCOMPRESSED_PUBLIC_KEY_BYTES * 2 ||\n !hex.startsWith(UNCOMPRESSED_PUBLIC_KEY_PREFIX) ||\n !/^[0-9a-fA-F]+$/.test(hex)\n ) {\n throw new Error(\"Public key must be a 65-byte uncompressed secp256k1 key\");\n }\n}\n\n/**\n * Builds keccak256(utf8(purpose || \":\" || lowercase hex(compressed pubkey))).\n *\n * Matches `dstack/guest-agent/src/rpc_service.rs:612-628`.\n */\nexport function appRootPreimage(purpose: string, publicKey: Hex): Hex {\n const keyHex = toHex(compressPublicKey(publicKey)).slice(2);\n return keccak256(toBytes(`${purpose}${PREIMAGE_SEPARATOR}${keyHex}`));\n}\n\n/**\n * Hashes the KMS prefix, raw app ID bytes, and compressed app-root public key.\n *\n * Matches `dstack/kms/src/crypto.rs:23-40`; there is no separator between the\n * raw 20-byte app ID and the compressed key.\n */\nexport function kmsIssuedPreimage(appId: Hex, appRootPublicKey: Hex): Hex {\n const prefix = concat([\n toBytes(`${KMS_ISSUED_PREFIX}${PREIMAGE_SEPARATOR}`),\n fromHex(appId, \"bytes\"),\n ]);\n const compressed = compressPublicKey(appRootPublicKey);\n\n return keccak256(concat([prefix, compressed]));\n}\n\nasync function recoverChainKey(\n hash: Hex,\n signature: Hex,\n link: number,\n): Promise<Hex> {\n try {\n return (await recoverPublicKey({ hash, signature })).toLowerCase() as Hex;\n } catch {\n throw new Error(`Invalid enclave signature chain link ${link}`);\n }\n}\n\n/**\n * Verifies the enclave wallet binding against fleet-provisioned trust anchors.\n *\n * @param evidence - Identity evidence returned by the enclave agent.\n * @param anchors - Pinned KMS root and allowed dstack app IDs.\n * @param expected - Owner, chain, and epoch requested by the caller.\n *\n * @remarks\n * The dstack chain signs only `purpose || publicKey` and `appId || appRootPub`;\n * owner, chain and epoch are bound to the key by the TDX quote report_data\n * (`keccak256(userPsId || address)`), which v1 stores but does not verify\n * (DCAP is step 4). Until then the agent's derivation check is the backstop.\n * @throws When any identity binding or trust check fails.\n */\nexport async function verifyEnclaveIdentityEvidence(\n evidence: EnclaveIdentityEvidence,\n anchors: EnclaveTrustAnchors,\n expected: ExpectedIdentity,\n): Promise<void> {\n if (evidence.v !== ENCLAVE_IDENTITY_EVIDENCE_VERSION) {\n throw new Error(\"Unsupported enclave identity evidence version\");\n }\n\n if (!Number.isInteger(evidence.epoch) || evidence.epoch < 1) {\n throw new Error(\"Invalid enclave identity epoch\");\n }\n\n if (evidence.chainId !== expected.chainId) {\n throw new Error(\n \"Enclave identity chain ID does not match expected chain ID\",\n );\n }\n\n if (!isAddressEqual(evidence.ownerAddress, expected.ownerAddress)) {\n throw new Error(\"Enclave identity owner does not match expected owner\");\n }\n\n if (evidence.epoch !== expected.epoch) {\n throw new Error(\"Enclave identity epoch does not match expected epoch\");\n }\n\n const expectedUserPsId = userPsId(expected.chainId, expected.ownerAddress);\n if (evidence.userPsId.toLowerCase() !== expectedUserPsId.toLowerCase()) {\n throw new Error(\"Enclave userPsId does not match expected identity\");\n }\n\n if (evidence.purpose !== ENCLAVE_WALLET_PURPOSE) {\n throw new Error(\"Unexpected enclave wallet purpose\");\n }\n\n if (anchors.kmsRootPubkey === EMPTY_HEX) {\n throw new Error(\"KMS root trust anchor is not provisioned\");\n }\n\n assertUncompressedKey(evidence.publicKey);\n const derivedAddress = publicKeyToAddress(evidence.publicKey);\n\n if (getAddress(derivedAddress) !== getAddress(evidence.address)) {\n throw new Error(\"Enclave public key does not match its address\");\n }\n\n const appRootPublicKey = await recoverChainKey(\n appRootPreimage(evidence.purpose, evidence.publicKey),\n evidence.signatureChain[0],\n 0,\n );\n const kmsRootPublicKey = await recoverChainKey(\n kmsIssuedPreimage(evidence.appId, appRootPublicKey),\n evidence.signatureChain[1],\n 1,\n );\n\n let matchesAnchor: boolean;\n try {\n matchesAnchor = sameKey(kmsRootPublicKey, anchors.kmsRootPubkey);\n } catch {\n throw new Error(\"KMS root trust anchor is malformed\");\n }\n\n if (!matchesAnchor) {\n throw new Error(\"KMS root public key does not match the trust anchor\");\n }\n\n if (\n keccak256(kmsRootPublicKey) !== evidence.kmsRootFingerprint.toLowerCase()\n ) {\n throw new Error(\"KMS root fingerprint does not match the evidence\");\n }\n\n const appId = evidence.appId.toLowerCase();\n if (\n !anchors.appIds.some((allowedAppId) => allowedAppId.toLowerCase() === appId)\n ) {\n throw new Error(\"Enclave app ID is not trusted\");\n }\n}\n\n/**\n * Builds the authenticated master-signature plaintext delivered to an enclave.\n *\n * @param evidence - Verified enclave identity evidence.\n * @param masterSignature - EIP-191 signature over the master-key message.\n * @param now - Issuance time in Unix seconds.\n * @returns The authenticated plaintext delivery object.\n * @throws When the signature is malformed or does not belong to the owner.\n */\nexport async function buildMasterSignatureDelivery(\n evidence: EnclaveIdentityEvidence,\n masterSignature: Hex,\n now = Math.floor(Date.now() / 1000),\n): Promise<MasterSignatureDelivery> {\n // Validate signature length and hex encoding before recovery.\n deriveMasterKey(masterSignature);\n const signerAddress = await recoverServerOwner(masterSignature);\n\n if (!isAddressEqual(signerAddress, evidence.ownerAddress)) {\n throw new Error(\"Master signature signer does not match evidence owner\");\n }\n\n return {\n v: MASTER_SIGNATURE_DELIVERY_VERSION,\n userPsId: evidence.userPsId,\n epoch: evidence.epoch,\n enclaveAddress: evidence.address,\n ownerAddress: evidence.ownerAddress,\n masterSignature,\n issuedAt: now,\n };\n}\n\n/**\n * Encrypts a master-signature delivery in the SDK's ECIES wire format.\n *\n * @param delivery - Plaintext delivery object.\n * @param publicKey - Enclave 65-byte uncompressed public key.\n * @param ecies - Platform-specific ECIES provider.\n * @returns The 0x-prefixed serialized ciphertext.\n * @throws When the public key is malformed or belongs to another enclave.\n */\nexport async function encryptMasterSignatureDelivery(\n delivery: MasterSignatureDelivery,\n publicKey: Hex,\n ecies: ECIESProvider,\n): Promise<Hex> {\n assertUncompressedKey(publicKey);\n\n if (!isAddressEqual(publicKeyToAddress(publicKey), delivery.enclaveAddress)) {\n throw new Error(\n \"Public key does not belong to the delivery's enclave address\",\n );\n }\n\n const plaintext = toBytes(JSON.stringify(delivery));\n const encrypted = await ecies.encrypt(fromHex(publicKey, \"bytes\"), plaintext);\n\n return `0x${serializeECIES(encrypted)}`;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -35,8 +35,13 @@ function emptyAnchor() {
|
|
|
35
35
|
const ENCLAVE_TRUST_ANCHORS = Object.freeze({
|
|
36
36
|
// filled at fleet provisioning; verify fails closed while empty
|
|
37
37
|
[VANA_MAINNET_CHAIN_ID]: emptyAnchor(),
|
|
38
|
-
//
|
|
39
|
-
[MOKSHA_CHAIN_ID]:
|
|
38
|
+
// Moksha workers share this dstack app and KMS root; the controller does not derive owner keys.
|
|
39
|
+
[MOKSHA_CHAIN_ID]: Object.freeze({
|
|
40
|
+
kmsRootPubkey: "0x0434c76e0c3f52ec64cbf9bbf5c910c272330166fd656c0a86bb330963e46910e1a0e6fa51bec74be2f9129342707636060d85856d102cee8290185409ecf7422f",
|
|
41
|
+
appIds: Object.freeze([
|
|
42
|
+
"0xec9a39de98c760e1ded9f1e97016dc5f0e357cf2"
|
|
43
|
+
])
|
|
44
|
+
})
|
|
40
45
|
});
|
|
41
46
|
function userPsId(chainId, ownerAddress) {
|
|
42
47
|
const packed = encodePacked(
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/protocol/identity.ts"],"sourcesContent":["/**\n * Enclave identity and consent primitives for Personal Servers.\n *\n * Owner/Web -> Gateway -> Agent(CVM): derive wallet and quote\n * Web verifies evidence; Account authenticates and encrypts the EIP-191 master\n * signature to the enclave key; Gateway relays it blind; Agent decrypts/seals.\n *\n * @category Protocol\n */\n\nimport * as secp256k1 from \"@noble/secp256k1\";\nimport {\n concat,\n encodePacked,\n fromHex,\n getAddress,\n isAddressEqual,\n keccak256,\n recoverPublicKey,\n toBytes,\n toHex,\n type Address,\n type Hex,\n} from \"viem\";\nimport { publicKeyToAddress } from \"viem/accounts\";\nimport { serializeECIES, type ECIESProvider } from \"../crypto/ecies/interface\";\nimport { deriveMasterKey, recoverServerOwner } from \"../crypto/keys/derive\";\nimport type { ServerRegistrationMessage } from \"./eip712\";\n\nexport const ENCLAVE_IDENTITY_EVIDENCE_VERSION = 1;\nexport const USER_PS_ID_DOMAIN = \"vana.ps-enclave.v1\";\nexport const ENCLAVE_WALLET_PURPOSE = \"vana.ps-enclave.wallet.v1\";\nexport const MASTER_SIGNATURE_DELIVERY_VERSION = \"vana.ps-enclave.delivery.v1\";\n/** Agent rejects deliveries when `|now - issuedAt|` exceeds this value. */\nexport const MASTER_SIGNATURE_DELIVERY_MAX_AGE_SECONDS = 600;\nexport const SEALED_ENVELOPE_VERSION = 1;\n\nconst VANA_MAINNET_CHAIN_ID = 1480;\nconst MOKSHA_CHAIN_ID = 14800;\nconst KMS_ISSUED_PREFIX = \"dstack-kms-issued\";\nconst PREIMAGE_SEPARATOR = \":\";\nconst UNCOMPRESSED_PUBLIC_KEY_BYTES = 65;\nconst UNCOMPRESSED_PUBLIC_KEY_PREFIX = \"04\";\nconst EMPTY_HEX = \"0x\";\n\n/** `keccak256(encodePacked([string,uint256,address], [USER_PS_ID_DOMAIN,chainId,owner]))`. */\nexport type UserPsId = Hex;\n\n/** Attested identity evidence returned by the enclave agent. */\nexport interface EnclaveIdentityEvidence {\n v: typeof ENCLAVE_IDENTITY_EVIDENCE_VERSION;\n userPsId: UserPsId;\n chainId: number;\n ownerAddress: Address;\n /** Path suffix `users/{id}/wallet/ethereum/secp256k1/v{epoch}`. */\n epoch: number;\n address: Address;\n /** 65-byte uncompressed `0x04..`; `publicKeyToAddress(publicKey) == address`. */\n publicKey: Hex;\n /** 20-byte dstack `app_id`. */\n appId: Hex;\n /** 32-byte compose hash. */\n composeHash: Hex;\n /** Omitted when the OS does not expose its image hash. */\n osImageHash?: Hex;\n /** Must equal `ENCLAVE_WALLET_PURPOSE`. */\n purpose: string;\n /** `[appRoot over link 0, kmsRoot over link 1]`; see `appRootPreimage` and `kmsIssuedPreimage`. */\n signatureChain: [Hex, Hex];\n /** Raw TDX quote with report_data `keccak256(userPsId || address)`; not parsed. */\n quote: Hex;\n eventLog?: string;\n /** `keccak256` of the uncompressed KMS root public key. */\n kmsRootFingerprint: Hex;\n}\n\n/** Identity values the caller expects the evidence to bind. */\nexport interface ExpectedIdentity {\n ownerAddress: Address;\n chainId: number;\n epoch: number;\n}\n\n/** Request for an owner-scoped enclave identity. */\nexport interface IdentityRequest {\n ownerAddress: Address;\n chainId: number;\n}\n\n/** Lifecycle state of an enclave identity. */\nexport type IdentityState = \"prepared\" | \"registered\" | \"sealed\" | \"retired\";\n\n/** Gateway response containing enclave identity state and registration data. */\nexport interface IdentityResponse {\n identity: EnclaveIdentityEvidence;\n state: IdentityState;\n created: boolean;\n /** Exact `ServerRegistration.serverUrl` the owner signs. */\n serverUrl: string;\n serverId?: Hex;\n serverStatus?:\n | \"pending\"\n | \"submitting\"\n | \"confirmed\"\n | \"finalized\"\n | \"failed\";\n sealed: boolean;\n}\n\n/** Versioned server-registration request for an enclave identity. */\nexport type IdentityRegistrationRequest =\n | { version: \"v2\"; message: ServerRegistrationMessage }\n | {\n version: \"v3\";\n message: ServerRegistrationMessage & { nonce: string; deadline: string };\n };\n\n/** Accepted identity-registration response. */\nexport interface IdentityRegistrationResponse {\n serverId: Hex;\n state: \"registered\";\n serverStatus: \"pending\";\n}\n\n/** Inner plaintext of the ECIES box. */\nexport interface MasterSignatureDelivery {\n v: typeof MASTER_SIGNATURE_DELIVERY_VERSION;\n userPsId: UserPsId;\n epoch: number;\n enclaveAddress: Address;\n ownerAddress: Address;\n /** 65-byte EIP-191 signature over `MASTER_KEY_MESSAGE`. */\n masterSignature: Hex;\n /** Unix seconds; see `MASTER_SIGNATURE_DELIVERY_MAX_AGE_SECONDS`. */\n issuedAt: number;\n}\n\n/** Enclave-bound ciphertext submitted for sealing. */\nexport interface SealedSecretSubmission {\n userPsId: UserPsId;\n epoch: number;\n enclaveAddress: Address;\n /** `iv(16) || ephemPub(65) || ct || mac(32)` to `evidence.publicKey`. */\n ciphertext: Hex;\n}\n\n/** Confirmation that the enclave sealed a submitted secret. */\nexport interface SealedSecretResponse {\n sealed: true;\n /** `sha256(ciphertext)`. */\n secretHash: Hex;\n sealedAt: string;\n}\n\n/** AES-GCM fields encoded as base64 strings. */\nexport interface AesGcmBox {\n /** Base64 initialization vector. */\n iv: string;\n /** Base64 ciphertext. */\n ciphertext: string;\n /** Base64 authentication tag. */\n tag: string;\n}\n\n/** Persisted enclave envelope; the Gateway treats it as opaque text. */\nexport interface SealedEnvelope extends AesGcmBox {\n v: typeof SEALED_ENVELOPE_VERSION;\n wrappedContentKey: AesGcmBox;\n}\n\n/** Fleet-pinned KMS root and allowed dstack application IDs. */\nexport interface EnclaveTrustAnchors {\n kmsRootPubkey: Hex;\n appIds: readonly Hex[];\n}\n\nfunction emptyAnchor(): Readonly<EnclaveTrustAnchors> {\n return Object.freeze({\n kmsRootPubkey: EMPTY_HEX,\n appIds: Object.freeze([] as Hex[]),\n });\n}\n\n/** Fleet-provisioned trust anchors keyed by Vana chain ID. */\nexport const ENCLAVE_TRUST_ANCHORS: Readonly<\n Record<number, Readonly<EnclaveTrustAnchors>>\n> = Object.freeze({\n // filled at fleet provisioning; verify fails closed while empty\n [VANA_MAINNET_CHAIN_ID]: emptyAnchor(),\n // filled at fleet provisioning; verify fails closed while empty\n [MOKSHA_CHAIN_ID]: emptyAnchor(),\n});\n\n/**\n * Derives the deterministic Personal Server ID used by enclave paths.\n *\n * @param chainId - Vana chain ID.\n * @param ownerAddress - Checksummed owner address.\n * @returns The path-compatible keccak256 digest.\n */\nexport function userPsId(chainId: number, ownerAddress: Address): UserPsId {\n const packed = encodePacked(\n [\"string\", \"uint256\", \"address\"],\n [USER_PS_ID_DOMAIN, BigInt(chainId), getAddress(ownerAddress)],\n );\n\n return keccak256(packed);\n}\n\nfunction compressPublicKey(publicKey: Hex): Uint8Array {\n return secp256k1.ProjectivePoint.fromHex(\n fromHex(publicKey, \"bytes\"),\n ).toRawBytes(true);\n}\n\nfunction sameKey(a: Hex, b: Hex): boolean {\n return toHex(compressPublicKey(a)) === toHex(compressPublicKey(b));\n}\n\nfunction assertUncompressedKey(publicKey: Hex): void {\n const hex = publicKey.slice(2);\n if (\n hex.length !== UNCOMPRESSED_PUBLIC_KEY_BYTES * 2 ||\n !hex.startsWith(UNCOMPRESSED_PUBLIC_KEY_PREFIX) ||\n !/^[0-9a-fA-F]+$/.test(hex)\n ) {\n throw new Error(\"Public key must be a 65-byte uncompressed secp256k1 key\");\n }\n}\n\n/**\n * Builds keccak256(utf8(purpose || \":\" || lowercase hex(compressed pubkey))).\n *\n * Matches `dstack/guest-agent/src/rpc_service.rs:612-628`.\n */\nexport function appRootPreimage(purpose: string, publicKey: Hex): Hex {\n const keyHex = toHex(compressPublicKey(publicKey)).slice(2);\n return keccak256(toBytes(`${purpose}${PREIMAGE_SEPARATOR}${keyHex}`));\n}\n\n/**\n * Hashes the KMS prefix, raw app ID bytes, and compressed app-root public key.\n *\n * Matches `dstack/kms/src/crypto.rs:23-40`; there is no separator between the\n * raw 20-byte app ID and the compressed key.\n */\nexport function kmsIssuedPreimage(appId: Hex, appRootPublicKey: Hex): Hex {\n const prefix = concat([\n toBytes(`${KMS_ISSUED_PREFIX}${PREIMAGE_SEPARATOR}`),\n fromHex(appId, \"bytes\"),\n ]);\n const compressed = compressPublicKey(appRootPublicKey);\n\n return keccak256(concat([prefix, compressed]));\n}\n\nasync function recoverChainKey(\n hash: Hex,\n signature: Hex,\n link: number,\n): Promise<Hex> {\n try {\n return (await recoverPublicKey({ hash, signature })).toLowerCase() as Hex;\n } catch {\n throw new Error(`Invalid enclave signature chain link ${link}`);\n }\n}\n\n/**\n * Verifies the enclave wallet binding against fleet-provisioned trust anchors.\n *\n * @param evidence - Identity evidence returned by the enclave agent.\n * @param anchors - Pinned KMS root and allowed dstack app IDs.\n * @param expected - Owner, chain, and epoch requested by the caller.\n *\n * @remarks\n * The dstack chain signs only `purpose || publicKey` and `appId || appRootPub`;\n * owner, chain and epoch are bound to the key by the TDX quote report_data\n * (`keccak256(userPsId || address)`), which v1 stores but does not verify\n * (DCAP is step 4). Until then the agent's derivation check is the backstop.\n * @throws When any identity binding or trust check fails.\n */\nexport async function verifyEnclaveIdentityEvidence(\n evidence: EnclaveIdentityEvidence,\n anchors: EnclaveTrustAnchors,\n expected: ExpectedIdentity,\n): Promise<void> {\n if (evidence.v !== ENCLAVE_IDENTITY_EVIDENCE_VERSION) {\n throw new Error(\"Unsupported enclave identity evidence version\");\n }\n\n if (!Number.isInteger(evidence.epoch) || evidence.epoch < 1) {\n throw new Error(\"Invalid enclave identity epoch\");\n }\n\n if (evidence.chainId !== expected.chainId) {\n throw new Error(\n \"Enclave identity chain ID does not match expected chain ID\",\n );\n }\n\n if (!isAddressEqual(evidence.ownerAddress, expected.ownerAddress)) {\n throw new Error(\"Enclave identity owner does not match expected owner\");\n }\n\n if (evidence.epoch !== expected.epoch) {\n throw new Error(\"Enclave identity epoch does not match expected epoch\");\n }\n\n const expectedUserPsId = userPsId(expected.chainId, expected.ownerAddress);\n if (evidence.userPsId.toLowerCase() !== expectedUserPsId.toLowerCase()) {\n throw new Error(\"Enclave userPsId does not match expected identity\");\n }\n\n if (evidence.purpose !== ENCLAVE_WALLET_PURPOSE) {\n throw new Error(\"Unexpected enclave wallet purpose\");\n }\n\n if (anchors.kmsRootPubkey === EMPTY_HEX) {\n throw new Error(\"KMS root trust anchor is not provisioned\");\n }\n\n assertUncompressedKey(evidence.publicKey);\n const derivedAddress = publicKeyToAddress(evidence.publicKey);\n\n if (getAddress(derivedAddress) !== getAddress(evidence.address)) {\n throw new Error(\"Enclave public key does not match its address\");\n }\n\n const appRootPublicKey = await recoverChainKey(\n appRootPreimage(evidence.purpose, evidence.publicKey),\n evidence.signatureChain[0],\n 0,\n );\n const kmsRootPublicKey = await recoverChainKey(\n kmsIssuedPreimage(evidence.appId, appRootPublicKey),\n evidence.signatureChain[1],\n 1,\n );\n\n let matchesAnchor: boolean;\n try {\n matchesAnchor = sameKey(kmsRootPublicKey, anchors.kmsRootPubkey);\n } catch {\n throw new Error(\"KMS root trust anchor is malformed\");\n }\n\n if (!matchesAnchor) {\n throw new Error(\"KMS root public key does not match the trust anchor\");\n }\n\n if (\n keccak256(kmsRootPublicKey) !== evidence.kmsRootFingerprint.toLowerCase()\n ) {\n throw new Error(\"KMS root fingerprint does not match the evidence\");\n }\n\n const appId = evidence.appId.toLowerCase();\n if (\n !anchors.appIds.some((allowedAppId) => allowedAppId.toLowerCase() === appId)\n ) {\n throw new Error(\"Enclave app ID is not trusted\");\n }\n}\n\n/**\n * Builds the authenticated master-signature plaintext delivered to an enclave.\n *\n * @param evidence - Verified enclave identity evidence.\n * @param masterSignature - EIP-191 signature over the master-key message.\n * @param now - Issuance time in Unix seconds.\n * @returns The authenticated plaintext delivery object.\n * @throws When the signature is malformed or does not belong to the owner.\n */\nexport async function buildMasterSignatureDelivery(\n evidence: EnclaveIdentityEvidence,\n masterSignature: Hex,\n now = Math.floor(Date.now() / 1000),\n): Promise<MasterSignatureDelivery> {\n // Validate signature length and hex encoding before recovery.\n deriveMasterKey(masterSignature);\n const signerAddress = await recoverServerOwner(masterSignature);\n\n if (!isAddressEqual(signerAddress, evidence.ownerAddress)) {\n throw new Error(\"Master signature signer does not match evidence owner\");\n }\n\n return {\n v: MASTER_SIGNATURE_DELIVERY_VERSION,\n userPsId: evidence.userPsId,\n epoch: evidence.epoch,\n enclaveAddress: evidence.address,\n ownerAddress: evidence.ownerAddress,\n masterSignature,\n issuedAt: now,\n };\n}\n\n/**\n * Encrypts a master-signature delivery in the SDK's ECIES wire format.\n *\n * @param delivery - Plaintext delivery object.\n * @param publicKey - Enclave 65-byte uncompressed public key.\n * @param ecies - Platform-specific ECIES provider.\n * @returns The 0x-prefixed serialized ciphertext.\n * @throws When the public key is malformed or belongs to another enclave.\n */\nexport async function encryptMasterSignatureDelivery(\n delivery: MasterSignatureDelivery,\n publicKey: Hex,\n ecies: ECIESProvider,\n): Promise<Hex> {\n assertUncompressedKey(publicKey);\n\n if (!isAddressEqual(publicKeyToAddress(publicKey), delivery.enclaveAddress)) {\n throw new Error(\n \"Public key does not belong to the delivery's enclave address\",\n );\n }\n\n const plaintext = toBytes(JSON.stringify(delivery));\n const encrypted = await ecies.encrypt(fromHex(publicKey, \"bytes\"), plaintext);\n\n return `0x${serializeECIES(encrypted)}`;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/protocol/identity.ts"],"sourcesContent":["/**\n * Enclave identity and consent primitives for Personal Servers.\n *\n * Owner/Web -> Gateway -> Agent(CVM): derive wallet and quote\n * Web verifies evidence; Account authenticates and encrypts the EIP-191 master\n * signature to the enclave key; Gateway relays it blind; Agent decrypts/seals.\n *\n * @category Protocol\n */\n\nimport * as secp256k1 from \"@noble/secp256k1\";\nimport {\n concat,\n encodePacked,\n fromHex,\n getAddress,\n isAddressEqual,\n keccak256,\n recoverPublicKey,\n toBytes,\n toHex,\n type Address,\n type Hex,\n} from \"viem\";\nimport { publicKeyToAddress } from \"viem/accounts\";\nimport { serializeECIES, type ECIESProvider } from \"../crypto/ecies/interface\";\nimport { deriveMasterKey, recoverServerOwner } from \"../crypto/keys/derive\";\nimport type { ServerRegistrationMessage } from \"./eip712\";\n\nexport const ENCLAVE_IDENTITY_EVIDENCE_VERSION = 1;\nexport const USER_PS_ID_DOMAIN = \"vana.ps-enclave.v1\";\nexport const ENCLAVE_WALLET_PURPOSE = \"vana.ps-enclave.wallet.v1\";\nexport const MASTER_SIGNATURE_DELIVERY_VERSION = \"vana.ps-enclave.delivery.v1\";\n/** Agent rejects deliveries when `|now - issuedAt|` exceeds this value. */\nexport const MASTER_SIGNATURE_DELIVERY_MAX_AGE_SECONDS = 600;\nexport const SEALED_ENVELOPE_VERSION = 1;\n\nconst VANA_MAINNET_CHAIN_ID = 1480;\nconst MOKSHA_CHAIN_ID = 14800;\nconst KMS_ISSUED_PREFIX = \"dstack-kms-issued\";\nconst PREIMAGE_SEPARATOR = \":\";\nconst UNCOMPRESSED_PUBLIC_KEY_BYTES = 65;\nconst UNCOMPRESSED_PUBLIC_KEY_PREFIX = \"04\";\nconst EMPTY_HEX = \"0x\";\n\n/** `keccak256(encodePacked([string,uint256,address], [USER_PS_ID_DOMAIN,chainId,owner]))`. */\nexport type UserPsId = Hex;\n\n/** Attested identity evidence returned by the enclave agent. */\nexport interface EnclaveIdentityEvidence {\n v: typeof ENCLAVE_IDENTITY_EVIDENCE_VERSION;\n userPsId: UserPsId;\n chainId: number;\n ownerAddress: Address;\n /** Path suffix `users/{id}/wallet/ethereum/secp256k1/v{epoch}`. */\n epoch: number;\n address: Address;\n /** 65-byte uncompressed `0x04..`; `publicKeyToAddress(publicKey) == address`. */\n publicKey: Hex;\n /** 20-byte dstack `app_id`. */\n appId: Hex;\n /** 32-byte compose hash. */\n composeHash: Hex;\n /** Omitted when the OS does not expose its image hash. */\n osImageHash?: Hex;\n /** Must equal `ENCLAVE_WALLET_PURPOSE`. */\n purpose: string;\n /** `[appRoot over link 0, kmsRoot over link 1]`; see `appRootPreimage` and `kmsIssuedPreimage`. */\n signatureChain: [Hex, Hex];\n /** Raw TDX quote with report_data `keccak256(userPsId || address)`; not parsed. */\n quote: Hex;\n eventLog?: string;\n /** `keccak256` of the uncompressed KMS root public key. */\n kmsRootFingerprint: Hex;\n}\n\n/** Identity values the caller expects the evidence to bind. */\nexport interface ExpectedIdentity {\n ownerAddress: Address;\n chainId: number;\n epoch: number;\n}\n\n/** Request for an owner-scoped enclave identity. */\nexport interface IdentityRequest {\n ownerAddress: Address;\n chainId: number;\n}\n\n/** Lifecycle state of an enclave identity. */\nexport type IdentityState = \"prepared\" | \"registered\" | \"sealed\" | \"retired\";\n\n/** Gateway response containing enclave identity state and registration data. */\nexport interface IdentityResponse {\n identity: EnclaveIdentityEvidence;\n state: IdentityState;\n created: boolean;\n /** Exact `ServerRegistration.serverUrl` the owner signs. */\n serverUrl: string;\n serverId?: Hex;\n serverStatus?:\n | \"pending\"\n | \"submitting\"\n | \"confirmed\"\n | \"finalized\"\n | \"failed\";\n sealed: boolean;\n}\n\n/** Versioned server-registration request for an enclave identity. */\nexport type IdentityRegistrationRequest =\n | { version: \"v2\"; message: ServerRegistrationMessage }\n | {\n version: \"v3\";\n message: ServerRegistrationMessage & { nonce: string; deadline: string };\n };\n\n/** Accepted identity-registration response. */\nexport interface IdentityRegistrationResponse {\n serverId: Hex;\n state: \"registered\";\n serverStatus: \"pending\";\n}\n\n/** Inner plaintext of the ECIES box. */\nexport interface MasterSignatureDelivery {\n v: typeof MASTER_SIGNATURE_DELIVERY_VERSION;\n userPsId: UserPsId;\n epoch: number;\n enclaveAddress: Address;\n ownerAddress: Address;\n /** 65-byte EIP-191 signature over `MASTER_KEY_MESSAGE`. */\n masterSignature: Hex;\n /** Unix seconds; see `MASTER_SIGNATURE_DELIVERY_MAX_AGE_SECONDS`. */\n issuedAt: number;\n}\n\n/** Enclave-bound ciphertext submitted for sealing. */\nexport interface SealedSecretSubmission {\n userPsId: UserPsId;\n epoch: number;\n enclaveAddress: Address;\n /** `iv(16) || ephemPub(65) || ct || mac(32)` to `evidence.publicKey`. */\n ciphertext: Hex;\n}\n\n/** Confirmation that the enclave sealed a submitted secret. */\nexport interface SealedSecretResponse {\n sealed: true;\n /** `sha256(ciphertext)`. */\n secretHash: Hex;\n sealedAt: string;\n}\n\n/** AES-GCM fields encoded as base64 strings. */\nexport interface AesGcmBox {\n /** Base64 initialization vector. */\n iv: string;\n /** Base64 ciphertext. */\n ciphertext: string;\n /** Base64 authentication tag. */\n tag: string;\n}\n\n/** Persisted enclave envelope; the Gateway treats it as opaque text. */\nexport interface SealedEnvelope extends AesGcmBox {\n v: typeof SEALED_ENVELOPE_VERSION;\n wrappedContentKey: AesGcmBox;\n}\n\n/** Fleet-pinned KMS root and allowed dstack application IDs. */\nexport interface EnclaveTrustAnchors {\n kmsRootPubkey: Hex;\n appIds: readonly Hex[];\n}\n\nfunction emptyAnchor(): Readonly<EnclaveTrustAnchors> {\n return Object.freeze({\n kmsRootPubkey: EMPTY_HEX,\n appIds: Object.freeze([] as Hex[]),\n });\n}\n\n/** Fleet-provisioned trust anchors keyed by Vana chain ID. */\nexport const ENCLAVE_TRUST_ANCHORS: Readonly<\n Record<number, Readonly<EnclaveTrustAnchors>>\n> = Object.freeze({\n // filled at fleet provisioning; verify fails closed while empty\n [VANA_MAINNET_CHAIN_ID]: emptyAnchor(),\n // Moksha workers share this dstack app and KMS root; the controller does not derive owner keys.\n [MOKSHA_CHAIN_ID]: Object.freeze({\n kmsRootPubkey:\n \"0x0434c76e0c3f52ec64cbf9bbf5c910c272330166fd656c0a86bb330963e46910e1a0e6fa51bec74be2f9129342707636060d85856d102cee8290185409ecf7422f\",\n appIds: Object.freeze([\n \"0xec9a39de98c760e1ded9f1e97016dc5f0e357cf2\" as Hex,\n ]),\n }),\n});\n\n/**\n * Derives the deterministic Personal Server ID used by enclave paths.\n *\n * @param chainId - Vana chain ID.\n * @param ownerAddress - Checksummed owner address.\n * @returns The path-compatible keccak256 digest.\n */\nexport function userPsId(chainId: number, ownerAddress: Address): UserPsId {\n const packed = encodePacked(\n [\"string\", \"uint256\", \"address\"],\n [USER_PS_ID_DOMAIN, BigInt(chainId), getAddress(ownerAddress)],\n );\n\n return keccak256(packed);\n}\n\nfunction compressPublicKey(publicKey: Hex): Uint8Array {\n return secp256k1.ProjectivePoint.fromHex(\n fromHex(publicKey, \"bytes\"),\n ).toRawBytes(true);\n}\n\nfunction sameKey(a: Hex, b: Hex): boolean {\n return toHex(compressPublicKey(a)) === toHex(compressPublicKey(b));\n}\n\nfunction assertUncompressedKey(publicKey: Hex): void {\n const hex = publicKey.slice(2);\n if (\n hex.length !== UNCOMPRESSED_PUBLIC_KEY_BYTES * 2 ||\n !hex.startsWith(UNCOMPRESSED_PUBLIC_KEY_PREFIX) ||\n !/^[0-9a-fA-F]+$/.test(hex)\n ) {\n throw new Error(\"Public key must be a 65-byte uncompressed secp256k1 key\");\n }\n}\n\n/**\n * Builds keccak256(utf8(purpose || \":\" || lowercase hex(compressed pubkey))).\n *\n * Matches `dstack/guest-agent/src/rpc_service.rs:612-628`.\n */\nexport function appRootPreimage(purpose: string, publicKey: Hex): Hex {\n const keyHex = toHex(compressPublicKey(publicKey)).slice(2);\n return keccak256(toBytes(`${purpose}${PREIMAGE_SEPARATOR}${keyHex}`));\n}\n\n/**\n * Hashes the KMS prefix, raw app ID bytes, and compressed app-root public key.\n *\n * Matches `dstack/kms/src/crypto.rs:23-40`; there is no separator between the\n * raw 20-byte app ID and the compressed key.\n */\nexport function kmsIssuedPreimage(appId: Hex, appRootPublicKey: Hex): Hex {\n const prefix = concat([\n toBytes(`${KMS_ISSUED_PREFIX}${PREIMAGE_SEPARATOR}`),\n fromHex(appId, \"bytes\"),\n ]);\n const compressed = compressPublicKey(appRootPublicKey);\n\n return keccak256(concat([prefix, compressed]));\n}\n\nasync function recoverChainKey(\n hash: Hex,\n signature: Hex,\n link: number,\n): Promise<Hex> {\n try {\n return (await recoverPublicKey({ hash, signature })).toLowerCase() as Hex;\n } catch {\n throw new Error(`Invalid enclave signature chain link ${link}`);\n }\n}\n\n/**\n * Verifies the enclave wallet binding against fleet-provisioned trust anchors.\n *\n * @param evidence - Identity evidence returned by the enclave agent.\n * @param anchors - Pinned KMS root and allowed dstack app IDs.\n * @param expected - Owner, chain, and epoch requested by the caller.\n *\n * @remarks\n * The dstack chain signs only `purpose || publicKey` and `appId || appRootPub`;\n * owner, chain and epoch are bound to the key by the TDX quote report_data\n * (`keccak256(userPsId || address)`), which v1 stores but does not verify\n * (DCAP is step 4). Until then the agent's derivation check is the backstop.\n * @throws When any identity binding or trust check fails.\n */\nexport async function verifyEnclaveIdentityEvidence(\n evidence: EnclaveIdentityEvidence,\n anchors: EnclaveTrustAnchors,\n expected: ExpectedIdentity,\n): Promise<void> {\n if (evidence.v !== ENCLAVE_IDENTITY_EVIDENCE_VERSION) {\n throw new Error(\"Unsupported enclave identity evidence version\");\n }\n\n if (!Number.isInteger(evidence.epoch) || evidence.epoch < 1) {\n throw new Error(\"Invalid enclave identity epoch\");\n }\n\n if (evidence.chainId !== expected.chainId) {\n throw new Error(\n \"Enclave identity chain ID does not match expected chain ID\",\n );\n }\n\n if (!isAddressEqual(evidence.ownerAddress, expected.ownerAddress)) {\n throw new Error(\"Enclave identity owner does not match expected owner\");\n }\n\n if (evidence.epoch !== expected.epoch) {\n throw new Error(\"Enclave identity epoch does not match expected epoch\");\n }\n\n const expectedUserPsId = userPsId(expected.chainId, expected.ownerAddress);\n if (evidence.userPsId.toLowerCase() !== expectedUserPsId.toLowerCase()) {\n throw new Error(\"Enclave userPsId does not match expected identity\");\n }\n\n if (evidence.purpose !== ENCLAVE_WALLET_PURPOSE) {\n throw new Error(\"Unexpected enclave wallet purpose\");\n }\n\n if (anchors.kmsRootPubkey === EMPTY_HEX) {\n throw new Error(\"KMS root trust anchor is not provisioned\");\n }\n\n assertUncompressedKey(evidence.publicKey);\n const derivedAddress = publicKeyToAddress(evidence.publicKey);\n\n if (getAddress(derivedAddress) !== getAddress(evidence.address)) {\n throw new Error(\"Enclave public key does not match its address\");\n }\n\n const appRootPublicKey = await recoverChainKey(\n appRootPreimage(evidence.purpose, evidence.publicKey),\n evidence.signatureChain[0],\n 0,\n );\n const kmsRootPublicKey = await recoverChainKey(\n kmsIssuedPreimage(evidence.appId, appRootPublicKey),\n evidence.signatureChain[1],\n 1,\n );\n\n let matchesAnchor: boolean;\n try {\n matchesAnchor = sameKey(kmsRootPublicKey, anchors.kmsRootPubkey);\n } catch {\n throw new Error(\"KMS root trust anchor is malformed\");\n }\n\n if (!matchesAnchor) {\n throw new Error(\"KMS root public key does not match the trust anchor\");\n }\n\n if (\n keccak256(kmsRootPublicKey) !== evidence.kmsRootFingerprint.toLowerCase()\n ) {\n throw new Error(\"KMS root fingerprint does not match the evidence\");\n }\n\n const appId = evidence.appId.toLowerCase();\n if (\n !anchors.appIds.some((allowedAppId) => allowedAppId.toLowerCase() === appId)\n ) {\n throw new Error(\"Enclave app ID is not trusted\");\n }\n}\n\n/**\n * Builds the authenticated master-signature plaintext delivered to an enclave.\n *\n * @param evidence - Verified enclave identity evidence.\n * @param masterSignature - EIP-191 signature over the master-key message.\n * @param now - Issuance time in Unix seconds.\n * @returns The authenticated plaintext delivery object.\n * @throws When the signature is malformed or does not belong to the owner.\n */\nexport async function buildMasterSignatureDelivery(\n evidence: EnclaveIdentityEvidence,\n masterSignature: Hex,\n now = Math.floor(Date.now() / 1000),\n): Promise<MasterSignatureDelivery> {\n // Validate signature length and hex encoding before recovery.\n deriveMasterKey(masterSignature);\n const signerAddress = await recoverServerOwner(masterSignature);\n\n if (!isAddressEqual(signerAddress, evidence.ownerAddress)) {\n throw new Error(\"Master signature signer does not match evidence owner\");\n }\n\n return {\n v: MASTER_SIGNATURE_DELIVERY_VERSION,\n userPsId: evidence.userPsId,\n epoch: evidence.epoch,\n enclaveAddress: evidence.address,\n ownerAddress: evidence.ownerAddress,\n masterSignature,\n issuedAt: now,\n };\n}\n\n/**\n * Encrypts a master-signature delivery in the SDK's ECIES wire format.\n *\n * @param delivery - Plaintext delivery object.\n * @param publicKey - Enclave 65-byte uncompressed public key.\n * @param ecies - Platform-specific ECIES provider.\n * @returns The 0x-prefixed serialized ciphertext.\n * @throws When the public key is malformed or belongs to another enclave.\n */\nexport async function encryptMasterSignatureDelivery(\n delivery: MasterSignatureDelivery,\n publicKey: Hex,\n ecies: ECIESProvider,\n): Promise<Hex> {\n assertUncompressedKey(publicKey);\n\n if (!isAddressEqual(publicKeyToAddress(publicKey), delivery.enclaveAddress)) {\n throw new Error(\n \"Public key does not belong to the delivery's enclave address\",\n );\n }\n\n const plaintext = toBytes(JSON.stringify(delivery));\n const encrypted = await ecies.encrypt(fromHex(publicKey, \"bytes\"), plaintext);\n\n return `0x${serializeECIES(encrypted)}`;\n}\n"],"mappings":"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","names":[]}
|