@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.
- package/dist/errors.cjs +2 -2
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.d.ts +7 -1
- package/dist/errors.js +2 -2
- package/dist/errors.js.map +1 -1
- package/dist/index.browser.d.ts +1 -1
- package/dist/index.browser.js +7 -8
- package/dist/index.browser.js.map +2 -2
- package/dist/index.node.cjs +865 -442
- package/dist/index.node.cjs.map +4 -4
- package/dist/index.node.d.ts +1 -1
- package/dist/index.node.js +919 -494
- package/dist/index.node.js.map +4 -4
- package/dist/protocol/identity.cjs +7 -8
- package/dist/protocol/identity.cjs.map +1 -1
- package/dist/protocol/identity.js +7 -8
- package/dist/protocol/identity.js.map +1 -1
- package/dist/protocol/jobs-client.cjs +196 -25
- package/dist/protocol/jobs-client.cjs.map +1 -1
- package/dist/protocol/jobs-client.d.ts +39 -1
- package/dist/protocol/jobs-client.js +199 -26
- package/dist/protocol/jobs-client.js.map +1 -1
- package/dist/protocol/jobs.cjs.map +1 -1
- package/dist/protocol/jobs.d.ts +71 -1
- package/dist/protocol/jobs.js.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
|
476
|
+
idempotencyKey,
|
|
349
477
|
jobId,
|
|
350
478
|
deadline,
|
|
351
|
-
requestCiphertext
|
|
479
|
+
requestCiphertext,
|
|
480
|
+
...params.maxPrice === void 0 ? {} : { maxPrice: params.maxPrice }
|
|
352
481
|
};
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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)({
|