@opendatalabs/vana-sdk 4.0.0 → 4.2.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.
@@ -61,15 +61,14 @@ const PREIMAGE_SEPARATOR = ":";
61
61
  const UNCOMPRESSED_PUBLIC_KEY_BYTES = 65;
62
62
  const UNCOMPRESSED_PUBLIC_KEY_PREFIX = "04";
63
63
  const EMPTY_HEX = "0x";
64
- function emptyAnchor() {
65
- return Object.freeze({
66
- kmsRootPubkey: EMPTY_HEX,
67
- appIds: Object.freeze([])
68
- });
69
- }
64
+ const MAINNET_ANCHOR = Object.freeze({
65
+ kmsRootPubkey: "0x0434c76e0c3f52ec64cbf9bbf5c910c272330166fd656c0a86bb330963e46910e1a0e6fa51bec74be2f9129342707636060d85856d102cee8290185409ecf7422f",
66
+ appIds: Object.freeze([
67
+ "0x01bb1b6dcaf1ea170f1480c5e53b093f384d939a"
68
+ ])
69
+ });
70
70
  const ENCLAVE_TRUST_ANCHORS = Object.freeze({
71
- // filled at fleet provisioning; verify fails closed while empty
72
- [VANA_MAINNET_CHAIN_ID]: emptyAnchor(),
71
+ [VANA_MAINNET_CHAIN_ID]: MAINNET_ANCHOR,
73
72
  // Moksha workers share this dstack app and KMS root; the controller does not derive owner keys.
74
73
  [MOKSHA_CHAIN_ID]: Object.freeze({
75
74
  kmsRootPubkey: "0x0434c76e0c3f52ec64cbf9bbf5c910c272330166fd656c0a86bb330963e46910e1a0e6fa51bec74be2f9129342707636060d85856d102cee8290185409ecf7422f",
@@ -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 // 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":[]}
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\n/**\n * Mainnet fleet anchors, harvested from the prod9 fleet on 2026-09-15.\n *\n * `kmsRootPubkey` is byte-identical to Moksha's: both fleets run under the same\n * Phala KMS, so the key-provider root does NOT separate them. `appIds` is what\n * does — it lists the mainnet worker app only, and must never contain a Moksha\n * app id. The controller is absent by design: it derives no owner keys.\n */\nconst MAINNET_ANCHOR: Readonly<EnclaveTrustAnchors> = Object.freeze({\n kmsRootPubkey:\n \"0x0434c76e0c3f52ec64cbf9bbf5c910c272330166fd656c0a86bb330963e46910e1a0e6fa51bec74be2f9129342707636060d85856d102cee8290185409ecf7422f\",\n appIds: Object.freeze([\n \"0x01bb1b6dcaf1ea170f1480c5e53b093f384d939a\",\n ] as Hex[]),\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 [VANA_MAINNET_CHAIN_ID]: MAINNET_ANCHOR,\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":[]}
@@ -26,15 +26,14 @@ const PREIMAGE_SEPARATOR = ":";
26
26
  const UNCOMPRESSED_PUBLIC_KEY_BYTES = 65;
27
27
  const UNCOMPRESSED_PUBLIC_KEY_PREFIX = "04";
28
28
  const EMPTY_HEX = "0x";
29
- function emptyAnchor() {
30
- return Object.freeze({
31
- kmsRootPubkey: EMPTY_HEX,
32
- appIds: Object.freeze([])
33
- });
34
- }
29
+ const MAINNET_ANCHOR = Object.freeze({
30
+ kmsRootPubkey: "0x0434c76e0c3f52ec64cbf9bbf5c910c272330166fd656c0a86bb330963e46910e1a0e6fa51bec74be2f9129342707636060d85856d102cee8290185409ecf7422f",
31
+ appIds: Object.freeze([
32
+ "0x01bb1b6dcaf1ea170f1480c5e53b093f384d939a"
33
+ ])
34
+ });
35
35
  const ENCLAVE_TRUST_ANCHORS = Object.freeze({
36
- // filled at fleet provisioning; verify fails closed while empty
37
- [VANA_MAINNET_CHAIN_ID]: emptyAnchor(),
36
+ [VANA_MAINNET_CHAIN_ID]: MAINNET_ANCHOR,
38
37
  // Moksha workers share this dstack app and KMS root; the controller does not derive owner keys.
39
38
  [MOKSHA_CHAIN_ID]: Object.freeze({
40
39
  kmsRootPubkey: "0x0434c76e0c3f52ec64cbf9bbf5c910c272330166fd656c0a86bb330963e46910e1a0e6fa51bec74be2f9129342707636060d85856d102cee8290185409ecf7422f",
@@ -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 // 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":[]}
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\n/**\n * Mainnet fleet anchors, harvested from the prod9 fleet on 2026-09-15.\n *\n * `kmsRootPubkey` is byte-identical to Moksha's: both fleets run under the same\n * Phala KMS, so the key-provider root does NOT separate them. `appIds` is what\n * does — it lists the mainnet worker app only, and must never contain a Moksha\n * app id. The controller is absent by design: it derives no owner keys.\n */\nconst MAINNET_ANCHOR: Readonly<EnclaveTrustAnchors> = Object.freeze({\n kmsRootPubkey:\n \"0x0434c76e0c3f52ec64cbf9bbf5c910c272330166fd656c0a86bb330963e46910e1a0e6fa51bec74be2f9129342707636060d85856d102cee8290185409ecf7422f\",\n appIds: Object.freeze([\n \"0x01bb1b6dcaf1ea170f1480c5e53b093f384d939a\",\n ] as Hex[]),\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 [VANA_MAINNET_CHAIN_ID]: MAINNET_ANCHOR,\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":[]}
@@ -29,12 +29,17 @@ var import_web3_signed_builder = require("../auth/web3-signed-builder");
29
29
  var import_node = require("../crypto/ecies/node");
30
30
  var import_job = require("../crypto/envelope/job");
31
31
  var import_errors = require("../errors.cjs");
32
+ var import_errors2 = require("../direct/errors");
33
+ var import_contracts = require("../config/contracts.config");
34
+ var import_escrow = require("./escrow");
32
35
  var import_identity = require("./identity");
33
36
  var import_jobs = require("./jobs");
34
37
  var import_write_signer = require("./write-signer");
35
38
  const IDENTITY_PATH = "/v1/identity";
36
39
  const JOBS_PATH = "/v1/jobs";
40
+ const JOB_QUOTE_PATH = "/v1/jobs/quote";
37
41
  const JOB_EXECUTE_PATH = "/v1/jobs/execute";
42
+ const JOB_ACCESS_OP_TYPE = "job_access";
38
43
  const AUTHORIZATION_HEADER = "Authorization";
39
44
  const CONTENT_TYPE_HEADER = "Content-Type";
40
45
  const JSON_CONTENT_TYPE = "application/json";
@@ -48,7 +53,11 @@ const HTTP_UNAUTHORIZED = 401;
48
53
  const HTTP_FORBIDDEN = 403;
49
54
  const HTTP_NOT_FOUND = 404;
50
55
  const HTTP_CONFLICT = 409;
56
+ const HTTP_PAYMENT_REQUIRED = 402;
51
57
  const HTTP_PAYLOAD_TOO_LARGE = 413;
58
+ const NONCE_TIMESTAMP_SHIFT = 32n;
59
+ const UINT256_DECIMAL = /^(0|[1-9]\d*)$/;
60
+ const POSITIVE_UINT256_DECIMAL = /^[1-9]\d*$/;
52
61
  const MILLISECONDS_PER_SECOND = 1e3;
53
62
  const DEFAULT_JOB_TIMEOUT_MS = 12e4;
54
63
  const DEFAULT_JOB_POLL_MS = import_jobs.CLAIM_POLL_FLOOR_MS;
@@ -131,11 +140,17 @@ function resolveBuilder(options) {
131
140
  "Builder signer must expose both address and publicKey"
132
141
  );
133
142
  }
143
+ const typedDataSigner = source.signTypedData;
134
144
  return {
135
145
  builder: {
136
146
  address: signer.address,
137
147
  publicKey,
138
- signMessage: signer.signMessage
148
+ signMessage: signer.signMessage,
149
+ ...typedDataSigner ? {
150
+ signTypedData: typedDataSigner.bind(
151
+ source
152
+ )
153
+ } : {}
139
154
  },
140
155
  ...options.builderPrivateKey ? { privateKey: options.builderPrivateKey } : {}
141
156
  };
@@ -240,6 +255,51 @@ function hasExpectedIdentity(value, owner, chainId) {
240
255
  function sleep(ms) {
241
256
  return new Promise((resolve) => setTimeout(resolve, ms));
242
257
  }
258
+ function resolveEscrowContract(options) {
259
+ if (options.escrowContract) return options.escrowContract;
260
+ const addresses = import_contracts.CONTRACTS["DataPortabilityEscrow"]?.addresses;
261
+ const address = addresses?.[options.chainId];
262
+ if (!address || !(0, import_viem.isAddress)(address)) {
263
+ throw new import_errors.JobRejectedError(
264
+ `No DataPortabilityEscrow address for chain ${options.chainId}; pass escrowContract`
265
+ );
266
+ }
267
+ return address;
268
+ }
269
+ function nextPaymentNonce() {
270
+ const random = new Uint32Array(1);
271
+ globalThis.crypto.getRandomValues(random);
272
+ return (BigInt(Date.now()) << NONCE_TIMESTAMP_SHIFT) + BigInt(random[0] ?? 0);
273
+ }
274
+ function parseJobQuote(body) {
275
+ if (!isRecord(body)) return null;
276
+ const { chainId, price, asset, payable, enforced } = body;
277
+ if (typeof chainId !== "number" || !Number.isSafeInteger(chainId))
278
+ return null;
279
+ if (typeof price !== "string" || !UINT256_DECIMAL.test(price)) return null;
280
+ if (typeof asset !== "string" || !(0, import_viem.isAddress)(asset)) return null;
281
+ if (typeof payable !== "boolean" || typeof enforced !== "boolean")
282
+ return null;
283
+ return { chainId, price, asset, payable, enforced };
284
+ }
285
+ async function paymentQuote(response) {
286
+ const body = await responseBody(response);
287
+ const asset = isRecord(body) ? body["asset"] : void 0;
288
+ const amount = isRecord(body) ? body["amount"] : void 0;
289
+ if (typeof asset !== "string" || !(0, import_viem.isAddress)(asset) || typeof amount !== "string" || !POSITIVE_UINT256_DECIMAL.test(amount)) {
290
+ throw new import_errors.JobRejectedError(
291
+ "Gateway asked for payment without a usable quote",
292
+ response.status
293
+ );
294
+ }
295
+ return { asset, amount };
296
+ }
297
+ async function paymentRefusal(response) {
298
+ const body = await responseBody(response);
299
+ const parsed = isRecord(body) ? body : {};
300
+ const message = typeof parsed["error"] === "string" ? parsed["error"] : "This read requires payment the builder escrow cannot back";
301
+ return { message, details: parsed };
302
+ }
243
303
  function requireFiniteDuration(value, field) {
244
304
  if (!Number.isFinite(value)) {
245
305
  throw new import_errors.JobRejectedError(`${field} must be a finite number`);
@@ -252,6 +312,73 @@ function createJobsClient(options) {
252
312
  const { builder, privateKey } = resolveBuilder(options);
253
313
  const ecies = options.ecies ?? new import_node.NodeECIESUint8Provider();
254
314
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
315
+ async function signQuote(jobId, quote) {
316
+ if (!builder.signTypedData) {
317
+ throw new import_errors.JobRejectedError(
318
+ "This read is charged, but the builder signer cannot sign typed data"
319
+ );
320
+ }
321
+ const paymentNonce = nextPaymentNonce();
322
+ const signature = await builder.signTypedData({
323
+ domain: (0, import_escrow.genericPaymentDomain)(
324
+ options.chainId,
325
+ resolveEscrowContract(options)
326
+ ),
327
+ types: import_escrow.GENERIC_PAYMENT_TYPES,
328
+ primaryType: "GenericPayment",
329
+ message: {
330
+ payerAddress: builder.address,
331
+ opType: JOB_ACCESS_OP_TYPE,
332
+ opId: (0, import_viem.keccak256)((0, import_viem.toHex)(jobId.toLowerCase())),
333
+ asset: quote.asset,
334
+ amount: BigInt(quote.amount),
335
+ paymentNonce
336
+ }
337
+ });
338
+ return {
339
+ signature,
340
+ amount: quote.amount,
341
+ asset: quote.asset,
342
+ paymentNonce: paymentNonce.toString()
343
+ };
344
+ }
345
+ async function postSubmission(submission, wait) {
346
+ const identifiers = {
347
+ jobId: submission.jobId,
348
+ idempotencyKey: submission.idempotencyKey
349
+ };
350
+ const bodyBytes = new TextEncoder().encode(JSON.stringify(submission));
351
+ const authorization = await (0, import_web3_signed_builder.buildWeb3SignedHeader)({
352
+ signMessage: builder.signMessage,
353
+ aud: audience,
354
+ method: POST_METHOD,
355
+ uri: JOBS_PATH,
356
+ body: bodyBytes,
357
+ nonce: (0, import_node_crypto.randomUUID)()
358
+ });
359
+ try {
360
+ return await request(
361
+ `${baseUrl}${JOBS_PATH}?wait=${wait}`,
362
+ {
363
+ method: POST_METHOD,
364
+ headers: {
365
+ [AUTHORIZATION_HEADER]: authorization,
366
+ [CONTENT_TYPE_HEADER]: JSON_CONTENT_TYPE
367
+ },
368
+ body: bodyBytes
369
+ },
370
+ "Job submission"
371
+ );
372
+ } catch (error) {
373
+ if (error instanceof import_errors.JobTransportError) {
374
+ throw new import_errors.JobTransportError(error.message, error.cause, {
375
+ ...error.details,
376
+ ...identifiers
377
+ });
378
+ }
379
+ throw error;
380
+ }
381
+ }
255
382
  async function request(url, init, description) {
256
383
  try {
257
384
  return await fetchFn(url, init);
@@ -307,7 +434,8 @@ function createJobsClient(options) {
307
434
  "deadlineSeconds"
308
435
  );
309
436
  const wait = clampInteger(params.wait, 0, 0, import_jobs.MAX_WAIT_SECONDS, "wait");
310
- const jobId = (0, import_node_crypto.randomUUID)();
437
+ const jobId = params.jobId ?? (0, import_node_crypto.randomUUID)();
438
+ const idempotencyKey = params.idempotencyKey ?? (0, import_node_crypto.randomUUID)();
311
439
  const nowMs = now().getTime();
312
440
  if (!Number.isFinite(nowMs)) {
313
441
  throw new import_errors.JobRejectedError("now must return a valid Date");
@@ -345,32 +473,41 @@ function createJobsClient(options) {
345
473
  grantId: requestBody.grantId,
346
474
  scope: requestBody.scope,
347
475
  operation: requestBody.operation,
348
- idempotencyKey: (0, import_node_crypto.randomUUID)(),
476
+ idempotencyKey,
349
477
  jobId,
350
478
  deadline,
351
- requestCiphertext
479
+ requestCiphertext,
480
+ ...params.maxPrice === void 0 ? {} : { maxPrice: params.maxPrice }
352
481
  };
353
- const bodyBytes = new TextEncoder().encode(JSON.stringify(submission));
354
- const authorization = await (0, import_web3_signed_builder.buildWeb3SignedHeader)({
355
- signMessage: builder.signMessage,
356
- aud: audience,
357
- method: POST_METHOD,
358
- uri: JOBS_PATH,
359
- body: bodyBytes,
360
- nonce: (0, import_node_crypto.randomUUID)()
361
- });
362
- const response = await request(
363
- `${baseUrl}${JOBS_PATH}?wait=${wait}`,
364
- {
365
- method: POST_METHOD,
366
- headers: {
367
- [AUTHORIZATION_HEADER]: authorization,
368
- [CONTENT_TYPE_HEADER]: JSON_CONTENT_TYPE
369
- },
370
- body: bodyBytes
371
- },
372
- "Job submission"
373
- );
482
+ let response = await postSubmission(submission, wait);
483
+ if (response.status === HTTP_PAYMENT_REQUIRED) {
484
+ const quote = await paymentQuote(response);
485
+ if (params.maxPrice !== void 0 && BigInt(quote.amount) > BigInt(params.maxPrice)) {
486
+ throw new import_errors2.PaymentRequiredError(
487
+ "The quoted price is above the maxPrice you set for this read",
488
+ {
489
+ jobId,
490
+ scope: params.scope,
491
+ grantId: params.grantId,
492
+ asset: quote.asset,
493
+ amount: quote.amount,
494
+ maxPrice: params.maxPrice
495
+ }
496
+ );
497
+ }
498
+ submission.payment = await signQuote(jobId, quote);
499
+ submission.priceAsset = quote.asset;
500
+ response = await postSubmission(submission, wait);
501
+ if (response.status === HTTP_PAYMENT_REQUIRED) {
502
+ const refusal = await paymentRefusal(response);
503
+ throw new import_errors2.PaymentRequiredError(refusal.message, {
504
+ jobId,
505
+ scope: params.scope,
506
+ grantId: params.grantId,
507
+ ...refusal.details
508
+ });
509
+ }
510
+ }
374
511
  if (!response.ok) {
375
512
  throw await rejectedResponse(response, "Job submission rejected");
376
513
  }
@@ -393,6 +530,40 @@ function createJobsClient(options) {
393
530
  response.status
394
531
  );
395
532
  },
533
+ async quoteJob() {
534
+ const authorization = await (0, import_web3_signed_builder.buildWeb3SignedHeader)({
535
+ signMessage: builder.signMessage,
536
+ aud: audience,
537
+ method: GET_METHOD,
538
+ uri: JOB_QUOTE_PATH,
539
+ nonce: (0, import_node_crypto.randomUUID)()
540
+ });
541
+ const response = await request(
542
+ `${baseUrl}${JOB_QUOTE_PATH}`,
543
+ {
544
+ method: GET_METHOD,
545
+ headers: { [AUTHORIZATION_HEADER]: authorization }
546
+ },
547
+ "Job quote"
548
+ );
549
+ if (!response.ok) {
550
+ throw await rejectedResponse(response, "Job quote rejected");
551
+ }
552
+ const quote = parseJobQuote(await responseBody(response));
553
+ if (!quote) {
554
+ throw new import_errors.JobRejectedError(
555
+ "Gateway returned an undocumented job quote",
556
+ response.status
557
+ );
558
+ }
559
+ if (quote.chainId !== options.chainId) {
560
+ throw new import_errors.JobRejectedError(
561
+ `Gateway quoted chain ${quote.chainId}, not the configured ${options.chainId}`,
562
+ response.status
563
+ );
564
+ }
565
+ return quote;
566
+ },
396
567
  async getJob(jobId) {
397
568
  const path = `${JOBS_PATH}/${encodeURIComponent(jobId)}`;
398
569
  const authorization = await (0, import_web3_signed_builder.buildWeb3SignedHeader)({