@agirails/sdk 4.4.8 → 4.5.2
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/builders/DeliveryProofBuilder.d.ts +224 -13
- package/dist/builders/DeliveryProofBuilder.d.ts.map +1 -1
- package/dist/builders/DeliveryProofBuilder.js +247 -13
- package/dist/builders/DeliveryProofBuilder.js.map +1 -1
- package/dist/cli/agirails.d.ts +85 -1
- package/dist/cli/agirails.d.ts.map +1 -1
- package/dist/cli/agirails.js +429 -154
- package/dist/cli/agirails.js.map +1 -1
- package/dist/cli/commands/init.d.ts +54 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +193 -1
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/receipt.d.ts +70 -2
- package/dist/cli/commands/receipt.d.ts.map +1 -1
- package/dist/cli/commands/receipt.js +218 -3
- package/dist/cli/commands/receipt.js.map +1 -1
- package/dist/cli/commands/test.d.ts +77 -1
- package/dist/cli/commands/test.d.ts.map +1 -1
- package/dist/cli/commands/test.js +264 -2
- package/dist/cli/commands/test.js.map +1 -1
- package/dist/cli/lib/runRequest.d.ts +90 -0
- package/dist/cli/lib/runRequest.d.ts.map +1 -1
- package/dist/cli/lib/runRequest.js +300 -9
- package/dist/cli/lib/runRequest.js.map +1 -1
- package/dist/cli/lib/sentinelReflections.d.ts +111 -0
- package/dist/cli/lib/sentinelReflections.d.ts.map +1 -0
- package/dist/cli/lib/sentinelReflections.js +193 -0
- package/dist/cli/lib/sentinelReflections.js.map +1 -0
- package/dist/delivery/MockDeliveryChannel.d.ts +208 -0
- package/dist/delivery/MockDeliveryChannel.d.ts.map +1 -0
- package/dist/delivery/MockDeliveryChannel.js +445 -0
- package/dist/delivery/MockDeliveryChannel.js.map +1 -0
- package/dist/delivery/RelayDeliveryChannel.d.ts +176 -0
- package/dist/delivery/RelayDeliveryChannel.d.ts.map +1 -0
- package/dist/delivery/RelayDeliveryChannel.js +377 -0
- package/dist/delivery/RelayDeliveryChannel.js.map +1 -0
- package/dist/delivery/channel.d.ts +282 -0
- package/dist/delivery/channel.d.ts.map +1 -0
- package/dist/delivery/channel.js +76 -0
- package/dist/delivery/channel.js.map +1 -0
- package/dist/delivery/channelLog.d.ts +115 -0
- package/dist/delivery/channelLog.d.ts.map +1 -0
- package/dist/delivery/channelLog.js +94 -0
- package/dist/delivery/channelLog.js.map +1 -0
- package/dist/delivery/crypto.d.ts +312 -0
- package/dist/delivery/crypto.d.ts.map +1 -0
- package/dist/delivery/crypto.js +495 -0
- package/dist/delivery/crypto.js.map +1 -0
- package/dist/delivery/eip712.d.ts +248 -0
- package/dist/delivery/eip712.d.ts.map +1 -0
- package/dist/delivery/eip712.js +397 -0
- package/dist/delivery/eip712.js.map +1 -0
- package/dist/delivery/envelopeBuilder.d.ts +531 -0
- package/dist/delivery/envelopeBuilder.d.ts.map +1 -0
- package/dist/delivery/envelopeBuilder.js +832 -0
- package/dist/delivery/envelopeBuilder.js.map +1 -0
- package/dist/delivery/index.d.ts +53 -0
- package/dist/delivery/index.d.ts.map +1 -0
- package/dist/delivery/index.js +143 -0
- package/dist/delivery/index.js.map +1 -0
- package/dist/delivery/keys.d.ts +344 -0
- package/dist/delivery/keys.d.ts.map +1 -0
- package/dist/delivery/keys.js +513 -0
- package/dist/delivery/keys.js.map +1 -0
- package/dist/delivery/nonce-keys.d.ts +93 -0
- package/dist/delivery/nonce-keys.d.ts.map +1 -0
- package/dist/delivery/nonce-keys.js +88 -0
- package/dist/delivery/nonce-keys.js.map +1 -0
- package/dist/delivery/setupBuilder.d.ts +403 -0
- package/dist/delivery/setupBuilder.d.ts.map +1 -0
- package/dist/delivery/setupBuilder.js +554 -0
- package/dist/delivery/setupBuilder.js.map +1 -0
- package/dist/delivery/types.d.ts +722 -0
- package/dist/delivery/types.d.ts.map +1 -0
- package/dist/delivery/types.js +150 -0
- package/dist/delivery/types.js.map +1 -0
- package/dist/delivery/validate.d.ts +288 -0
- package/dist/delivery/validate.d.ts.map +1 -0
- package/dist/delivery/validate.js +648 -0
- package/dist/delivery/validate.js.map +1 -0
- package/dist/level1/Agent.d.ts +130 -0
- package/dist/level1/Agent.d.ts.map +1 -1
- package/dist/level1/Agent.js +248 -0
- package/dist/level1/Agent.js.map +1 -1
- package/dist/level1/types/Options.d.ts +62 -0
- package/dist/level1/types/Options.d.ts.map +1 -1
- package/dist/level1/types/Options.js +22 -0
- package/dist/level1/types/Options.js.map +1 -1
- package/dist/runtime/MockRuntime.d.ts +32 -0
- package/dist/runtime/MockRuntime.d.ts.map +1 -1
- package/dist/runtime/MockRuntime.js +44 -0
- package/dist/runtime/MockRuntime.js.map +1 -1
- package/dist/wallet/aa/BundlerClient.d.ts.map +1 -1
- package/dist/wallet/aa/BundlerClient.js +18 -3
- package/dist/wallet/aa/BundlerClient.js.map +1 -1
- package/dist/wallet/aa/PaymasterClient.d.ts.map +1 -1
- package/dist/wallet/aa/PaymasterClient.js +4 -1
- package/dist/wallet/aa/PaymasterClient.js.map +1 -1
- package/package.json +6 -1
|
@@ -0,0 +1,832 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* AIP-16 Delivery Surface — Provider Envelope Builder + Verifier + Decryptor (Phase 2b)
|
|
4
|
+
* =======================================================================================
|
|
5
|
+
*
|
|
6
|
+
* Constructs and verifies the provider-signed `DeliveryEnvelopeV1` payload —
|
|
7
|
+
* the load-bearing artifact of the AIP-16 Rev 5 delivery surface. The
|
|
8
|
+
* provider posts this signed object to the delivery channel after the
|
|
9
|
+
* buyer's setup is received (and after the on-chain transaction reaches
|
|
10
|
+
* `COMMITTED`), carrying:
|
|
11
|
+
*
|
|
12
|
+
* - the EIP-712 binding to a specific kernel, chain, and `txId`,
|
|
13
|
+
* - the on-chain identity acting as the provider, plus the EOA that
|
|
14
|
+
* signed (smart-wallet two-step auth),
|
|
15
|
+
* - the cryptographic scheme used to protect the body (`public-v1` or
|
|
16
|
+
* `x25519-aes256gcm-v1`),
|
|
17
|
+
* - the provider's ephemeral X25519 pubkey (encrypted scheme) or
|
|
18
|
+
* canonical-empty bytes32 (public scheme),
|
|
19
|
+
* - the AES-256-GCM nonce + tag (encrypted) or canonical-empty
|
|
20
|
+
* bytes12 / bytes16 (public),
|
|
21
|
+
* - `payloadHash = keccak256(bodyBytes)` — the on-chain anchor for the
|
|
22
|
+
* exact bytes the buyer will receive,
|
|
23
|
+
* - the body bytes themselves (alongside the signed projection, in the
|
|
24
|
+
* wire envelope).
|
|
25
|
+
*
|
|
26
|
+
* ## Body encoding (SCHEME-AWARE — FIX-1, AIP-16 Phase 3.5)
|
|
27
|
+
*
|
|
28
|
+
* The wire encoding is scheme-dependent so the SDK and the Platform
|
|
29
|
+
* verifier (`Platform/agirails.app/web/lib/delivery/auth.ts`) hash the
|
|
30
|
+
* SAME bytes in BOTH locations:
|
|
31
|
+
*
|
|
32
|
+
* - `public-v1`: `wire.body = JSON.stringify(payload)` — plaintext
|
|
33
|
+
* UTF-8 JSON string, NOT hex. `payloadHash = keccak256(utf8Bytes(body))`.
|
|
34
|
+
* The Platform verifier computes `keccak256(toUtf8Bytes(body))`
|
|
35
|
+
* directly on the wire body. Hex-encoding the plaintext here would
|
|
36
|
+
* make the Platform recompute `keccak256(utf8Bytes("0x7b22…"))` —
|
|
37
|
+
* a different digest — and every public envelope would 400 with
|
|
38
|
+
* `payload_hash_mismatch`.
|
|
39
|
+
*
|
|
40
|
+
* - `x25519-aes256gcm-v1`: `wire.body = "0x" + hex(aesGcmCiphertext)`.
|
|
41
|
+
* `payloadHash = keccak256(rawCiphertextBytes)`. The Platform
|
|
42
|
+
* verifier hex-decodes `wire.body` and then keccak256s the bytes.
|
|
43
|
+
* Ciphertext is a byte string that does NOT round-trip through UTF-8
|
|
44
|
+
* (it contains arbitrary 0..255 bytes incl. 0x00 and high bits),
|
|
45
|
+
* so hex is the only safe text encoding — base64 would also work but
|
|
46
|
+
* AIP-16 standardizes on hex for cross-language SDK consistency.
|
|
47
|
+
*
|
|
48
|
+
* Rationale for the asymmetry:
|
|
49
|
+
* 1. **Public payloads are already text.** JSON serializes to valid
|
|
50
|
+
* UTF-8 — no encoding wrapper needed. The plaintext IS the wire.
|
|
51
|
+
* 2. **Ciphertext is binary.** Cannot travel naked in JSON; hex is the
|
|
52
|
+
* chosen text encoding (matches `nonce`, `tag`, `payloadHash`,
|
|
53
|
+
* `providerEphemeralPubkey`).
|
|
54
|
+
* 3. **Spec match.** AIP-16 §6.2 sign-side and the Platform verify-
|
|
55
|
+
* side both implement this exact rule.
|
|
56
|
+
*
|
|
57
|
+
* ## Builder shape (matches DeliverySetupBuilder)
|
|
58
|
+
*
|
|
59
|
+
* - `constructor(signer?)` — signer required for `build*()`, optional
|
|
60
|
+
* for `verify()` / `computeHash()` / `decryptPayload()`. The static
|
|
61
|
+
* helpers do not consult builder state.
|
|
62
|
+
* - `buildPublic(params)` / `buildEncrypted(params)` are `async`
|
|
63
|
+
* because EIP-712 signing on real wallets is async.
|
|
64
|
+
* - `verify`, `decryptPayload`, `verifyAndDecrypt`, `computeHash` are
|
|
65
|
+
* `static` — mirrors {@link DeliverySetupBuilder} and the existing
|
|
66
|
+
* delivery-proof / quote builders.
|
|
67
|
+
*
|
|
68
|
+
* ## Smart-wallet two-step auth (DEC-10 / V2 receipts)
|
|
69
|
+
*
|
|
70
|
+
* `providerAddress` (on-chain participant — possibly a Smart Wallet)
|
|
71
|
+
* and `signerAddress` (the EOA that actually signed) are accepted
|
|
72
|
+
* SEPARATELY. The SDK does NOT derive one from the other; the smart-
|
|
73
|
+
* wallet equality check
|
|
74
|
+
* computeSmartWalletFromSigner(signerAddress) === providerAddress
|
|
75
|
+
* is the SERVER'S responsibility (cross-vendor factory ABI agnostic).
|
|
76
|
+
*
|
|
77
|
+
* What the SDK enforces in `build*()` is the cheaper invariant:
|
|
78
|
+
* `signerAddress === await signer.getAddress()`.
|
|
79
|
+
*
|
|
80
|
+
* ## payloadHash defends against body-after-sign tamper
|
|
81
|
+
*
|
|
82
|
+
* Because `payloadHash = keccak256(bodyBytes)` is part of the SIGNED
|
|
83
|
+
* EIP-712 projection, any post-signature mutation of `wire.body`
|
|
84
|
+
* causes `bodyHash(wire.body)` to diverge from `signed.payloadHash`.
|
|
85
|
+
* The verifier short-circuits with `envelope_payload_hash_mismatch`
|
|
86
|
+
* before signature recovery, so a malicious relay cannot swap bodies
|
|
87
|
+
* even within an otherwise valid signed envelope.
|
|
88
|
+
*
|
|
89
|
+
* ## Verification order (first failure short-circuits)
|
|
90
|
+
*
|
|
91
|
+
* 1. `validateEnvelopeWire(wire)` — structural shape, types, lengths,
|
|
92
|
+
* and scheme/canonical-empty consistency in one pass.
|
|
93
|
+
* 2. `validateSchemeConsistency(signed)` — defense-in-depth re-check
|
|
94
|
+
* (already covered by step 1 but called explicitly so future
|
|
95
|
+
* refactors of the validator do not silently weaken the contract).
|
|
96
|
+
* 3. `signed.chainId === expectedChainId` → `envelope_chain_mismatch`.
|
|
97
|
+
* 4. `signed.kernelAddress` (lc) === `expectedKernelAddress` (lc)
|
|
98
|
+
* → `envelope_kernel_mismatch`.
|
|
99
|
+
* 5. `bodyHash(wire.body) === signed.payloadHash`
|
|
100
|
+
* → `envelope_payload_hash_mismatch`.
|
|
101
|
+
* 6. `recoverEnvelopeSigner(signed, providerSig, expectedKernel)`
|
|
102
|
+
* (lc) === `signed.signerAddress` (lc)
|
|
103
|
+
* → `envelope_signature_invalid`.
|
|
104
|
+
* 7. `|now - signed.createdAt| <= ENVELOPE_TIMESTAMP_SKEW_SEC`
|
|
105
|
+
* → `envelope_timestamp_skew`.
|
|
106
|
+
*
|
|
107
|
+
* Note that timestamp skew is checked LAST — a forged signature is a
|
|
108
|
+
* more severe error than a stale-but-genuine envelope and we want the
|
|
109
|
+
* caller to see the more severe failure first.
|
|
110
|
+
*
|
|
111
|
+
* @module delivery/envelopeBuilder
|
|
112
|
+
* @see ./types — signed/wire interfaces and {@link BuildEnvelopeResult}.
|
|
113
|
+
* @see ./eip712 — domain, types, and {@link recoverEnvelopeSigner}.
|
|
114
|
+
* @see ./validate — {@link validateEnvelopeWire},
|
|
115
|
+
* {@link validateSchemeConsistency}.
|
|
116
|
+
* @see ./keys — X25519 keygen + ECDH + HKDF.
|
|
117
|
+
* @see ./crypto — AES-256-GCM seal/open + {@link bodyHash}.
|
|
118
|
+
* @see ./setupBuilder — sibling builder; same shape conventions.
|
|
119
|
+
*/
|
|
120
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
121
|
+
exports.DeliveryEnvelopeBuilder = exports.resetSecondsNowForTests = exports.setSecondsNowForTests = exports.buildEnvelopeAad = exports.ENVELOPE_AAD_LENGTH = exports.ENVELOPE_TIMESTAMP_SKEW_SEC = void 0;
|
|
122
|
+
const ethers_1 = require("ethers");
|
|
123
|
+
const canonicalJson_1 = require("../utils/canonicalJson");
|
|
124
|
+
const crypto_1 = require("./crypto");
|
|
125
|
+
const eip712_1 = require("./eip712");
|
|
126
|
+
const keys_1 = require("./keys");
|
|
127
|
+
const types_1 = require("./types");
|
|
128
|
+
const validate_1 = require("./validate");
|
|
129
|
+
// ============================================================================
|
|
130
|
+
// Constants
|
|
131
|
+
// ============================================================================
|
|
132
|
+
/**
|
|
133
|
+
* Maximum tolerated clock-skew, in seconds, between the signed
|
|
134
|
+
* `createdAt` and the verifier's wall clock. Symmetric (past + future).
|
|
135
|
+
*
|
|
136
|
+
* 900s (15 min) matches {@link DeliverySetupBuilder}'s
|
|
137
|
+
* `SETUP_TIMESTAMP_SKEW_SEC`, the receipts-V2 freshness window, and
|
|
138
|
+
* the AIP-3 anchor-receipt skew bound.
|
|
139
|
+
*/
|
|
140
|
+
exports.ENVELOPE_TIMESTAMP_SKEW_SEC = 900;
|
|
141
|
+
/**
|
|
142
|
+
* Length (in bytes) of the AES-256-GCM AAD used by the encrypted
|
|
143
|
+
* scheme — `txId_bytes (32) || signerAddress_bytes (20) = 52`.
|
|
144
|
+
*
|
|
145
|
+
* H5 binding: GCM authenticates the AAD; a misrouted envelope (correct
|
|
146
|
+
* ciphertext + nonce + tag + sessionKey, but delivered as if for a
|
|
147
|
+
* different `txId` or `signerAddress`) fails the tag check on decrypt.
|
|
148
|
+
* The hash-input `payloadHash` in the EIP-712 signature already binds
|
|
149
|
+
* the body to a specific `txId` at the signature layer; this AAD adds
|
|
150
|
+
* the same binding INSIDE the GCM authentication, defense-in-depth.
|
|
151
|
+
*
|
|
152
|
+
* Bytes layout (network byte order, no padding):
|
|
153
|
+
* - [0..32): `txId` raw 32 bytes (from the 0x + 64 hex chars).
|
|
154
|
+
* - [32..52): `signerAddress` raw 20 bytes (from the 0x + 40 hex chars).
|
|
155
|
+
*/
|
|
156
|
+
exports.ENVELOPE_AAD_LENGTH = 52;
|
|
157
|
+
/**
|
|
158
|
+
* Construct the AES-256-GCM AAD for the `x25519-aes256gcm-v1` scheme.
|
|
159
|
+
*
|
|
160
|
+
* Format: `txId (32 bytes) || signerAddress (20 bytes) = 52 bytes`.
|
|
161
|
+
*
|
|
162
|
+
* Both the build side (in {@link DeliveryEnvelopeBuilder.buildEncrypted})
|
|
163
|
+
* and the decrypt side (in {@link DeliveryEnvelopeBuilder.decryptPayload})
|
|
164
|
+
* call this helper with the SAME txId/signerAddress so the GCM tag
|
|
165
|
+
* commits to identical AAD bytes. Address case is normalized via
|
|
166
|
+
* `bytesFromHex` (which is case-insensitive on the hex characters), so
|
|
167
|
+
* checksum vs lowercase inputs round-trip to the same 20 raw bytes.
|
|
168
|
+
*
|
|
169
|
+
* @param txId - On-chain transaction id, `0x` + 64 hex chars.
|
|
170
|
+
* @param signerAddress - EOA address, `0x` + 40 hex chars.
|
|
171
|
+
* @returns 52-byte AAD buffer (`txId_bytes || signerAddress_bytes`).
|
|
172
|
+
* @throws {DeliveryCryptoError} `crypto_decrypt_failed` if either
|
|
173
|
+
* parameter has the wrong byte length (decoded via `bytesFromHex`
|
|
174
|
+
* from `./crypto`); the underlying helper raises this code.
|
|
175
|
+
*
|
|
176
|
+
* @internal Used by the envelope builder; exposed for cross-SDK
|
|
177
|
+
* fixtures and the H5 test suite.
|
|
178
|
+
*/
|
|
179
|
+
function buildEnvelopeAad(txId, signerAddress) {
|
|
180
|
+
const txIdBytes = (0, crypto_1.bytesFromHex)(txId);
|
|
181
|
+
if (txIdBytes.length !== 32) {
|
|
182
|
+
throw new eip712_1.DeliveryEip712Error('BUILDER_AAD_TXID_INVALID_LENGTH', `txId must decode to 32 bytes, got ${txIdBytes.length}`, { actualLength: txIdBytes.length });
|
|
183
|
+
}
|
|
184
|
+
const signerBytes = (0, crypto_1.bytesFromHex)(signerAddress);
|
|
185
|
+
if (signerBytes.length !== 20) {
|
|
186
|
+
throw new eip712_1.DeliveryEip712Error('BUILDER_AAD_SIGNER_INVALID_LENGTH', `signerAddress must decode to 20 bytes, got ${signerBytes.length}`, { actualLength: signerBytes.length });
|
|
187
|
+
}
|
|
188
|
+
const aad = new Uint8Array(exports.ENVELOPE_AAD_LENGTH);
|
|
189
|
+
aad.set(txIdBytes, 0);
|
|
190
|
+
aad.set(signerBytes, 32);
|
|
191
|
+
return aad;
|
|
192
|
+
}
|
|
193
|
+
exports.buildEnvelopeAad = buildEnvelopeAad;
|
|
194
|
+
// ============================================================================
|
|
195
|
+
// secondsNow — Injectable Clock
|
|
196
|
+
// ============================================================================
|
|
197
|
+
//
|
|
198
|
+
// Every timestamp read inside this module flows through `secondsNow()`.
|
|
199
|
+
// Tests inject deterministic clocks via {@link setSecondsNowForTests};
|
|
200
|
+
// production calls fall through to the real wall clock.
|
|
201
|
+
//
|
|
202
|
+
// This is the ONLY allowed wall-clock site in this file — the same
|
|
203
|
+
// single-seam discipline used by `setupBuilder.ts` and the other
|
|
204
|
+
// builders. Forbidden-token lint depends on this.
|
|
205
|
+
//
|
|
206
|
+
let secondsNowImpl = () => {
|
|
207
|
+
// Single allowed wall-clock site in this module. `Math.floor` produces
|
|
208
|
+
// an integer; integer Unix seconds round-trip cleanly through the
|
|
209
|
+
// EIP-712 `uint64` field.
|
|
210
|
+
return Math.floor(Date.now() / 1000);
|
|
211
|
+
};
|
|
212
|
+
/**
|
|
213
|
+
* Return the current wall-clock time in Unix seconds.
|
|
214
|
+
*
|
|
215
|
+
* Production: real `Date.now()` via {@link secondsNowImpl}.
|
|
216
|
+
* Tests: injected via {@link setSecondsNowForTests}.
|
|
217
|
+
*
|
|
218
|
+
* @returns Integer seconds since the Unix epoch.
|
|
219
|
+
*/
|
|
220
|
+
function secondsNow() {
|
|
221
|
+
return secondsNowImpl();
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Replace the wall-clock implementation used inside this module.
|
|
225
|
+
*
|
|
226
|
+
* **TEST-ONLY.** Production code MUST NOT call this. Pass `null` (or
|
|
227
|
+
* call {@link resetSecondsNowForTests}) to restore the real-clock
|
|
228
|
+
* implementation.
|
|
229
|
+
*
|
|
230
|
+
* @param impl - Replacement function returning Unix seconds, or `null`
|
|
231
|
+
* to restore the default real-clock implementation.
|
|
232
|
+
*/
|
|
233
|
+
function setSecondsNowForTests(impl) {
|
|
234
|
+
if (impl === null) {
|
|
235
|
+
resetSecondsNowForTests();
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
secondsNowImpl = impl;
|
|
239
|
+
}
|
|
240
|
+
exports.setSecondsNowForTests = setSecondsNowForTests;
|
|
241
|
+
/**
|
|
242
|
+
* Restore {@link secondsNow} to its default real-clock implementation.
|
|
243
|
+
*
|
|
244
|
+
* **TEST-ONLY.** Safe to call when no override is active.
|
|
245
|
+
*/
|
|
246
|
+
function resetSecondsNowForTests() {
|
|
247
|
+
secondsNowImpl = () => Math.floor(Date.now() / 1000);
|
|
248
|
+
}
|
|
249
|
+
exports.resetSecondsNowForTests = resetSecondsNowForTests;
|
|
250
|
+
// ============================================================================
|
|
251
|
+
// Envelope Builder
|
|
252
|
+
// ============================================================================
|
|
253
|
+
/**
|
|
254
|
+
* Builder + verifier + decryptor for AIP-16 delivery envelopes.
|
|
255
|
+
*
|
|
256
|
+
* Instances are cheap to construct and have no I/O side effects.
|
|
257
|
+
* `verify()`, `decryptPayload()`, `verifyAndDecrypt()`, and
|
|
258
|
+
* `computeHash()` are static — call them without constructing an
|
|
259
|
+
* instance.
|
|
260
|
+
*
|
|
261
|
+
* @example Provider build (public)
|
|
262
|
+
* ```typescript
|
|
263
|
+
* const builder = new DeliveryEnvelopeBuilder(wallet);
|
|
264
|
+
* const { wire } = await builder.buildPublic({
|
|
265
|
+
* txId, chainId: 84532, kernelAddress: KERNEL,
|
|
266
|
+
* providerAddress: SMART_WALLET, signerAddress: EOA,
|
|
267
|
+
* payload: { result: 'ok' },
|
|
268
|
+
* });
|
|
269
|
+
* await postToRelay(wire);
|
|
270
|
+
* ```
|
|
271
|
+
*
|
|
272
|
+
* @example Provider build (encrypted)
|
|
273
|
+
* ```typescript
|
|
274
|
+
* const { wire, blobKey } = await builder.buildEncrypted({
|
|
275
|
+
* txId, chainId: 84532, kernelAddress: KERNEL,
|
|
276
|
+
* providerAddress: SMART_WALLET, signerAddress: EOA,
|
|
277
|
+
* payload: { secret: 'data' },
|
|
278
|
+
* buyerEphemeralPubkey: setupSigned.buyerEphemeralPubkey,
|
|
279
|
+
* });
|
|
280
|
+
* ```
|
|
281
|
+
*
|
|
282
|
+
* @example Buyer decrypt
|
|
283
|
+
* ```typescript
|
|
284
|
+
* const result = await DeliveryEnvelopeBuilder.verifyAndDecrypt(
|
|
285
|
+
* wire,
|
|
286
|
+
* buyerEphemeralPrivKey,
|
|
287
|
+
* { expectedKernelAddress: KERNEL, expectedChainId: 84532 },
|
|
288
|
+
* );
|
|
289
|
+
* if (!result.ok) throw new Error(result.code);
|
|
290
|
+
* const payload = result.payload;
|
|
291
|
+
* ```
|
|
292
|
+
*/
|
|
293
|
+
class DeliveryEnvelopeBuilder {
|
|
294
|
+
/**
|
|
295
|
+
* Construct a new builder.
|
|
296
|
+
*
|
|
297
|
+
* @param signer - EOA signer required for {@link buildPublic} and
|
|
298
|
+
* {@link buildEncrypted}. Pass `undefined` for a verify-/decrypt-only
|
|
299
|
+
* instance; all verification / decryption helpers are static and
|
|
300
|
+
* do not consult builder state.
|
|
301
|
+
*/
|
|
302
|
+
constructor(signer) {
|
|
303
|
+
this.signer = signer;
|
|
304
|
+
}
|
|
305
|
+
// --------------------------------------------------------------------------
|
|
306
|
+
// buildPublic
|
|
307
|
+
// --------------------------------------------------------------------------
|
|
308
|
+
/**
|
|
309
|
+
* Construct, sign, and return a {@link DeliveryEnvelopeWireV1} using
|
|
310
|
+
* the `public-v1` scheme.
|
|
311
|
+
*
|
|
312
|
+
* Encoding (FIX-1, AIP-16 Phase 3.5):
|
|
313
|
+
* - `bodyString = JSON.stringify(params.payload)`.
|
|
314
|
+
* - `wire.body = bodyString` (plaintext UTF-8 JSON, NOT hex).
|
|
315
|
+
* - `payloadHash = keccak256(utf8Bytes(bodyString))`.
|
|
316
|
+
* - The Platform verifier recomputes `keccak256(toUtf8Bytes(body))`
|
|
317
|
+
* on the wire body directly, so the SDK and verifier hash the
|
|
318
|
+
* same bytes byte-for-byte.
|
|
319
|
+
*
|
|
320
|
+
* Canonical-empty enforcement:
|
|
321
|
+
* - `providerEphemeralPubkey = CANONICAL_EMPTY_BYTES32`.
|
|
322
|
+
* - `nonce = CANONICAL_EMPTY_BYTES12`.
|
|
323
|
+
* - `tag = CANONICAL_EMPTY_BYTES16`.
|
|
324
|
+
*
|
|
325
|
+
* Pre-checks:
|
|
326
|
+
* - signer MUST be present.
|
|
327
|
+
* - `signerAddress` MUST equal `await signer.getAddress()`.
|
|
328
|
+
*
|
|
329
|
+
* @param params - {@link BuildPublicEnvelopeParams}.
|
|
330
|
+
* @returns A {@link BuildEnvelopeResult} carrying the signed wire
|
|
331
|
+
* envelope and the raw plaintext bytes (`bodyBytes`) that the
|
|
332
|
+
* `payloadHash` was computed over. `blobKey` is `undefined` for
|
|
333
|
+
* the public scheme.
|
|
334
|
+
* @throws {DeliveryEip712Error} on signer absence or signer/address
|
|
335
|
+
* mismatch.
|
|
336
|
+
*/
|
|
337
|
+
async buildPublic(params) {
|
|
338
|
+
if (!this.signer) {
|
|
339
|
+
throw new eip712_1.DeliveryEip712Error('BUILDER_NO_SIGNER', 'DeliveryEnvelopeBuilder.buildPublic requires a signer; construct the builder with a Signer to sign envelopes.');
|
|
340
|
+
}
|
|
341
|
+
// ----- Timestamps -----
|
|
342
|
+
const createdAt = params.createdAt ?? secondsNow();
|
|
343
|
+
if (!Number.isInteger(createdAt) || createdAt <= 0) {
|
|
344
|
+
throw new eip712_1.DeliveryEip712Error('BUILDER_INVALID_CREATED_AT', `createdAt must be a positive integer, got ${String(createdAt)}`, { createdAt });
|
|
345
|
+
}
|
|
346
|
+
// ----- Signer-address binding -----
|
|
347
|
+
//
|
|
348
|
+
// Caught at build time so a wrong-EOA bug surfaces here rather than
|
|
349
|
+
// later at relay-side verification (where the error would be much
|
|
350
|
+
// harder to diagnose).
|
|
351
|
+
const actualSigner = await this.signer.getAddress();
|
|
352
|
+
if (actualSigner.toLowerCase() !== params.signerAddress.toLowerCase()) {
|
|
353
|
+
throw new eip712_1.DeliveryEip712Error('BUILDER_SIGNER_ADDRESS_MISMATCH', 'params.signerAddress does not match signer.getAddress()', {
|
|
354
|
+
expected: actualSigner.toLowerCase(),
|
|
355
|
+
got: params.signerAddress.toLowerCase(),
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
// ----- Smart-wallet nonce (H4 fix) -----
|
|
359
|
+
const smartWalletNonce = params.smartWalletNonce ?? 0;
|
|
360
|
+
if (!Number.isInteger(smartWalletNonce) || smartWalletNonce < 0) {
|
|
361
|
+
throw new eip712_1.DeliveryEip712Error('BUILDER_INVALID_SMART_WALLET_NONCE', `smartWalletNonce must be a non-negative integer, got ${String(smartWalletNonce)}`, { smartWalletNonce });
|
|
362
|
+
}
|
|
363
|
+
// ----- Encode body -----
|
|
364
|
+
//
|
|
365
|
+
// JSON.stringify is the canonical serializer for the public scheme;
|
|
366
|
+
// we do NOT use canonicalJsonStringify here because the body is a
|
|
367
|
+
// user payload (any JSON value), and the BUYER needs to recover the
|
|
368
|
+
// exact object the provider wrote — sorting keys post-hoc would
|
|
369
|
+
// silently mutate the structure.
|
|
370
|
+
//
|
|
371
|
+
// FIX-1 (AIP-16 Phase 3.5 HIGH): wire.body for public-v1 is the
|
|
372
|
+
// plaintext UTF-8 JSON STRING — NOT hex. Hashing path is
|
|
373
|
+
// `keccak256(utf8Bytes(bodyString))`, which `bodyHash(string)`
|
|
374
|
+
// produces by routing through `toBytes(string, 'body')`. The
|
|
375
|
+
// Platform verifier at `lib/delivery/auth.ts` for `public-v1`
|
|
376
|
+
// computes `keccak256(toUtf8Bytes(body))` — wire-compatible iff
|
|
377
|
+
// `wire.body` is the plaintext string. (Encrypted scheme below
|
|
378
|
+
// remains hex; the encrypted recompute hex-decodes first.)
|
|
379
|
+
const bodyString = JSON.stringify(params.payload);
|
|
380
|
+
const plaintextBytes = new Uint8Array(Buffer.from(bodyString, 'utf8'));
|
|
381
|
+
const wireBody = bodyString; // plaintext UTF-8 JSON, NOT hex
|
|
382
|
+
const payloadHash = (0, crypto_1.bodyHash)(bodyString); // bodyHash(string) → utf8 bytes
|
|
383
|
+
// ----- Build signed projection -----
|
|
384
|
+
//
|
|
385
|
+
// Field order in the OBJECT does not matter — EIP-712 hashes by the
|
|
386
|
+
// type schema. We mirror the schema order in the source for
|
|
387
|
+
// readability and to make drift against
|
|
388
|
+
// `DELIVERY_ENVELOPE_TYPES_V1` easy to spot.
|
|
389
|
+
const signed = {
|
|
390
|
+
version: 1,
|
|
391
|
+
txId: params.txId,
|
|
392
|
+
chainId: params.chainId,
|
|
393
|
+
kernelAddress: params.kernelAddress,
|
|
394
|
+
providerAddress: params.providerAddress,
|
|
395
|
+
signerAddress: params.signerAddress,
|
|
396
|
+
scheme: 'public-v1',
|
|
397
|
+
providerEphemeralPubkey: types_1.CANONICAL_EMPTY_BYTES32,
|
|
398
|
+
nonce: types_1.CANONICAL_EMPTY_BYTES12,
|
|
399
|
+
payloadHash,
|
|
400
|
+
tag: types_1.CANONICAL_EMPTY_BYTES16,
|
|
401
|
+
createdAt,
|
|
402
|
+
smartWalletNonce,
|
|
403
|
+
};
|
|
404
|
+
// ----- Sign -----
|
|
405
|
+
const domain = (0, eip712_1.buildDeliveryDomain)(params.chainId, params.kernelAddress);
|
|
406
|
+
const providerSig = (await this.signer.signTypedData(domain, eip712_1.DELIVERY_ENVELOPE_TYPES_V1, signed));
|
|
407
|
+
const wire = {
|
|
408
|
+
signed,
|
|
409
|
+
body: wireBody,
|
|
410
|
+
providerSig,
|
|
411
|
+
};
|
|
412
|
+
return {
|
|
413
|
+
wire,
|
|
414
|
+
bodyBytes: plaintextBytes,
|
|
415
|
+
// blobKey intentionally omitted for public scheme.
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
// --------------------------------------------------------------------------
|
|
419
|
+
// buildEncrypted
|
|
420
|
+
// --------------------------------------------------------------------------
|
|
421
|
+
/**
|
|
422
|
+
* Construct, sign, and return a {@link DeliveryEnvelopeWireV1} using
|
|
423
|
+
* the `x25519-aes256gcm-v1` scheme.
|
|
424
|
+
*
|
|
425
|
+
* Crypto flow:
|
|
426
|
+
* 1. Generate (or accept) a provider ephemeral X25519 keypair.
|
|
427
|
+
* 2. `shared = X25519(providerPriv, buyerPub)`.
|
|
428
|
+
* 3. `sessionKey = HKDF-SHA256(ikm=shared, salt=txId, info="agirails-delivery-v1", L=32)`.
|
|
429
|
+
* 4. `plaintextBytes = utf8Bytes(JSON.stringify(payload))`.
|
|
430
|
+
* 5. `{ciphertext, nonce, tag} = AES-256-GCM(plaintextBytes, sessionKey)`.
|
|
431
|
+
* 6. `wire.body = bytesToHex(ciphertext)`.
|
|
432
|
+
* 7. `payloadHash = keccak256(ciphertext)`.
|
|
433
|
+
* 8. Sign the EIP-712 projection containing `scheme = "x25519-aes256gcm-v1"`,
|
|
434
|
+
* the provider's ephemeral pubkey, the AES-GCM nonce + tag, and
|
|
435
|
+
* `payloadHash`.
|
|
436
|
+
*
|
|
437
|
+
* The provider's ephemeral PRIVATE key is dropped after step 5;
|
|
438
|
+
* forward secrecy w.r.t. provider long-term keys is provided by the
|
|
439
|
+
* fresh keypair per delivery.
|
|
440
|
+
*
|
|
441
|
+
* Pre-checks:
|
|
442
|
+
* - signer MUST be present.
|
|
443
|
+
* - `signerAddress` MUST equal `await signer.getAddress()`.
|
|
444
|
+
* - `buyerEphemeralPubkey` MUST NOT be {@link CANONICAL_EMPTY_BYTES32}.
|
|
445
|
+
*
|
|
446
|
+
* @param params - {@link BuildEncryptedEnvelopeParams}.
|
|
447
|
+
* @returns A {@link BuildEnvelopeResult} carrying the signed wire
|
|
448
|
+
* envelope, the ciphertext bytes (`bodyBytes`) the `payloadHash`
|
|
449
|
+
* was computed over, and the symmetric session `blobKey` (for
|
|
450
|
+
* provider-side observability / future reference-mode flows; the
|
|
451
|
+
* buyer derives the key independently).
|
|
452
|
+
* @throws {DeliveryEip712Error} on signer absence, signer/address
|
|
453
|
+
* mismatch, or canonical-empty buyer pubkey.
|
|
454
|
+
*/
|
|
455
|
+
async buildEncrypted(params) {
|
|
456
|
+
if (!this.signer) {
|
|
457
|
+
throw new eip712_1.DeliveryEip712Error('BUILDER_NO_SIGNER', 'DeliveryEnvelopeBuilder.buildEncrypted requires a signer; construct the builder with a Signer to sign envelopes.');
|
|
458
|
+
}
|
|
459
|
+
// ----- Buyer pubkey canonical-empty rejection -----
|
|
460
|
+
if (params.buyerEphemeralPubkey.toLowerCase() ===
|
|
461
|
+
types_1.CANONICAL_EMPTY_BYTES32.toLowerCase()) {
|
|
462
|
+
throw new eip712_1.DeliveryEip712Error('BUILDER_ENCRYPTED_BUYER_PUBKEY_IS_CANONICAL_EMPTY', 'x25519-aes256gcm-v1 requires a non-zero X25519 buyer pubkey (RFC 7748 §6.1).', { buyerEphemeralPubkey: params.buyerEphemeralPubkey });
|
|
463
|
+
}
|
|
464
|
+
// ----- Timestamps -----
|
|
465
|
+
const createdAt = params.createdAt ?? secondsNow();
|
|
466
|
+
if (!Number.isInteger(createdAt) || createdAt <= 0) {
|
|
467
|
+
throw new eip712_1.DeliveryEip712Error('BUILDER_INVALID_CREATED_AT', `createdAt must be a positive integer, got ${String(createdAt)}`, { createdAt });
|
|
468
|
+
}
|
|
469
|
+
// ----- Signer-address binding -----
|
|
470
|
+
const actualSigner = await this.signer.getAddress();
|
|
471
|
+
if (actualSigner.toLowerCase() !== params.signerAddress.toLowerCase()) {
|
|
472
|
+
throw new eip712_1.DeliveryEip712Error('BUILDER_SIGNER_ADDRESS_MISMATCH', 'params.signerAddress does not match signer.getAddress()', {
|
|
473
|
+
expected: actualSigner.toLowerCase(),
|
|
474
|
+
got: params.signerAddress.toLowerCase(),
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
// ----- Ephemeral keypair (generate or accept) -----
|
|
478
|
+
//
|
|
479
|
+
// Production callers omit `providerEphemeralKeyPair` so the private
|
|
480
|
+
// key never crosses a call boundary. Tests pass an explicit pair
|
|
481
|
+
// for determinism / known-answer-test vectors.
|
|
482
|
+
const providerKp = params.providerEphemeralKeyPair ?? (0, keys_1.generateEphemeralKeyPair)();
|
|
483
|
+
// ----- ECDH + HKDF -----
|
|
484
|
+
const peerPubkey = (0, keys_1.pubkeyFromHex)(params.buyerEphemeralPubkey);
|
|
485
|
+
const shared = (0, keys_1.deriveSharedSecret)(providerKp.privateKey, peerPubkey);
|
|
486
|
+
const sessionKey = (0, keys_1.deriveSessionKey)(shared, params.txId);
|
|
487
|
+
// ----- Encrypt (with H5 AAD binding) -----
|
|
488
|
+
//
|
|
489
|
+
// AAD = txId_bytes (32) || signerAddress_bytes (20) = 52 bytes.
|
|
490
|
+
// Bound INSIDE the GCM authentication tag — a misrouted envelope
|
|
491
|
+
// delivered as if for a different txId or signerAddress cannot be
|
|
492
|
+
// opened even if the attacker possesses the ciphertext + nonce +
|
|
493
|
+
// tag + session key. Defense-in-depth on top of the EIP-712
|
|
494
|
+
// signature binding via `payloadHash` and `txId`.
|
|
495
|
+
const aad = buildEnvelopeAad(params.txId, params.signerAddress);
|
|
496
|
+
const bodyString = JSON.stringify(params.payload);
|
|
497
|
+
const plaintextBytes = new Uint8Array(Buffer.from(bodyString, 'utf8'));
|
|
498
|
+
const { ciphertext, nonce, tag } = (0, crypto_1.encryptBody)(plaintextBytes, sessionKey, aad);
|
|
499
|
+
// ----- Wire body + payloadHash -----
|
|
500
|
+
//
|
|
501
|
+
// `payloadHash` is computed over the CIPHERTEXT bytes (not the
|
|
502
|
+
// plaintext) so the signature commits to exactly what travels on
|
|
503
|
+
// the wire. See AIP-16 §6.2 and `bodyHash` JSDoc.
|
|
504
|
+
const wireBodyHex = (0, crypto_1.bytesToHex)(ciphertext);
|
|
505
|
+
const payloadHash = (0, crypto_1.bodyHash)(ciphertext);
|
|
506
|
+
// ----- Smart-wallet nonce (H4 fix) -----
|
|
507
|
+
const smartWalletNonce = params.smartWalletNonce ?? 0;
|
|
508
|
+
if (!Number.isInteger(smartWalletNonce) || smartWalletNonce < 0) {
|
|
509
|
+
throw new eip712_1.DeliveryEip712Error('BUILDER_INVALID_SMART_WALLET_NONCE', `smartWalletNonce must be a non-negative integer, got ${String(smartWalletNonce)}`, { smartWalletNonce });
|
|
510
|
+
}
|
|
511
|
+
// ----- Build signed projection -----
|
|
512
|
+
const signed = {
|
|
513
|
+
version: 1,
|
|
514
|
+
txId: params.txId,
|
|
515
|
+
chainId: params.chainId,
|
|
516
|
+
kernelAddress: params.kernelAddress,
|
|
517
|
+
providerAddress: params.providerAddress,
|
|
518
|
+
signerAddress: params.signerAddress,
|
|
519
|
+
scheme: 'x25519-aes256gcm-v1',
|
|
520
|
+
providerEphemeralPubkey: (0, keys_1.pubkeyToHex)(providerKp.publicKey),
|
|
521
|
+
nonce: (0, crypto_1.bytesToHex)(nonce),
|
|
522
|
+
payloadHash,
|
|
523
|
+
tag: (0, crypto_1.bytesToHex)(tag),
|
|
524
|
+
createdAt,
|
|
525
|
+
smartWalletNonce,
|
|
526
|
+
};
|
|
527
|
+
// ----- Sign -----
|
|
528
|
+
const domain = (0, eip712_1.buildDeliveryDomain)(params.chainId, params.kernelAddress);
|
|
529
|
+
const providerSig = (await this.signer.signTypedData(domain, eip712_1.DELIVERY_ENVELOPE_TYPES_V1, signed));
|
|
530
|
+
const wire = {
|
|
531
|
+
signed,
|
|
532
|
+
body: wireBodyHex,
|
|
533
|
+
providerSig,
|
|
534
|
+
};
|
|
535
|
+
return {
|
|
536
|
+
wire,
|
|
537
|
+
bodyBytes: ciphertext,
|
|
538
|
+
blobKey: sessionKey,
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
// --------------------------------------------------------------------------
|
|
542
|
+
// verify (static)
|
|
543
|
+
// --------------------------------------------------------------------------
|
|
544
|
+
/**
|
|
545
|
+
* Verify a {@link DeliveryEnvelopeWireV1} received from the relay.
|
|
546
|
+
*
|
|
547
|
+
* See the module-level header for the full check ordering and rationale.
|
|
548
|
+
*
|
|
549
|
+
* @param wire - The wire envelope received from the relay.
|
|
550
|
+
* @param opts.expectedKernelAddress - Trusted kernel address for the
|
|
551
|
+
* target chain (from the verifier's allowlist).
|
|
552
|
+
* @param opts.expectedChainId - Trusted chainId for the target chain.
|
|
553
|
+
* @param opts.now - Override for the verifier's wall clock (Unix
|
|
554
|
+
* seconds). Tests use this for deterministic timestamp-skew paths;
|
|
555
|
+
* production callers SHOULD omit.
|
|
556
|
+
* @returns `{ ok: true, signed }` on success, `{ ok: false, code, error }`
|
|
557
|
+
* on failure.
|
|
558
|
+
*/
|
|
559
|
+
static verify(wire, opts) {
|
|
560
|
+
// Step 1: structural / shape validation (includes scheme/canonical-
|
|
561
|
+
// empty consistency under the hood via `validateEnvelopeSigned`).
|
|
562
|
+
// Any structural defect surfaces as `envelope_signature_invalid`
|
|
563
|
+
// — see setupBuilder.ts for the same rationale.
|
|
564
|
+
const shapeResult = (0, validate_1.validateEnvelopeWire)(wire);
|
|
565
|
+
if (!shapeResult.ok) {
|
|
566
|
+
return {
|
|
567
|
+
ok: false,
|
|
568
|
+
code: 'envelope_signature_invalid',
|
|
569
|
+
error: shapeResult.error,
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
const signed = wire.signed;
|
|
573
|
+
// Step 2: defense-in-depth scheme/canonical-empty re-check. The
|
|
574
|
+
// wire-validator already enforces this, but calling it explicitly
|
|
575
|
+
// here means a future refactor of the validator that accidentally
|
|
576
|
+
// weakens the check is caught by THIS file's tests rather than
|
|
577
|
+
// silently shipping. Cheap enough to be worth the explicitness.
|
|
578
|
+
const consistencyResult = (0, validate_1.validateSchemeConsistency)(signed);
|
|
579
|
+
if (!consistencyResult.ok) {
|
|
580
|
+
return {
|
|
581
|
+
ok: false,
|
|
582
|
+
code: 'envelope_signature_invalid',
|
|
583
|
+
error: consistencyResult.error,
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
// Step 3: chainId match (trusted vs payload).
|
|
587
|
+
if (signed.chainId !== opts.expectedChainId) {
|
|
588
|
+
return {
|
|
589
|
+
ok: false,
|
|
590
|
+
code: 'envelope_chain_mismatch',
|
|
591
|
+
error: `expected chainId ${opts.expectedChainId}, got ${signed.chainId}`,
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
// Step 4: kernel-address match (allowlist anchor).
|
|
595
|
+
const expectedKernelLc = opts.expectedKernelAddress.toLowerCase();
|
|
596
|
+
const payloadKernelLc = signed.kernelAddress.toLowerCase();
|
|
597
|
+
if (payloadKernelLc !== expectedKernelLc) {
|
|
598
|
+
return {
|
|
599
|
+
ok: false,
|
|
600
|
+
code: 'envelope_kernel_mismatch',
|
|
601
|
+
error: `expected kernel ${expectedKernelLc}, got ${payloadKernelLc}`,
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
// Step 5: payloadHash binding. This is THE defense against post-
|
|
605
|
+
// signature body tamper: any change to `wire.body` propagates into
|
|
606
|
+
// `recomputedHash` and diverges from the signed `payloadHash`.
|
|
607
|
+
//
|
|
608
|
+
// Scheme-aware (FIX-1, AIP-16 Phase 3.5 HIGH):
|
|
609
|
+
// - public-v1: wire.body IS the plaintext UTF-8 JSON string.
|
|
610
|
+
// `bodyHash(string)` routes through `toBytes(string, 'body')` →
|
|
611
|
+
// `keccak256(utf8Bytes(body))`. This matches the build path
|
|
612
|
+
// above AND the Platform verifier in `lib/delivery/auth.ts`.
|
|
613
|
+
// - x25519-aes256gcm-v1: wire.body is `0x`-prefixed hex of the
|
|
614
|
+
// raw ciphertext bytes. We hex-decode first, then hash — that
|
|
615
|
+
// is exactly what `bodyHash(ciphertext)` did on the build side.
|
|
616
|
+
let recomputedHash;
|
|
617
|
+
try {
|
|
618
|
+
// Branch on scheme: public-v1 hashes UTF-8 of plaintext string,
|
|
619
|
+
// encrypted hashes hex-decoded ciphertext bytes. The same SDK
|
|
620
|
+
// and Platform paths must agree byte-for-byte.
|
|
621
|
+
if (signed.scheme === 'public-v1') {
|
|
622
|
+
recomputedHash = (0, crypto_1.bodyHash)(wire.body);
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
const bodyBytes = (0, crypto_1.bytesFromHex)(wire.body);
|
|
626
|
+
recomputedHash = (0, crypto_1.bodyHash)(bodyBytes);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
catch (e) {
|
|
630
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
631
|
+
return {
|
|
632
|
+
ok: false,
|
|
633
|
+
code: 'envelope_payload_hash_mismatch',
|
|
634
|
+
error: `failed to decode wire.body for payloadHash recomputation: ${msg}`,
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
if (recomputedHash.toLowerCase() !== signed.payloadHash.toLowerCase()) {
|
|
638
|
+
return {
|
|
639
|
+
ok: false,
|
|
640
|
+
code: 'envelope_payload_hash_mismatch',
|
|
641
|
+
error: `recomputed ${recomputedHash.toLowerCase()} does not match signed.payloadHash ${signed.payloadHash.toLowerCase()}`,
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
// Step 6: signature recovery. We pass the TRUSTED kernel address
|
|
645
|
+
// (already proven to equal the payload's at step 4) so future
|
|
646
|
+
// refactors cannot accidentally let an attacker control the
|
|
647
|
+
// recovery domain.
|
|
648
|
+
let recovered;
|
|
649
|
+
try {
|
|
650
|
+
recovered = (0, eip712_1.recoverEnvelopeSigner)(signed, wire.providerSig, opts.expectedKernelAddress);
|
|
651
|
+
}
|
|
652
|
+
catch (e) {
|
|
653
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
654
|
+
return {
|
|
655
|
+
ok: false,
|
|
656
|
+
code: 'envelope_signature_invalid',
|
|
657
|
+
error: msg,
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
if (recovered.toLowerCase() !== signed.signerAddress.toLowerCase()) {
|
|
661
|
+
return {
|
|
662
|
+
ok: false,
|
|
663
|
+
code: 'envelope_signature_invalid',
|
|
664
|
+
error: `recovered signer ${recovered.toLowerCase()} does not match signed.signerAddress ${signed.signerAddress.toLowerCase()}`,
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
// Step 7: timestamp skew. Symmetric — both past and future. Checked
|
|
668
|
+
// LAST so a forged signature is surfaced first (more severe class
|
|
669
|
+
// of failure).
|
|
670
|
+
const now = opts.now ?? secondsNow();
|
|
671
|
+
if (Math.abs(now - signed.createdAt) > exports.ENVELOPE_TIMESTAMP_SKEW_SEC) {
|
|
672
|
+
return {
|
|
673
|
+
ok: false,
|
|
674
|
+
code: 'envelope_timestamp_skew',
|
|
675
|
+
error: `|now (${now}) - createdAt (${signed.createdAt})| > ${exports.ENVELOPE_TIMESTAMP_SKEW_SEC}s`,
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
return { ok: true, signed };
|
|
679
|
+
}
|
|
680
|
+
// --------------------------------------------------------------------------
|
|
681
|
+
// decryptPayload (static)
|
|
682
|
+
// --------------------------------------------------------------------------
|
|
683
|
+
/**
|
|
684
|
+
* Decrypt the payload of an `x25519-aes256gcm-v1` envelope using the
|
|
685
|
+
* buyer's ephemeral private key.
|
|
686
|
+
*
|
|
687
|
+
* Does NOT verify the EIP-712 signature, the chainId / kernel binding,
|
|
688
|
+
* or the payloadHash. Callers that have not already run {@link verify}
|
|
689
|
+
* SHOULD use {@link verifyAndDecrypt} instead.
|
|
690
|
+
*
|
|
691
|
+
* Throws on:
|
|
692
|
+
* - non-encrypted scheme (`public-v1` envelopes are not decrypted),
|
|
693
|
+
* - malformed buyerEphemeralPrivKey length,
|
|
694
|
+
* - ECDH failure (low-order peer pubkey, etc.),
|
|
695
|
+
* - HKDF failure,
|
|
696
|
+
* - AES-GCM authentication failure (tag mismatch).
|
|
697
|
+
*
|
|
698
|
+
* @param wire - The envelope to decrypt.
|
|
699
|
+
* @param buyerEphemeralPrivKey - The buyer's 32-byte X25519 private
|
|
700
|
+
* key (the one whose pubkey was embedded in the setup).
|
|
701
|
+
* @returns The JSON-parsed payload (`unknown`).
|
|
702
|
+
* @throws {DeliveryCryptoError} via the underlying crypto helpers.
|
|
703
|
+
* @throws {DeliveryEip712Error} `BUILDER_PUBLIC_DECRYPT_NOT_APPLICABLE`
|
|
704
|
+
* when called on a `public-v1` envelope.
|
|
705
|
+
*/
|
|
706
|
+
static async decryptPayload(wire, buyerEphemeralPrivKey) {
|
|
707
|
+
const signed = wire.signed;
|
|
708
|
+
if (signed.scheme !== 'x25519-aes256gcm-v1') {
|
|
709
|
+
throw new eip712_1.DeliveryEip712Error('BUILDER_PUBLIC_DECRYPT_NOT_APPLICABLE', `decryptPayload requires scheme=x25519-aes256gcm-v1; got ${signed.scheme}`, { scheme: signed.scheme });
|
|
710
|
+
}
|
|
711
|
+
// ECDH + HKDF → session key.
|
|
712
|
+
const providerPubkey = (0, keys_1.pubkeyFromHex)(signed.providerEphemeralPubkey);
|
|
713
|
+
const shared = (0, keys_1.deriveSharedSecret)(buyerEphemeralPrivKey, providerPubkey);
|
|
714
|
+
const sessionKey = (0, keys_1.deriveSessionKey)(shared, signed.txId);
|
|
715
|
+
// Decode wire-form values (ciphertext / nonce / tag).
|
|
716
|
+
const ciphertext = (0, crypto_1.bytesFromHex)(wire.body);
|
|
717
|
+
const nonce = (0, crypto_1.bytesFromHex)(signed.nonce);
|
|
718
|
+
const tag = (0, crypto_1.bytesFromHex)(signed.tag);
|
|
719
|
+
// H5 binding: reconstruct the same AAD the encrypt side used.
|
|
720
|
+
// If the envelope was misrouted (txId or signerAddress in
|
|
721
|
+
// `signed` does not match what the encrypt side committed to),
|
|
722
|
+
// the AAD differs and the GCM tag fails to verify — surfaced as
|
|
723
|
+
// `crypto_decrypt_failed`. Both signed.txId and signed.signerAddress
|
|
724
|
+
// are themselves protected by the EIP-712 signature (verified upstream
|
|
725
|
+
// in `verify`), so an attacker cannot lie about them without invalidating
|
|
726
|
+
// the envelope at the signature layer first.
|
|
727
|
+
const aad = buildEnvelopeAad(signed.txId, signed.signerAddress);
|
|
728
|
+
// Authenticated decrypt. `decryptBody` throws on tag mismatch via
|
|
729
|
+
// `crypto_decrypt_failed` — propagated as-is to the caller.
|
|
730
|
+
const plaintextBytes = (0, crypto_1.decryptBody)(ciphertext, sessionKey, nonce, tag, aad);
|
|
731
|
+
// Decode UTF-8 and JSON-parse. We use the global TextDecoder so the
|
|
732
|
+
// implementation is portable across Node and Bun.
|
|
733
|
+
const decoder = new TextDecoder('utf-8', { fatal: true });
|
|
734
|
+
const json = decoder.decode(plaintextBytes);
|
|
735
|
+
return JSON.parse(json);
|
|
736
|
+
}
|
|
737
|
+
// --------------------------------------------------------------------------
|
|
738
|
+
// verifyAndDecrypt (static)
|
|
739
|
+
// --------------------------------------------------------------------------
|
|
740
|
+
/**
|
|
741
|
+
* Combined {@link verify} + payload extraction.
|
|
742
|
+
*
|
|
743
|
+
* For `public-v1`: after a successful `verify`, the wire body (hex)
|
|
744
|
+
* is decoded to bytes, UTF-8 → string, JSON-parsed, and returned.
|
|
745
|
+
*
|
|
746
|
+
* For `x25519-aes256gcm-v1`: after a successful `verify`,
|
|
747
|
+
* {@link decryptPayload} is invoked with `buyerEphemeralPrivKey`.
|
|
748
|
+
*
|
|
749
|
+
* Verification failures are returned as `{ ok: false, code, error }`
|
|
750
|
+
* — the structured shape matches `verify`. Decryption failures are
|
|
751
|
+
* also returned as `{ ok: false, code: "envelope_decrypt_failed", ...}`
|
|
752
|
+
* (catching the underlying `DeliveryCryptoError`).
|
|
753
|
+
*
|
|
754
|
+
* @param wire - The envelope to verify + decrypt.
|
|
755
|
+
* @param buyerEphemeralPrivKey - 32-byte X25519 private key. Ignored
|
|
756
|
+
* for `public-v1` envelopes (pass `new Uint8Array(32)` if you do
|
|
757
|
+
* not have one — the value is never read in that branch).
|
|
758
|
+
* @param opts - Same as {@link verify}.
|
|
759
|
+
* @returns `{ ok: true, payload }` on success, `{ ok: false, code, error }`
|
|
760
|
+
* on any failure.
|
|
761
|
+
*/
|
|
762
|
+
static async verifyAndDecrypt(wire, buyerEphemeralPrivKey, opts) {
|
|
763
|
+
const verifyResult = DeliveryEnvelopeBuilder.verify(wire, opts);
|
|
764
|
+
if (!verifyResult.ok) {
|
|
765
|
+
return verifyResult;
|
|
766
|
+
}
|
|
767
|
+
const signed = verifyResult.signed;
|
|
768
|
+
if (signed.scheme === 'public-v1') {
|
|
769
|
+
// FIX-1 (AIP-16 Phase 3.5 HIGH): wire.body for public-v1 is the
|
|
770
|
+
// plaintext UTF-8 JSON string itself — JSON.parse directly.
|
|
771
|
+
try {
|
|
772
|
+
const payload = JSON.parse(wire.body);
|
|
773
|
+
return { ok: true, payload };
|
|
774
|
+
}
|
|
775
|
+
catch (e) {
|
|
776
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
777
|
+
return {
|
|
778
|
+
ok: false,
|
|
779
|
+
code: 'envelope_decrypt_failed',
|
|
780
|
+
error: `failed to parse public-v1 body as JSON: ${msg}`,
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
// Encrypted scheme — run the decrypt helper. Catch the underlying
|
|
785
|
+
// DeliveryCryptoError / TypeError shape and surface as
|
|
786
|
+
// `envelope_decrypt_failed` so the caller's structured-code surface
|
|
787
|
+
// does not have to know about crypto_* codes.
|
|
788
|
+
try {
|
|
789
|
+
const payload = await DeliveryEnvelopeBuilder.decryptPayload(wire, buyerEphemeralPrivKey);
|
|
790
|
+
return { ok: true, payload };
|
|
791
|
+
}
|
|
792
|
+
catch (e) {
|
|
793
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
794
|
+
return {
|
|
795
|
+
ok: false,
|
|
796
|
+
code: 'envelope_decrypt_failed',
|
|
797
|
+
error: msg,
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
// --------------------------------------------------------------------------
|
|
802
|
+
// computeHash (static)
|
|
803
|
+
// --------------------------------------------------------------------------
|
|
804
|
+
/**
|
|
805
|
+
* Compute a stable, cross-SDK identifier for an envelope wire object.
|
|
806
|
+
*
|
|
807
|
+
* The hash is `keccak256(utf8Bytes(canonicalJsonStringify(wire.signed)))`:
|
|
808
|
+
*
|
|
809
|
+
* - canonical JSON (sorted keys, no whitespace) guarantees byte-for-
|
|
810
|
+
* byte identical input across SDK languages,
|
|
811
|
+
* - `keccak256` matches the on-chain hashing convention,
|
|
812
|
+
* - hashing the SIGNED projection (not the full wire) excludes the
|
|
813
|
+
* signature, body, and any `serverMeta` so the hash is stable
|
|
814
|
+
* across relay-side decoration and signature malleability.
|
|
815
|
+
*
|
|
816
|
+
* This is NOT the EIP-712 signing hash; it is a content-addressing
|
|
817
|
+
* helper for logs, dedup sets, and cross-SDK fixtures.
|
|
818
|
+
*
|
|
819
|
+
* @param wire - The wire envelope to hash.
|
|
820
|
+
* @returns 32-byte hex-encoded keccak256 hash (`0x` + 64 hex chars).
|
|
821
|
+
*/
|
|
822
|
+
static computeHash(wire) {
|
|
823
|
+
return (0, ethers_1.keccak256)((0, ethers_1.toUtf8Bytes)((0, canonicalJson_1.canonicalJsonStringify)(wire.signed)));
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
exports.DeliveryEnvelopeBuilder = DeliveryEnvelopeBuilder;
|
|
827
|
+
// Re-affirm ethers import is used. Like setupBuilder.ts, we keep this
|
|
828
|
+
// guard so very aggressive tree-shakers do not drop the `ethers` symbol
|
|
829
|
+
// while we still depend on its types.
|
|
830
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
831
|
+
const _ethersUsed = ethers_1.ethers;
|
|
832
|
+
//# sourceMappingURL=envelopeBuilder.js.map
|