@heyanon-arp/sdk 0.0.2 → 0.0.4
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/index.js +74 -2
- package/dist/index.mjs +71 -3
- package/dist/purpose.d.ts +2 -2
- package/dist/types/body.d.ts +107 -1
- package/dist/types/envelope.d.ts +16 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/poll.d.ts +116 -0
- package/dist/webhook/index.d.ts +3 -2
- package/dist/webhook/payload-types.d.ts +198 -0
- package/dist/webhook/webhook.d.ts +140 -20
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -273,8 +273,39 @@ function buildWebhookSignatureHeader(input, sharedSecret) {
|
|
|
273
273
|
return `${Purpose.WEBHOOK}=${base.base64.encode(mac)}`;
|
|
274
274
|
}
|
|
275
275
|
function verifyWebhookSignatureHeader(headerValue, input, sharedSecret) {
|
|
276
|
-
const
|
|
277
|
-
|
|
276
|
+
const secrets = Array.isArray(sharedSecret) ? sharedSecret : [sharedSecret];
|
|
277
|
+
if (secrets.length === 0) return false;
|
|
278
|
+
let matched = false;
|
|
279
|
+
for (const secret of secrets) {
|
|
280
|
+
const expected = buildWebhookSignatureHeader(input, secret);
|
|
281
|
+
matched = constantTimeEqual(expected, headerValue) || matched;
|
|
282
|
+
}
|
|
283
|
+
return matched;
|
|
284
|
+
}
|
|
285
|
+
function verifyAndCheckPayload(args) {
|
|
286
|
+
const tolerance = args.servedAtToleranceMs === void 0 ? 5 * 6e4 : args.servedAtToleranceMs;
|
|
287
|
+
if (tolerance !== null) {
|
|
288
|
+
const servedAtMs = Date.parse(args.input.served_at);
|
|
289
|
+
const nowMs = (args.now ?? /* @__PURE__ */ new Date()).getTime();
|
|
290
|
+
if (Number.isNaN(servedAtMs) || Math.abs(nowMs - servedAtMs) > tolerance) return "stale_attempt";
|
|
291
|
+
}
|
|
292
|
+
const fullInput = { ...args.input, payload_sha256: sha256HexLower(args.rawBody) };
|
|
293
|
+
if (!verifyWebhookSignatureHeader(args.headerValue, fullInput, args.secrets)) return "invalid_signature";
|
|
294
|
+
return "ok";
|
|
295
|
+
}
|
|
296
|
+
function isProbeRequest(headers) {
|
|
297
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
298
|
+
if (name.toLowerCase() !== "x-arp-probe") continue;
|
|
299
|
+
const v = Array.isArray(value) ? value[0] : value;
|
|
300
|
+
if (v === "1" || v === "true") return true;
|
|
301
|
+
}
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
function sha256HexLower(bytes) {
|
|
305
|
+
const digest = sha2.sha256(bytes);
|
|
306
|
+
let hex = "";
|
|
307
|
+
for (const b of digest) hex += b.toString(16).padStart(2, "0");
|
|
308
|
+
return hex;
|
|
278
309
|
}
|
|
279
310
|
function constantTimeEqual(a, b) {
|
|
280
311
|
if (a.length !== b.length) return false;
|
|
@@ -645,6 +676,43 @@ function expiresAt(ttlSeconds, now = /* @__PURE__ */ new Date()) {
|
|
|
645
676
|
return rfc3339(new Date(now.getTime() + ttlSeconds * 1e3));
|
|
646
677
|
}
|
|
647
678
|
|
|
679
|
+
// src/utils/poll.ts
|
|
680
|
+
async function pollUntil(opts) {
|
|
681
|
+
const intervalMs = Math.max(100, opts.intervalMs ?? 3e3);
|
|
682
|
+
const timeoutMs = Math.max(intervalMs, opts.timeoutMs ?? 3e5);
|
|
683
|
+
const deadline = Date.now() + timeoutMs;
|
|
684
|
+
let last;
|
|
685
|
+
if (opts.abortSignal?.aborted) return { kind: "aborted", last: void 0 };
|
|
686
|
+
for (; ; ) {
|
|
687
|
+
try {
|
|
688
|
+
last = await opts.fetch();
|
|
689
|
+
if (opts.predicate(last)) return { kind: "matched", value: last };
|
|
690
|
+
} catch (err) {
|
|
691
|
+
if (!opts.swallowFetchErrors) throw err;
|
|
692
|
+
opts.onFetchError?.(err);
|
|
693
|
+
}
|
|
694
|
+
const remaining = deadline - Date.now();
|
|
695
|
+
if (remaining <= 0) return { kind: "timeout", last };
|
|
696
|
+
const sleepMs = Math.min(intervalMs, remaining);
|
|
697
|
+
await sleepWithAbort(sleepMs, opts.abortSignal);
|
|
698
|
+
if (opts.abortSignal?.aborted) return { kind: "aborted", last };
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
function sleepWithAbort(ms, signal) {
|
|
702
|
+
if (signal?.aborted) return Promise.resolve();
|
|
703
|
+
return new Promise((resolve) => {
|
|
704
|
+
const timer = setTimeout(() => {
|
|
705
|
+
signal?.removeEventListener("abort", onAbort);
|
|
706
|
+
resolve();
|
|
707
|
+
}, ms);
|
|
708
|
+
const onAbort = () => {
|
|
709
|
+
clearTimeout(timer);
|
|
710
|
+
resolve();
|
|
711
|
+
};
|
|
712
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
|
|
648
716
|
// src/types/body.ts
|
|
649
717
|
var DECLINE_REASONS = ["missing_brief", "rate_too_low", "out_of_scope", "policy", "expired_proposal", "capacity", "unspecified", "other"];
|
|
650
718
|
function isDeclineReason(v) {
|
|
@@ -827,15 +895,18 @@ exports.generateKeyPair = generateKeyPair;
|
|
|
827
895
|
exports.getPublicKey = getPublicKey2;
|
|
828
896
|
exports.isAssetIdentifier = isAssetIdentifier;
|
|
829
897
|
exports.isDeclineReason = isDeclineReason;
|
|
898
|
+
exports.isProbeRequest = isProbeRequest;
|
|
830
899
|
exports.isValidDid = isValidDid;
|
|
831
900
|
exports.parseCaip19SolanaAssetId = parseCaip19SolanaAssetId;
|
|
832
901
|
exports.parseDid = parseDid;
|
|
902
|
+
exports.pollUntil = pollUntil;
|
|
833
903
|
exports.resolveAsset = resolveAsset;
|
|
834
904
|
exports.rfc3339 = rfc3339;
|
|
835
905
|
exports.scryptPasswordProofSign = scryptPasswordProofSign;
|
|
836
906
|
exports.scryptPasswordProofVerify = scryptPasswordProofVerify;
|
|
837
907
|
exports.senderNonce = senderNonce;
|
|
838
908
|
exports.serverEventHash = serverEventHash;
|
|
909
|
+
exports.sha256HexLower = sha256HexLower;
|
|
839
910
|
exports.sign = sign2;
|
|
840
911
|
exports.signChallenge = signChallenge;
|
|
841
912
|
exports.signCosignature = signCosignature;
|
|
@@ -845,6 +916,7 @@ exports.signKeyRotationAttestation = signKeyRotationAttestation;
|
|
|
845
916
|
exports.signedMessageHash = signedMessageHash;
|
|
846
917
|
exports.uuidV4 = uuidV4;
|
|
847
918
|
exports.verify = verify2;
|
|
919
|
+
exports.verifyAndCheckPayload = verifyAndCheckPayload;
|
|
848
920
|
exports.verifyChallenge = verifyChallenge;
|
|
849
921
|
exports.verifyCosignature = verifyCosignature;
|
|
850
922
|
exports.verifyEnvelope = verifyEnvelope;
|
package/dist/index.mjs
CHANGED
|
@@ -248,8 +248,39 @@ function buildWebhookSignatureHeader(input, sharedSecret) {
|
|
|
248
248
|
return `${Purpose.WEBHOOK}=${base64.encode(mac)}`;
|
|
249
249
|
}
|
|
250
250
|
function verifyWebhookSignatureHeader(headerValue, input, sharedSecret) {
|
|
251
|
-
const
|
|
252
|
-
|
|
251
|
+
const secrets = Array.isArray(sharedSecret) ? sharedSecret : [sharedSecret];
|
|
252
|
+
if (secrets.length === 0) return false;
|
|
253
|
+
let matched = false;
|
|
254
|
+
for (const secret of secrets) {
|
|
255
|
+
const expected = buildWebhookSignatureHeader(input, secret);
|
|
256
|
+
matched = constantTimeEqual(expected, headerValue) || matched;
|
|
257
|
+
}
|
|
258
|
+
return matched;
|
|
259
|
+
}
|
|
260
|
+
function verifyAndCheckPayload(args) {
|
|
261
|
+
const tolerance = args.servedAtToleranceMs === void 0 ? 5 * 6e4 : args.servedAtToleranceMs;
|
|
262
|
+
if (tolerance !== null) {
|
|
263
|
+
const servedAtMs = Date.parse(args.input.served_at);
|
|
264
|
+
const nowMs = (args.now ?? /* @__PURE__ */ new Date()).getTime();
|
|
265
|
+
if (Number.isNaN(servedAtMs) || Math.abs(nowMs - servedAtMs) > tolerance) return "stale_attempt";
|
|
266
|
+
}
|
|
267
|
+
const fullInput = { ...args.input, payload_sha256: sha256HexLower(args.rawBody) };
|
|
268
|
+
if (!verifyWebhookSignatureHeader(args.headerValue, fullInput, args.secrets)) return "invalid_signature";
|
|
269
|
+
return "ok";
|
|
270
|
+
}
|
|
271
|
+
function isProbeRequest(headers) {
|
|
272
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
273
|
+
if (name.toLowerCase() !== "x-arp-probe") continue;
|
|
274
|
+
const v = Array.isArray(value) ? value[0] : value;
|
|
275
|
+
if (v === "1" || v === "true") return true;
|
|
276
|
+
}
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
function sha256HexLower(bytes) {
|
|
280
|
+
const digest = sha256(bytes);
|
|
281
|
+
let hex = "";
|
|
282
|
+
for (const b of digest) hex += b.toString(16).padStart(2, "0");
|
|
283
|
+
return hex;
|
|
253
284
|
}
|
|
254
285
|
function constantTimeEqual(a, b) {
|
|
255
286
|
if (a.length !== b.length) return false;
|
|
@@ -620,6 +651,43 @@ function expiresAt(ttlSeconds, now = /* @__PURE__ */ new Date()) {
|
|
|
620
651
|
return rfc3339(new Date(now.getTime() + ttlSeconds * 1e3));
|
|
621
652
|
}
|
|
622
653
|
|
|
654
|
+
// src/utils/poll.ts
|
|
655
|
+
async function pollUntil(opts) {
|
|
656
|
+
const intervalMs = Math.max(100, opts.intervalMs ?? 3e3);
|
|
657
|
+
const timeoutMs = Math.max(intervalMs, opts.timeoutMs ?? 3e5);
|
|
658
|
+
const deadline = Date.now() + timeoutMs;
|
|
659
|
+
let last;
|
|
660
|
+
if (opts.abortSignal?.aborted) return { kind: "aborted", last: void 0 };
|
|
661
|
+
for (; ; ) {
|
|
662
|
+
try {
|
|
663
|
+
last = await opts.fetch();
|
|
664
|
+
if (opts.predicate(last)) return { kind: "matched", value: last };
|
|
665
|
+
} catch (err) {
|
|
666
|
+
if (!opts.swallowFetchErrors) throw err;
|
|
667
|
+
opts.onFetchError?.(err);
|
|
668
|
+
}
|
|
669
|
+
const remaining = deadline - Date.now();
|
|
670
|
+
if (remaining <= 0) return { kind: "timeout", last };
|
|
671
|
+
const sleepMs = Math.min(intervalMs, remaining);
|
|
672
|
+
await sleepWithAbort(sleepMs, opts.abortSignal);
|
|
673
|
+
if (opts.abortSignal?.aborted) return { kind: "aborted", last };
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
function sleepWithAbort(ms, signal) {
|
|
677
|
+
if (signal?.aborted) return Promise.resolve();
|
|
678
|
+
return new Promise((resolve) => {
|
|
679
|
+
const timer = setTimeout(() => {
|
|
680
|
+
signal?.removeEventListener("abort", onAbort);
|
|
681
|
+
resolve();
|
|
682
|
+
}, ms);
|
|
683
|
+
const onAbort = () => {
|
|
684
|
+
clearTimeout(timer);
|
|
685
|
+
resolve();
|
|
686
|
+
};
|
|
687
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
|
|
623
691
|
// src/types/body.ts
|
|
624
692
|
var DECLINE_REASONS = ["missing_brief", "rate_too_low", "out_of_scope", "policy", "expired_proposal", "capacity", "unspecified", "other"];
|
|
625
693
|
function isDeclineReason(v) {
|
|
@@ -758,4 +826,4 @@ function computeCreateLockDiscriminator() {
|
|
|
758
826
|
return h.slice(0, 8);
|
|
759
827
|
}
|
|
760
828
|
|
|
761
|
-
export { CAIP19_REGEX, COSIGNATURE_PURPOSES, CREATE_LOCK_DISCRIMINATOR, DECLINE_REASONS, PROTECTED_PURPOSES, PURPOSE_PARTIAL_RELEASE_STRING, PURPOSE_REFUND_STRING, PURPOSE_RELEASE_STRING, Purpose, REFUND_REASON_BYTES, SCRYPT_PARAMS, SETTLEMENT_PURPOSES, SLIP44_SOLANA, SOLANA_CLUSTER_IDS, SPL_TOKEN_PROGRAM_ID_BASE58, TOKEN_2022_PROGRAM_ID_BASE58, USDC_MINTS, WELL_KNOWN_ASSETS, WELL_KNOWN_ASSET_KEYS, base58btcDecode, base58btcEncode, buildCreateLockIxData, buildPartialReleaseDigest, buildRefundDigest, buildReleaseDigest, buildWebhookSignatureHeader, bytes16ToDelegationId, canonicalBytes, canonicalJson, canonicalSha256Hex, computeCreateLockDiscriminator, delegationIdToBytes16, deriveConditionHash, deriveLockId, deriveScryptKey, detectTokenProgramFromOwner, detectTokenProgramFromOwnerBytes, expiresAt, findFirstChainDivergence, formatDid, generateKeyPair, getPublicKey2 as getPublicKey, isAssetIdentifier, isDeclineReason, isValidDid, parseCaip19SolanaAssetId, parseDid, resolveAsset, rfc3339, scryptPasswordProofSign, scryptPasswordProofVerify, senderNonce, serverEventHash, sign2 as sign, signChallenge, signCosignature, signEnvelope, signKeyLinkAttestation, signKeyRotationAttestation, signedMessageHash, uuidV4, verify2 as verify, verifyChallenge, verifyCosignature, verifyEnvelope, verifyKeyLinkAttestation, verifyKeyRotationAttestation, verifyWebhookSignatureHeader };
|
|
829
|
+
export { CAIP19_REGEX, COSIGNATURE_PURPOSES, CREATE_LOCK_DISCRIMINATOR, DECLINE_REASONS, PROTECTED_PURPOSES, PURPOSE_PARTIAL_RELEASE_STRING, PURPOSE_REFUND_STRING, PURPOSE_RELEASE_STRING, Purpose, REFUND_REASON_BYTES, SCRYPT_PARAMS, SETTLEMENT_PURPOSES, SLIP44_SOLANA, SOLANA_CLUSTER_IDS, SPL_TOKEN_PROGRAM_ID_BASE58, TOKEN_2022_PROGRAM_ID_BASE58, USDC_MINTS, WELL_KNOWN_ASSETS, WELL_KNOWN_ASSET_KEYS, base58btcDecode, base58btcEncode, buildCreateLockIxData, buildPartialReleaseDigest, buildRefundDigest, buildReleaseDigest, buildWebhookSignatureHeader, bytes16ToDelegationId, canonicalBytes, canonicalJson, canonicalSha256Hex, computeCreateLockDiscriminator, delegationIdToBytes16, deriveConditionHash, deriveLockId, deriveScryptKey, detectTokenProgramFromOwner, detectTokenProgramFromOwnerBytes, expiresAt, findFirstChainDivergence, formatDid, generateKeyPair, getPublicKey2 as getPublicKey, isAssetIdentifier, isDeclineReason, isProbeRequest, isValidDid, parseCaip19SolanaAssetId, parseDid, pollUntil, resolveAsset, rfc3339, scryptPasswordProofSign, scryptPasswordProofVerify, senderNonce, serverEventHash, sha256HexLower, sign2 as sign, signChallenge, signCosignature, signEnvelope, signKeyLinkAttestation, signKeyRotationAttestation, signedMessageHash, uuidV4, verify2 as verify, verifyAndCheckPayload, verifyChallenge, verifyCosignature, verifyEnvelope, verifyKeyLinkAttestation, verifyKeyRotationAttestation, verifyWebhookSignatureHeader };
|
package/dist/purpose.d.ts
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* "this byte string was signed" assertion is not enough; the bytes
|
|
8
8
|
* themselves carry the role).
|
|
9
9
|
*
|
|
10
|
-
* Adding a new purpose
|
|
11
|
-
* [00-core/protocol.md](../../00-core/protocol.md) in
|
|
10
|
+
* Adding a new purpose means adding an entry here and keeping
|
|
11
|
+
* [00-core/protocol.md](../../00-core/protocol.md) in sync.
|
|
12
12
|
*/
|
|
13
13
|
export declare const Purpose: {
|
|
14
14
|
/** Default for `protected.purpose` on body messages. */
|
package/dist/types/body.d.ts
CHANGED
|
@@ -238,11 +238,117 @@ export interface DisputeContent {
|
|
|
238
238
|
evidence_refs?: string[];
|
|
239
239
|
response_text?: string;
|
|
240
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* `settlement_signature` — payee's settlement-key signature on a
|
|
243
|
+
* release-digest, sent as a STAND-ALONE envelope AFTER the matching
|
|
244
|
+
* receipt-propose envelope is committed.
|
|
245
|
+
*
|
|
246
|
+
* Why a separate envelope (and not an attachment on receipt-propose)?
|
|
247
|
+
* The release digest (`buildReleaseDigest` / `buildPartialReleaseDigest`
|
|
248
|
+
* in `packages/sdk/src/settlement/settlement.ts`) takes `receiptEventHash`
|
|
249
|
+
* as one of its inputs. `receipt_event_hash` is the SERVER-ASSIGNED
|
|
250
|
+
* hash of the receipt envelope — it is computed only AFTER server-side
|
|
251
|
+
* validation completes (it includes server-assigned `prev_server_event_hash`
|
|
252
|
+
* + `server_timestamp` in its canonical input). The payee CANNOT
|
|
253
|
+
* compute the digest at receipt-propose time because the digest input
|
|
254
|
+
* does not yet exist. So the wire flow is necessarily two envelopes:
|
|
255
|
+
*
|
|
256
|
+
* 1. Payee sends `receipt` (propose) — server commits + returns
|
|
257
|
+
* `IngestResultDto.serverEventHash`.
|
|
258
|
+
* 2. Payee reads `serverEventHash`, computes the release digest,
|
|
259
|
+
* signs it, and sends THIS `settlement_signature` envelope.
|
|
260
|
+
* 3. Buyer reads the `settlement_signature` envelope from inbox SSE
|
|
261
|
+
* (sender-recipient routing is standard), then assembles the
|
|
262
|
+
* `attachments.settlement_signatures` block on the cosign envelope.
|
|
263
|
+
*
|
|
264
|
+
* This body type carries ONLY the payee side. The payer's settlement
|
|
265
|
+
* sig still lands on the cosign envelope's `attachments` block — that
|
|
266
|
+
* stays unchanged. The server's handler for `settlement_signature`
|
|
267
|
+
* also performs Ed25519 verification of the payee sig against the
|
|
268
|
+
* reconstructed digest as defence-in-depth: a bad sig fails loudly on
|
|
269
|
+
* the worker side instead of stranding the buyer's cosign later.
|
|
270
|
+
*
|
|
271
|
+
* Refund flow is NOT served by this body type — `buildRefundDigest`
|
|
272
|
+
* does NOT bind to `receipt_event_hash` (it binds to lock-derived
|
|
273
|
+
* fields only), so refund signatures can be computed independent of
|
|
274
|
+
* any envelope timing and ride their own dedicated paths. The
|
|
275
|
+
* `purpose` field below is intentionally narrowed to RELEASE +
|
|
276
|
+
* PARTIAL_RELEASE for the same reason — REFUND-v1 sigs do not belong
|
|
277
|
+
* on this envelope.
|
|
278
|
+
*/
|
|
279
|
+
export interface SettlementSignatureBody extends Body<SettlementSignatureContent> {
|
|
280
|
+
type: 'settlement_signature';
|
|
281
|
+
}
|
|
282
|
+
export interface SettlementSignatureContent {
|
|
283
|
+
/** The delegation whose receipt is being settlement-signed. */
|
|
284
|
+
delegation_id: string;
|
|
285
|
+
/**
|
|
286
|
+
* Server-assigned hash of the receipt-propose envelope this signature
|
|
287
|
+
* settles. Must match `Receipt.receiptEventHash` server-side; that is
|
|
288
|
+
* the value the server's cosign-validator feeds into the digest
|
|
289
|
+
* reconstruction as `receiptEventHash`.
|
|
290
|
+
*/
|
|
291
|
+
receipt_event_hash: Sha256Hex;
|
|
292
|
+
/**
|
|
293
|
+
* Which digest the `sig` was computed over. Narrowed to RELEASE +
|
|
294
|
+
* PARTIAL_RELEASE: REFUND-v1 sigs ride other paths because the
|
|
295
|
+
* refund digest is independent of `receipt_event_hash` and never
|
|
296
|
+
* needs this envelope as a transport.
|
|
297
|
+
*/
|
|
298
|
+
purpose: 'ARP-SOLANA-RELEASE-v1.5' | 'ARP-SOLANA-PARTIAL-RELEASE-v1.5';
|
|
299
|
+
/**
|
|
300
|
+
* Payee's settlement-key public key (base58, 32 bytes raw). The
|
|
301
|
+
* server cross-checks this against the registered settlement key on
|
|
302
|
+
* the payee's DID document before re-verifying the sig — a sig
|
|
303
|
+
* signed by an unregistered key is rejected even if it
|
|
304
|
+
* mathematically verifies.
|
|
305
|
+
*/
|
|
306
|
+
payee_settlement_pubkey: string;
|
|
307
|
+
/**
|
|
308
|
+
* Ed25519 sig over the reconstructed release/partial-release digest,
|
|
309
|
+
* **raw base64 — NO `ed25519:` prefix.** Wire format intentionally
|
|
310
|
+
* mismatches the SDK's prefixed `Ed25519Sig` type because the buyer
|
|
311
|
+
* forwards this exact string verbatim into
|
|
312
|
+
* `attachments.settlement_signatures.payee.sig` at cosign time, and
|
|
313
|
+
* the server's `ReceiptCosignValidatorService.parseBase64Sig`
|
|
314
|
+
* (`apps/arp-server/src/escrow/services/receipt-cosign-validator.service.ts:557-565`)
|
|
315
|
+
* `Buffer.from(input, 'base64')` directly — no prefix stripping.
|
|
316
|
+
* Aligns with the CLI's `--payer-settlement-sig <base64>` /
|
|
317
|
+
* `--payee-settlement-sig <base64>` flags that consumers already
|
|
318
|
+
* wire to (see `packages/cli/src/commands/receipt.ts:670-673`).
|
|
319
|
+
*
|
|
320
|
+
* Server re-builds the same digest from lock + receipt + envelope
|
|
321
|
+
* content and verifies this sig as defence-in-depth at handler time
|
|
322
|
+
* — a malformed or wrong-digest sig is rejected pre-projection so
|
|
323
|
+
* the bad receipt sig never reaches the buyer's cosign attempt.
|
|
324
|
+
*/
|
|
325
|
+
sig: string;
|
|
326
|
+
/**
|
|
327
|
+
* Unix seconds — the `expires_at` value the payee bound into the
|
|
328
|
+
* digest. Server re-uses it when reconstructing the digest and
|
|
329
|
+
* cross-checks against `expires_at > now` + `expires_at <=
|
|
330
|
+
* lock.expiry - DISPUTE_BUFFER_SECONDS`. The payer
|
|
331
|
+
* echoes the SAME value on the cosign envelope's settlement_signatures
|
|
332
|
+
* block so both sides sign the same bytes.
|
|
333
|
+
*/
|
|
334
|
+
expires_at: number;
|
|
335
|
+
/**
|
|
336
|
+
* Base-unit decimal-integer string — required when `purpose ===
|
|
337
|
+
* 'ARP-SOLANA-PARTIAL-RELEASE-v1.5'`, omitted (or undefined) for
|
|
338
|
+
* full RELEASE. The server cross-checks it against
|
|
339
|
+
* `receipt.usage.computed_amount` for usage_based contracts before
|
|
340
|
+
* verifying the digest (same invariant
|
|
341
|
+
* `ESC_USAGE_COMPUTED_AMOUNT_MISMATCH` already guards on the
|
|
342
|
+
* propose-time receipt body).
|
|
343
|
+
*/
|
|
344
|
+
payee_amount?: string;
|
|
345
|
+
[extra: string]: unknown;
|
|
346
|
+
}
|
|
241
347
|
/**
|
|
242
348
|
* Union over every standard body type. Consumers can narrow on
|
|
243
349
|
* `body.type` via discriminated dispatch.
|
|
244
350
|
*/
|
|
245
|
-
export type AnyBody = HandshakeBody | HandshakeResponseBody | ContractBody | DelegationBody | WorkRequestBody | WorkResponseBody | ReceiptBody | MemoryDeltaBody | DisputeBody;
|
|
351
|
+
export type AnyBody = HandshakeBody | HandshakeResponseBody | ContractBody | DelegationBody | WorkRequestBody | WorkResponseBody | ReceiptBody | MemoryDeltaBody | DisputeBody | SettlementSignatureBody;
|
|
246
352
|
/** Receipt co-signature payload — what gets `payload_hash`'d in `attachments.co_signature`. */
|
|
247
353
|
export interface ReceiptCosignPayload {
|
|
248
354
|
purpose: 'ARP-RECEIPT-v1';
|
package/dist/types/envelope.d.ts
CHANGED
|
@@ -97,7 +97,22 @@ export interface SettlementSignatures {
|
|
|
97
97
|
}
|
|
98
98
|
export interface SettlementParty {
|
|
99
99
|
settlement_pubkey: string;
|
|
100
|
-
|
|
100
|
+
/**
|
|
101
|
+
* Ed25519 signature, **raw base64 — NO `ed25519:` prefix.** Wire
|
|
102
|
+
* format intentionally mismatches the prefixed `Ed25519Sig` type
|
|
103
|
+
* because this block is consumed by the Solana Ed25519Program at
|
|
104
|
+
* release-tx-builder time, where prefixed strings would break. The
|
|
105
|
+
* server's `ReceiptCosignValidatorService.parseBase64Sig`
|
|
106
|
+
* (`apps/arp-server/src/escrow/services/receipt-cosign-validator.service.ts:557-565`)
|
|
107
|
+
* decodes 64 bytes directly via `Buffer.from(input, 'base64')`
|
|
108
|
+
* without any prefix stripping. The CLI's
|
|
109
|
+
* `heyarp wallet sign-settlement-release` already emits raw base64
|
|
110
|
+
* (`packages/cli/src/commands/wallet.ts:1718-1722`). Matches the
|
|
111
|
+
* raw-base64 shape of `SettlementSignatureContent.sig` — divergent
|
|
112
|
+
* types here would leave buyers unable to copy the value verbatim
|
|
113
|
+
* into the cosign attachment.
|
|
114
|
+
*/
|
|
115
|
+
sig: string;
|
|
101
116
|
}
|
|
102
117
|
/** Top-level envelope as it appears on the wire. */
|
|
103
118
|
export interface Envelope<TBody extends Body = Body> {
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type { Sha256Hex, Ed25519Sig, Did, ProtectedBlock, Body, Attachments, CoSignature, SettlementSignatures, SettlementParty, EscrowLockAttachment, Envelope, PersistedEvent, } from './envelope';
|
|
2
|
-
export type { HandshakeBody, HandshakeContent, HandshakeResponseBody, HandshakeResponseContent, ContractBody, ContractContent, DelegationBody, DelegationContent, WorkRequestBody, WorkRequestContent, WorkResponseBody, WorkResponseContent, ReceiptBody, ReceiptContent, MemoryDeltaBody, MemoryDeltaContent, DisputeBody, DisputeContent, AnyBody, ReceiptCosignPayload, DisputeResponseCosignPayload, CosignPayload, DeclineReason, AssetIdentifier, } from './body';
|
|
2
|
+
export type { HandshakeBody, HandshakeContent, HandshakeResponseBody, HandshakeResponseContent, ContractBody, ContractContent, DelegationBody, DelegationContent, WorkRequestBody, WorkRequestContent, WorkResponseBody, WorkResponseContent, ReceiptBody, ReceiptContent, MemoryDeltaBody, MemoryDeltaContent, DisputeBody, DisputeContent, SettlementSignatureBody, SettlementSignatureContent, AnyBody, ReceiptCosignPayload, DisputeResponseCosignPayload, CosignPayload, DeclineReason, AssetIdentifier, } from './body';
|
|
3
3
|
export { DECLINE_REASONS, isDeclineReason } from './body';
|
|
4
4
|
export type { OwnerSigningMethod, KeyLinkPayload, KeyRotationPayload, ScryptPasswordAttestation } from './identity';
|
|
5
5
|
export { SCRYPT_PARAMS } from './identity';
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic polling helper for SDK consumers that need to block until
|
|
3
|
+
* an FSM transition lands.
|
|
4
|
+
*
|
|
5
|
+
* The SDK is wire-format-only — it has no HTTP client and no
|
|
6
|
+
* awareness of the relationship/delegation/receipt state-machine
|
|
7
|
+
* strings. Callers bring their own `fetch` function (axios, native
|
|
8
|
+
* fetch, custom transport) and their own predicate; this helper
|
|
9
|
+
* supplies the polling discipline (interval, timeout, cancellable
|
|
10
|
+
* AbortSignal, exponential backoff on transient errors).
|
|
11
|
+
*
|
|
12
|
+
* Why this is useful enough to ship even without a state-machine
|
|
13
|
+
* type in the SDK: third-party bot authors today write the loop
|
|
14
|
+
* themselves and routinely get the discipline wrong — they tight-loop
|
|
15
|
+
* the RPC, ignore timeouts, swallow errors that should propagate, or
|
|
16
|
+
* miss state transitions that landed between polls. A 40-LOC
|
|
17
|
+
* primitive with documented behaviour is worth more than the
|
|
18
|
+
* duplicated `setTimeout` chains it replaces.
|
|
19
|
+
*
|
|
20
|
+
* Pairs with the CLI's `--wait-until <phase>` flag on FSM-advancing
|
|
21
|
+
* commands: same polling shape, same exit-on-predicate semantics,
|
|
22
|
+
* ported to the API-client layer for callers who don't shell out to
|
|
23
|
+
* `heyarp`. Either path can be skipped — `heyarp status <rel-id>
|
|
24
|
+
* --wait --until <phase>` remains the documented escape hatch.
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* Outcome of a `pollUntil` call.
|
|
28
|
+
*
|
|
29
|
+
* - `{ kind: 'matched', value }` — predicate returned true; `value`
|
|
30
|
+
* is the matched fetcher return.
|
|
31
|
+
* - `{ kind: 'timeout', last }` — wall-clock exceeded `timeoutMs`;
|
|
32
|
+
* `last` is the most recent fetcher return (which DID NOT match the
|
|
33
|
+
* predicate) for the caller's diagnostic / retry decision.
|
|
34
|
+
* - `{ kind: 'aborted', last }` — `abortSignal.aborted` became true;
|
|
35
|
+
* `last` is the most recent fetcher return, or `undefined` if the
|
|
36
|
+
* abort fired before the first fetch landed.
|
|
37
|
+
*
|
|
38
|
+
* Discriminated union (`kind` literal) so consumers can `switch`
|
|
39
|
+
* exhaustively. No exceptions are thrown for the timeout/abort
|
|
40
|
+
* outcomes — they're modelled as values so the caller's branching
|
|
41
|
+
* stays explicit. Fetcher errors are a different story: they bubble
|
|
42
|
+
* unless `swallowFetchErrors` is true.
|
|
43
|
+
*/
|
|
44
|
+
export type PollUntilResult<T> = {
|
|
45
|
+
kind: 'matched';
|
|
46
|
+
value: T;
|
|
47
|
+
} | {
|
|
48
|
+
kind: 'timeout';
|
|
49
|
+
last: T | undefined;
|
|
50
|
+
} | {
|
|
51
|
+
kind: 'aborted';
|
|
52
|
+
last: T | undefined;
|
|
53
|
+
};
|
|
54
|
+
export interface PollUntilOptions<T> {
|
|
55
|
+
/**
|
|
56
|
+
* Async fetcher invoked once per poll tick. Should return the
|
|
57
|
+
* domain object the predicate evaluates against. Implementations
|
|
58
|
+
* typically read the same row the CLI's `composeStatus` reads —
|
|
59
|
+
* a single signed-GET against the relationship's entities is
|
|
60
|
+
* common.
|
|
61
|
+
*/
|
|
62
|
+
fetch: () => Promise<T>;
|
|
63
|
+
/**
|
|
64
|
+
* Returns `true` when `value` satisfies the wait condition. The
|
|
65
|
+
* helper exits with `{ kind: 'matched', value }` on the first
|
|
66
|
+
* `true`. Predicate exceptions bubble (treat as a fail-fast
|
|
67
|
+
* programmer error — the predicate shouldn't be side-effectful or
|
|
68
|
+
* I/O-bound).
|
|
69
|
+
*/
|
|
70
|
+
predicate: (value: T) => boolean;
|
|
71
|
+
/** Milliseconds between fetches. Defaults to 3000 (matches CLI WAIT_DEFAULT_INTERVAL_SEC). Lower bound is 100ms to avoid tight-loop accidents. */
|
|
72
|
+
intervalMs?: number;
|
|
73
|
+
/** Wall-clock budget in milliseconds. Defaults to 300000 (5 min). On exceed, returns `{ kind: 'timeout', last }`. */
|
|
74
|
+
timeoutMs?: number;
|
|
75
|
+
/**
|
|
76
|
+
* If true, fetch errors are caught and logged via `onFetchError`
|
|
77
|
+
* (or silently swallowed when that's absent); the loop continues
|
|
78
|
+
* to the next tick. If false (default), the first fetch error
|
|
79
|
+
* rejects the returned promise — appropriate for "fail loud on
|
|
80
|
+
* the first network blip" scripts.
|
|
81
|
+
*/
|
|
82
|
+
swallowFetchErrors?: boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Called once per fetch error when `swallowFetchErrors` is true.
|
|
85
|
+
* Gets the raw error so the caller can log / sample / surface to
|
|
86
|
+
* an observability sink without coupling the polling primitive to
|
|
87
|
+
* any particular logger.
|
|
88
|
+
*/
|
|
89
|
+
onFetchError?: (err: unknown) => void;
|
|
90
|
+
/**
|
|
91
|
+
* Cancellation signal — when its `aborted` flag flips, the
|
|
92
|
+
* helper resolves with `{ kind: 'aborted', last }` after the
|
|
93
|
+
* current sleep completes (or immediately if currently mid-sleep
|
|
94
|
+
* and the signal supports listener-driven abort). The fetcher
|
|
95
|
+
* itself is not abort-aware here; if it needs to be, the caller
|
|
96
|
+
* should pass the same signal into its own implementation.
|
|
97
|
+
*/
|
|
98
|
+
abortSignal?: AbortSignal;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Poll `fetch` until `predicate` returns true, the timeout fires, or
|
|
102
|
+
* the optional `abortSignal` aborts. Returns a discriminated outcome
|
|
103
|
+
* (no exceptions for the expected paths).
|
|
104
|
+
*
|
|
105
|
+
* Behaviour notes:
|
|
106
|
+
* - The first fetch happens immediately (no leading sleep). If the
|
|
107
|
+
* predicate matches on the first call, returns without any sleep
|
|
108
|
+
* — useful when the caller already knows the state may have
|
|
109
|
+
* transitioned in the background.
|
|
110
|
+
* - Sleeps are clamped to the remaining budget; the final sleep
|
|
111
|
+
* will be shorter than `intervalMs` when timeout is near.
|
|
112
|
+
* - Lower-bound `intervalMs` to 100ms. Higher-frequency polling is
|
|
113
|
+
* an accident; if you really need it, build your own loop with
|
|
114
|
+
* direct `setTimeout` calls.
|
|
115
|
+
*/
|
|
116
|
+
export declare function pollUntil<T>(opts: PollUntilOptions<T>): Promise<PollUntilResult<T>>;
|
package/dist/webhook/index.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
export { buildWebhookSignatureHeader, verifyWebhookSignatureHeader } from './webhook';
|
|
2
|
-
export type { WebhookSignableInput } from './webhook';
|
|
1
|
+
export { buildWebhookSignatureHeader, isProbeRequest, sha256HexLower, verifyAndCheckPayload, verifyWebhookSignatureHeader } from './webhook';
|
|
2
|
+
export type { WebhookSignableInput, WebhookSignableSource } from './webhook';
|
|
3
|
+
export type { ChainEventRecoveryRow, ChainEventRecoveryRowCommon, ChainPayloadCommon, ChainWebhookPayload, DisputeResolvedWebhookPayload, EnvelopeWebhookPayload, LockCreatedWebhookPayload, LockPartiallyReleasedWebhookPayload, LockRefundedWebhookPayload, LockReleasedWebhookPayload, WebhookPayload, } from './payload-types';
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SDK consumer types for the on-the-wire body the OutboxDeliveryWorker
|
|
3
|
+
* POSTs to a recipient's `webhookUrl`. Mirrors the JSON shape the
|
|
4
|
+
* server's `OutboxDeliveryWorkerService.deliver` builds.
|
|
5
|
+
*
|
|
6
|
+
* The body is a discriminated union on `event_type`:
|
|
7
|
+
*
|
|
8
|
+
* - `envelope.<type>` → `payload` is the envelope `EventPublic`
|
|
9
|
+
* shape (eventId, messageId, sender/recipient DIDs, protected
|
|
10
|
+
* block, body, attachments, server-derived hashes).
|
|
11
|
+
* - `escrow.<event>` → `payload` is the per-event-type chain shape
|
|
12
|
+
* (delegation/lock IDs, amounts, verdict, etc.).
|
|
13
|
+
*
|
|
14
|
+
* Recipient TypeScript consumers `switch (payload.event_type)` to
|
|
15
|
+
* branch exhaustively without `default` — drift on either side
|
|
16
|
+
* surfaces as a missing-case compile error.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* What the server-side `events` collection row looks like on the wire
|
|
20
|
+
* after `buildEnvelopeDeliveryPayload` formatting. Recipients see the
|
|
21
|
+
* full canonical envelope so they can re-verify the signature locally
|
|
22
|
+
* with the SDK (same call they'd make on `/inbox?since=` rows).
|
|
23
|
+
*
|
|
24
|
+
* Body shape varies by `type` — keep `body` open (the SDK's
|
|
25
|
+
* type-narrowing for each `BodyType` lives in
|
|
26
|
+
* `packages/sdk/src/envelope/*` and is consumed when the recipient
|
|
27
|
+
* branches on body.type).
|
|
28
|
+
*/
|
|
29
|
+
export interface EnvelopeWebhookPayload {
|
|
30
|
+
event_type: `envelope.${string}`;
|
|
31
|
+
delivery_id: string;
|
|
32
|
+
attempt_n: number;
|
|
33
|
+
served_at: string;
|
|
34
|
+
payload: {
|
|
35
|
+
eventId: string;
|
|
36
|
+
messageId: string;
|
|
37
|
+
senderDid: string;
|
|
38
|
+
recipientDid: string;
|
|
39
|
+
relationshipId: string;
|
|
40
|
+
senderSequence: number;
|
|
41
|
+
protocolVersion: string;
|
|
42
|
+
purpose: string;
|
|
43
|
+
type: string;
|
|
44
|
+
protectedBlock: Record<string, unknown>;
|
|
45
|
+
body: Record<string, unknown>;
|
|
46
|
+
attachments?: Record<string, unknown>;
|
|
47
|
+
senderSignature: string;
|
|
48
|
+
relationshipEventIndex: number;
|
|
49
|
+
prevServerEventHash: string | null;
|
|
50
|
+
serverTimestamp: string;
|
|
51
|
+
signedMessageHash: string;
|
|
52
|
+
serverEventHash: string;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Fields every chain-source payload carries. `chain_event_id` +
|
|
57
|
+
* `instruction_idx` let the recipient reconstruct
|
|
58
|
+
* `WebhookSignableInput.source` for HMAC verification from the body
|
|
59
|
+
* alone — no reverse-parse of `delivery_id` required.
|
|
60
|
+
*/
|
|
61
|
+
export interface ChainPayloadCommon {
|
|
62
|
+
chain_event_id: string;
|
|
63
|
+
instruction_idx: number;
|
|
64
|
+
delegation_id: string;
|
|
65
|
+
relationship_id: string;
|
|
66
|
+
payer_did: string;
|
|
67
|
+
payee_did: string;
|
|
68
|
+
tx_signature: string;
|
|
69
|
+
slot: number;
|
|
70
|
+
block_time_iso?: string;
|
|
71
|
+
lock_id: string;
|
|
72
|
+
}
|
|
73
|
+
export interface LockCreatedWebhookPayload {
|
|
74
|
+
event_type: 'escrow.lock_created';
|
|
75
|
+
delivery_id: string;
|
|
76
|
+
attempt_n: number;
|
|
77
|
+
served_at: string;
|
|
78
|
+
payload: ChainPayloadCommon & {
|
|
79
|
+
amount: string;
|
|
80
|
+
asset_id?: string;
|
|
81
|
+
expiry_unix?: number;
|
|
82
|
+
fee_bps?: number;
|
|
83
|
+
fee_recipient?: string;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
export interface LockReleasedWebhookPayload {
|
|
87
|
+
event_type: 'escrow.lock_released';
|
|
88
|
+
delivery_id: string;
|
|
89
|
+
attempt_n: number;
|
|
90
|
+
served_at: string;
|
|
91
|
+
payload: ChainPayloadCommon & {
|
|
92
|
+
released_amount: string;
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
export interface LockPartiallyReleasedWebhookPayload {
|
|
96
|
+
event_type: 'escrow.lock_partially_released';
|
|
97
|
+
delivery_id: string;
|
|
98
|
+
attempt_n: number;
|
|
99
|
+
served_at: string;
|
|
100
|
+
payload: ChainPayloadCommon & {
|
|
101
|
+
released_amount: string;
|
|
102
|
+
refunded_amount: string;
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
export interface LockRefundedWebhookPayload {
|
|
106
|
+
event_type: 'escrow.lock_refunded';
|
|
107
|
+
delivery_id: string;
|
|
108
|
+
attempt_n: number;
|
|
109
|
+
served_at: string;
|
|
110
|
+
payload: ChainPayloadCommon & {
|
|
111
|
+
refunded_amount: string;
|
|
112
|
+
reason?: string;
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
export interface DisputeResolvedWebhookPayload {
|
|
116
|
+
event_type: 'escrow.dispute_resolved';
|
|
117
|
+
delivery_id: string;
|
|
118
|
+
attempt_n: number;
|
|
119
|
+
served_at: string;
|
|
120
|
+
payload: ChainPayloadCommon & {
|
|
121
|
+
verdict: string;
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Discriminated union of every webhook delivery the recipient might
|
|
126
|
+
* receive. Use exhaustive `switch` on `event_type`:
|
|
127
|
+
*
|
|
128
|
+
* ```ts
|
|
129
|
+
* function handle(p: WebhookPayload) {
|
|
130
|
+
* switch (p.event_type) {
|
|
131
|
+
* case 'escrow.lock_created': return onLockCreated(p);
|
|
132
|
+
* case 'escrow.lock_released': return onLockReleased(p);
|
|
133
|
+
* case 'escrow.lock_partially_released': return onPartial(p);
|
|
134
|
+
* case 'escrow.lock_refunded': return onRefund(p);
|
|
135
|
+
* case 'escrow.dispute_resolved': return onDispute(p);
|
|
136
|
+
* default:
|
|
137
|
+
* // p.event_type is `envelope.${string}` here
|
|
138
|
+
* return onEnvelope(p);
|
|
139
|
+
* }
|
|
140
|
+
* }
|
|
141
|
+
* ```
|
|
142
|
+
*
|
|
143
|
+
* The chain branches narrow exhaustively because each is a string-
|
|
144
|
+
* literal type; the envelope branch carries the catch-all
|
|
145
|
+
* `envelope.${string}` template-literal type so recipients can
|
|
146
|
+
* still narrow on `payload.type` if they care about specific body
|
|
147
|
+
* types.
|
|
148
|
+
*/
|
|
149
|
+
export type ChainWebhookPayload = LockCreatedWebhookPayload | LockReleasedWebhookPayload | LockPartiallyReleasedWebhookPayload | LockRefundedWebhookPayload | DisputeResolvedWebhookPayload;
|
|
150
|
+
export type WebhookPayload = EnvelopeWebhookPayload | ChainWebhookPayload;
|
|
151
|
+
/**
|
|
152
|
+
* Fields every recovery row carries irrespective of event type — the
|
|
153
|
+
* envelope around the per-event-type `payload`.
|
|
154
|
+
*/
|
|
155
|
+
export interface ChainEventRecoveryRowCommon {
|
|
156
|
+
chain_event_id: string;
|
|
157
|
+
delegation_id: string;
|
|
158
|
+
relationship_id: string;
|
|
159
|
+
lock_id: string;
|
|
160
|
+
tx_signature: string;
|
|
161
|
+
slot: number;
|
|
162
|
+
block_time_iso?: string;
|
|
163
|
+
created_at: string;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Recovery channel for chain events. Returned by the
|
|
167
|
+
* `GET /v1/agents/me/chain-events?since=…` endpoint. Recipients call
|
|
168
|
+
* this on handler restart / reconnection to catch any chain events
|
|
169
|
+
* they missed while their handler was down.
|
|
170
|
+
*
|
|
171
|
+
* **Discriminated union**: both `event_type` and `payload` come from
|
|
172
|
+
* the SAME tagged union — `switch (row.event_type)` narrows
|
|
173
|
+
* `row.payload` to the right per-event-type shape so the advertised
|
|
174
|
+
* "share a single handler between push and pull" actually type-checks.
|
|
175
|
+
*
|
|
176
|
+
* switch (row.event_type) {
|
|
177
|
+
* case 'escrow.lock_released':
|
|
178
|
+
* // row.payload is LockReleasedWebhookPayload['payload'] here
|
|
179
|
+
* break;
|
|
180
|
+
* ...
|
|
181
|
+
* }
|
|
182
|
+
*/
|
|
183
|
+
export type ChainEventRecoveryRow = ChainEventRecoveryRowCommon & ({
|
|
184
|
+
event_type: 'escrow.lock_created';
|
|
185
|
+
payload: LockCreatedWebhookPayload['payload'];
|
|
186
|
+
} | {
|
|
187
|
+
event_type: 'escrow.lock_released';
|
|
188
|
+
payload: LockReleasedWebhookPayload['payload'];
|
|
189
|
+
} | {
|
|
190
|
+
event_type: 'escrow.lock_partially_released';
|
|
191
|
+
payload: LockPartiallyReleasedWebhookPayload['payload'];
|
|
192
|
+
} | {
|
|
193
|
+
event_type: 'escrow.lock_refunded';
|
|
194
|
+
payload: LockRefundedWebhookPayload['payload'];
|
|
195
|
+
} | {
|
|
196
|
+
event_type: 'escrow.dispute_resolved';
|
|
197
|
+
payload: DisputeResolvedWebhookPayload['payload'];
|
|
198
|
+
});
|
|
@@ -2,37 +2,157 @@
|
|
|
2
2
|
* `ARP-WEBHOOK-v1` HMAC over the canonical webhook envelope. Used by
|
|
3
3
|
* the OutboxDeliveryWorker for the `X-ARP-Signature` header.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
5
|
+
* The signable input is a discriminated `source` union covering BOTH
|
|
6
|
+
* envelope deliveries (`source.kind === 'envelope'`) AND chain
|
|
7
|
+
* settlement events (`source.kind === 'chain'`). The single
|
|
8
|
+
* canonical-JSON output binds the HMAC to the source variant so a
|
|
9
|
+
* recipient receiving a chain webhook can't accidentally verify
|
|
10
|
+
* against an envelope-shaped signable, and vice versa.
|
|
11
|
+
*
|
|
12
|
+
* The body-tampering defence (`payload_sha256`) is mandatory on the
|
|
13
|
+
* new shape — recipients SHOULD verify the hash matches their
|
|
14
|
+
* received raw body BEFORE the HMAC check (cheap, and surfaces
|
|
15
|
+
* "wrong body" as a separate diagnostic from "wrong HMAC").
|
|
16
16
|
*/
|
|
17
|
+
export type WebhookSignableSource = {
|
|
18
|
+
kind: 'envelope';
|
|
19
|
+
envelope_message_id: string;
|
|
20
|
+
server_event_hash: string;
|
|
21
|
+
} | {
|
|
22
|
+
kind: 'chain';
|
|
23
|
+
chain_event_id: string;
|
|
24
|
+
tx_signature: string;
|
|
25
|
+
instruction_idx: number;
|
|
26
|
+
};
|
|
17
27
|
export interface WebhookSignableInput {
|
|
28
|
+
/** Outbox row id (deterministic per delivery attempt). */
|
|
18
29
|
delivery_id: string;
|
|
19
30
|
recipient_did: string;
|
|
20
|
-
|
|
21
|
-
|
|
31
|
+
/** Public event-type discriminator (`envelope.<type>` or `escrow.<event>`). */
|
|
32
|
+
event_type: string;
|
|
33
|
+
/** Source-variant discriminated union. Binds the HMAC to the kind. */
|
|
34
|
+
source: WebhookSignableSource;
|
|
35
|
+
/** SHA-256 (lowercase hex, no `sha256:` prefix) of the canonical raw body bytes the recipient receives. */
|
|
36
|
+
payload_sha256: string;
|
|
37
|
+
/** 1-based retry counter — participates in canonical input so replays of an old header on a fresh attempt fail. */
|
|
22
38
|
attempt_n: number;
|
|
39
|
+
/** RFC 3339 wall-clock time the server generated this attempt. Recipient policy MAY reject outside a tolerance window. */
|
|
23
40
|
served_at: string;
|
|
24
41
|
}
|
|
25
42
|
/**
|
|
26
|
-
* Compute `X-ARP-Signature` value:
|
|
43
|
+
* Compute `X-ARP-Signature` value:
|
|
44
|
+
* `<purpose>=<base64(HMAC-SHA256(secret, sha256(canonical_json(input))))>`.
|
|
27
45
|
*
|
|
28
|
-
* Recipients lookup their per-sender shared secret, recompute the
|
|
46
|
+
* Recipients lookup their per-sender shared secret(s), recompute the
|
|
29
47
|
* HMAC over the same canonical input, and compare against the
|
|
30
|
-
* received header.
|
|
48
|
+
* received header. See `verifyWebhookSignatureHeader` for the
|
|
49
|
+
* standard recipient flow with rotation grace.
|
|
31
50
|
*/
|
|
32
51
|
export declare function buildWebhookSignatureHeader(input: WebhookSignableInput, sharedSecret: Uint8Array): string;
|
|
33
52
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
53
|
+
* Verify an inbound `X-ARP-Signature` header against the reconstructed
|
|
54
|
+
* signable input. The 3-argument form is the convenience for
|
|
55
|
+
* single-secret consumers (most use); the array form is the rotation-
|
|
56
|
+
* grace path (two-phase rotate: recipient verifies against
|
|
57
|
+
* `[current, pending, previous]` during the 1h grace window).
|
|
58
|
+
*
|
|
59
|
+
* Returns `false` on shape mismatch, missing purpose label, OR HMAC
|
|
60
|
+
* mismatch under EVERY supplied secret. Never throws.
|
|
61
|
+
*
|
|
62
|
+
* **Timing-uniform across rotation slots** — the array form runs ALL
|
|
63
|
+
* HMAC computations + constant-time compares before returning, so the
|
|
64
|
+
* total time is proportional to N (number of secrets) regardless of
|
|
65
|
+
* which slot matched. A short-circuiting implementation would leak
|
|
66
|
+
* the position of the matching key via total time. Practical impact
|
|
67
|
+
* is small (attacker would only learn "current vs pending vs previous
|
|
68
|
+
* matched", not key material), but timing-uniform is the right
|
|
69
|
+
* default for a verification primitive.
|
|
70
|
+
*
|
|
71
|
+
* Recipient flow:
|
|
72
|
+
* 1. Receive POST with `X-ARP-Signature` header + raw body bytes.
|
|
73
|
+
* 2. Compute `payload_sha256 = sha256-hex(rawBodyBytes)`.
|
|
74
|
+
* 3. Reconstruct `WebhookSignableInput` from headers + payload sha256.
|
|
75
|
+
* 4. Call `verifyWebhookSignatureHeader(header, input, [current, pending, previous].filter(Boolean))`.
|
|
76
|
+
* 5. On `true`, accept. On `false`, 401 the request.
|
|
77
|
+
*
|
|
78
|
+
* Helper `verifyAndCheckPayload` (below) bundles steps 2-4 into one
|
|
79
|
+
* call for the common path.
|
|
80
|
+
*/
|
|
81
|
+
export declare function verifyWebhookSignatureHeader(headerValue: string, input: WebhookSignableInput, sharedSecret: Uint8Array | Uint8Array[]): boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Recipient convenience: combine body-hash binding + HMAC verify +
|
|
84
|
+
* optional `served_at` clock-skew tolerance into one call.
|
|
85
|
+
*
|
|
86
|
+
* 1. Compute `payload_sha256 = sha256-hex(rawBody)` and OVERLAY it
|
|
87
|
+
* onto `input.payload_sha256` (caller's value, if any, is
|
|
88
|
+
* replaced).
|
|
89
|
+
* 2. Verify `served_at` is within `toleranceMs` of `now()` (default
|
|
90
|
+
* ±5 minutes; pass `null` to skip).
|
|
91
|
+
* 3. Verify HMAC against each secret in `secrets`.
|
|
92
|
+
*
|
|
93
|
+
* Returns a tagged outcome the caller can branch on:
|
|
94
|
+
* - `'ok'` — accept the delivery
|
|
95
|
+
* - `'stale_attempt'` — served_at outside the tolerance window
|
|
96
|
+
* - `'invalid_signature'` — HMAC didn't verify under any secret
|
|
97
|
+
*
|
|
98
|
+
* **Why no `body_mismatch` outcome** — `payload_sha256` is a derived
|
|
99
|
+
* field bound to the raw body. The HMAC signs the input INCLUDING
|
|
100
|
+
* `payload_sha256`, so a tampered body causes the recipient's
|
|
101
|
+
* recomputed hash to differ from the server-signed one, which fails
|
|
102
|
+
* the HMAC check as `'invalid_signature'`. Body tampering is
|
|
103
|
+
* detected via the HMAC failure; if you need a dedicated diagnostic,
|
|
104
|
+
* compute `sha256HexLower(rawBody)` yourself and compare to an
|
|
105
|
+
* out-of-band trusted source (e.g. a signed mirror of the body).
|
|
106
|
+
*/
|
|
107
|
+
export declare function verifyAndCheckPayload(args: {
|
|
108
|
+
headerValue: string;
|
|
109
|
+
/** Reconstructed signable input MINUS `payload_sha256` — helper computes that field from `rawBody`. */
|
|
110
|
+
input: Omit<WebhookSignableInput, 'payload_sha256'>;
|
|
111
|
+
rawBody: Uint8Array;
|
|
112
|
+
secrets: Uint8Array | Uint8Array[];
|
|
113
|
+
servedAtToleranceMs?: number | null;
|
|
114
|
+
now?: Date;
|
|
115
|
+
}): 'ok' | 'stale_attempt' | 'invalid_signature';
|
|
116
|
+
/**
|
|
117
|
+
* Probe-skip helper for recipient handlers. The `POST /v1/agents/me/webhook-config`
|
|
118
|
+
* flow fires an UNSIGNED probe POST with the `X-ARP-Probe: 1` header.
|
|
119
|
+
* Recipient handlers MUST 2xx without HMAC verification on probe
|
|
120
|
+
* requests — otherwise the operator can't set their webhook URL
|
|
121
|
+
* (the probe gets rejected as unsigned, the URL persist step fails).
|
|
122
|
+
*
|
|
123
|
+
* Use this at the top of your handler:
|
|
124
|
+
*
|
|
125
|
+
* ```ts
|
|
126
|
+
* if (isProbeRequest(req.headers)) {
|
|
127
|
+
* res.statusCode = 200;
|
|
128
|
+
* res.end();
|
|
129
|
+
* return;
|
|
130
|
+
* }
|
|
131
|
+
* // Otherwise: verify signature normally.
|
|
132
|
+
* ```
|
|
133
|
+
*
|
|
134
|
+
* Header values are inspected case-insensitively per Node's
|
|
135
|
+
* IncomingMessage convention.
|
|
136
|
+
*
|
|
137
|
+
* **SECURITY (spoofability)** — the helper is a pure header check;
|
|
138
|
+
* it cannot itself defend against an attacker setting
|
|
139
|
+
* `X-ARP-Probe: 1` to bypass HMAC verification. The probe branch
|
|
140
|
+
* in your handler MUST be side-effect-free:
|
|
141
|
+
*
|
|
142
|
+
* - Respond 2xx with an empty body — no DB write, no downstream
|
|
143
|
+
* fan-out, no metric increment that an attacker could amplify.
|
|
144
|
+
* - Consider rate-limiting probe-skipped requests per source IP if
|
|
145
|
+
* the surrounding stack doesn't already.
|
|
146
|
+
* - Do NOT log full request bodies on the probe branch (an
|
|
147
|
+
* attacker can pump log noise).
|
|
148
|
+
* - If your handler genuinely has zero side-effects on a 200
|
|
149
|
+
* response, this is automatic; otherwise treat the probe branch
|
|
150
|
+
* as you would an unauthenticated health-check endpoint.
|
|
151
|
+
*/
|
|
152
|
+
export declare function isProbeRequest(headers: Record<string, string | string[] | undefined>): boolean;
|
|
153
|
+
/**
|
|
154
|
+
* SHA-256 hex digest of the input bytes, lowercase. Exported for unit
|
|
155
|
+
* tests + the rare consumer that needs to compute payload_sha256
|
|
156
|
+
* outside `verifyAndCheckPayload`.
|
|
37
157
|
*/
|
|
38
|
-
export declare function
|
|
158
|
+
export declare function sha256HexLower(bytes: Uint8Array): string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heyanon-arp/sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "TypeScript SDK for the Agent Relationship Protocol — canonical JSON, Ed25519 envelope sign/verify, did:arp identity, receipt co-signatures, scrypt key attestation, chain-audit helpers.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|