s402 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,48 @@
1
+ //#region src/errors.d.ts
2
+ /**
3
+ * s402 Error Types — typed error codes with recovery hints
4
+ *
5
+ * Every s402 error tells the client:
6
+ * 1. What went wrong (code)
7
+ * 2. Whether it can retry (retryable)
8
+ * 3. What to do about it (suggestedAction)
9
+ */
10
+ declare const s402ErrorCode: {
11
+ readonly INSUFFICIENT_BALANCE: "INSUFFICIENT_BALANCE";
12
+ readonly MANDATE_EXPIRED: "MANDATE_EXPIRED";
13
+ readonly MANDATE_LIMIT_EXCEEDED: "MANDATE_LIMIT_EXCEEDED";
14
+ readonly STREAM_DEPLETED: "STREAM_DEPLETED";
15
+ readonly ESCROW_DEADLINE_PASSED: "ESCROW_DEADLINE_PASSED";
16
+ readonly UNLOCK_DECRYPTION_FAILED: "UNLOCK_DECRYPTION_FAILED";
17
+ readonly FINALITY_TIMEOUT: "FINALITY_TIMEOUT";
18
+ readonly FACILITATOR_UNAVAILABLE: "FACILITATOR_UNAVAILABLE";
19
+ readonly INVALID_PAYLOAD: "INVALID_PAYLOAD";
20
+ readonly SCHEME_NOT_SUPPORTED: "SCHEME_NOT_SUPPORTED";
21
+ readonly NETWORK_MISMATCH: "NETWORK_MISMATCH";
22
+ readonly SIGNATURE_INVALID: "SIGNATURE_INVALID";
23
+ readonly REQUIREMENTS_EXPIRED: "REQUIREMENTS_EXPIRED";
24
+ readonly VERIFICATION_FAILED: "VERIFICATION_FAILED";
25
+ };
26
+ type s402ErrorCodeType = (typeof s402ErrorCode)[keyof typeof s402ErrorCode];
27
+ interface s402ErrorInfo {
28
+ code: s402ErrorCodeType;
29
+ message: string;
30
+ retryable: boolean;
31
+ suggestedAction: string;
32
+ }
33
+ /**
34
+ * Create a typed s402 error with recovery hints.
35
+ */
36
+ declare function createS402Error(code: s402ErrorCodeType, message?: string): s402ErrorInfo;
37
+ /**
38
+ * s402 error class for throwing in code.
39
+ */
40
+ declare class s402Error extends Error {
41
+ readonly code: s402ErrorCodeType;
42
+ readonly retryable: boolean;
43
+ readonly suggestedAction: string;
44
+ constructor(code: s402ErrorCodeType, message?: string);
45
+ toJSON(): s402ErrorInfo;
46
+ }
47
+ //#endregion
48
+ export { createS402Error, s402Error, s402ErrorCode, s402ErrorCodeType, s402ErrorInfo };
@@ -0,0 +1,123 @@
1
+ //#region src/errors.ts
2
+ /**
3
+ * s402 Error Types — typed error codes with recovery hints
4
+ *
5
+ * Every s402 error tells the client:
6
+ * 1. What went wrong (code)
7
+ * 2. Whether it can retry (retryable)
8
+ * 3. What to do about it (suggestedAction)
9
+ */
10
+ const s402ErrorCode = {
11
+ INSUFFICIENT_BALANCE: "INSUFFICIENT_BALANCE",
12
+ MANDATE_EXPIRED: "MANDATE_EXPIRED",
13
+ MANDATE_LIMIT_EXCEEDED: "MANDATE_LIMIT_EXCEEDED",
14
+ STREAM_DEPLETED: "STREAM_DEPLETED",
15
+ ESCROW_DEADLINE_PASSED: "ESCROW_DEADLINE_PASSED",
16
+ UNLOCK_DECRYPTION_FAILED: "UNLOCK_DECRYPTION_FAILED",
17
+ FINALITY_TIMEOUT: "FINALITY_TIMEOUT",
18
+ FACILITATOR_UNAVAILABLE: "FACILITATOR_UNAVAILABLE",
19
+ INVALID_PAYLOAD: "INVALID_PAYLOAD",
20
+ SCHEME_NOT_SUPPORTED: "SCHEME_NOT_SUPPORTED",
21
+ NETWORK_MISMATCH: "NETWORK_MISMATCH",
22
+ SIGNATURE_INVALID: "SIGNATURE_INVALID",
23
+ REQUIREMENTS_EXPIRED: "REQUIREMENTS_EXPIRED",
24
+ VERIFICATION_FAILED: "VERIFICATION_FAILED"
25
+ };
26
+ /** Error recovery hints for each error code */
27
+ const ERROR_HINTS = {
28
+ INSUFFICIENT_BALANCE: {
29
+ retryable: false,
30
+ suggestedAction: "Top up wallet balance or try with a smaller amount"
31
+ },
32
+ MANDATE_EXPIRED: {
33
+ retryable: false,
34
+ suggestedAction: "Request a new mandate from the delegator"
35
+ },
36
+ MANDATE_LIMIT_EXCEEDED: {
37
+ retryable: false,
38
+ suggestedAction: "Request mandate increase or split across transactions"
39
+ },
40
+ STREAM_DEPLETED: {
41
+ retryable: true,
42
+ suggestedAction: "Top up the stream deposit"
43
+ },
44
+ ESCROW_DEADLINE_PASSED: {
45
+ retryable: false,
46
+ suggestedAction: "Create a new escrow with a later deadline"
47
+ },
48
+ UNLOCK_DECRYPTION_FAILED: {
49
+ retryable: true,
50
+ suggestedAction: "Re-request decryption key with a fresh session key"
51
+ },
52
+ FINALITY_TIMEOUT: {
53
+ retryable: true,
54
+ suggestedAction: "Transaction submitted but not confirmed — retry finality check"
55
+ },
56
+ FACILITATOR_UNAVAILABLE: {
57
+ retryable: true,
58
+ suggestedAction: "Fall back to direct settlement if signer is available"
59
+ },
60
+ INVALID_PAYLOAD: {
61
+ retryable: false,
62
+ suggestedAction: "Check payload format and re-sign the transaction"
63
+ },
64
+ SCHEME_NOT_SUPPORTED: {
65
+ retryable: false,
66
+ suggestedAction: "Use the \"exact\" scheme (always supported for x402 compat)"
67
+ },
68
+ NETWORK_MISMATCH: {
69
+ retryable: false,
70
+ suggestedAction: "Ensure client and server are on the same Sui network"
71
+ },
72
+ SIGNATURE_INVALID: {
73
+ retryable: false,
74
+ suggestedAction: "Re-sign the transaction with the correct keypair"
75
+ },
76
+ REQUIREMENTS_EXPIRED: {
77
+ retryable: true,
78
+ suggestedAction: "Re-fetch payment requirements from the server"
79
+ },
80
+ VERIFICATION_FAILED: {
81
+ retryable: false,
82
+ suggestedAction: "Check payment amount and transaction structure"
83
+ }
84
+ };
85
+ /**
86
+ * Create a typed s402 error with recovery hints.
87
+ */
88
+ function createS402Error(code, message) {
89
+ const hints = ERROR_HINTS[code];
90
+ return {
91
+ code,
92
+ message: message ?? code,
93
+ retryable: hints.retryable,
94
+ suggestedAction: hints.suggestedAction
95
+ };
96
+ }
97
+ /**
98
+ * s402 error class for throwing in code.
99
+ */
100
+ var s402Error = class extends Error {
101
+ code;
102
+ retryable;
103
+ suggestedAction;
104
+ constructor(code, message) {
105
+ const info = createS402Error(code, message);
106
+ super(info.message);
107
+ this.name = "s402Error";
108
+ this.code = info.code;
109
+ this.retryable = info.retryable;
110
+ this.suggestedAction = info.suggestedAction;
111
+ }
112
+ toJSON() {
113
+ return {
114
+ code: this.code,
115
+ message: this.message,
116
+ retryable: this.retryable,
117
+ suggestedAction: this.suggestedAction
118
+ };
119
+ }
120
+ };
121
+
122
+ //#endregion
123
+ export { createS402Error, s402Error, s402ErrorCode };
@@ -0,0 +1,67 @@
1
+ import { s402PaymentPayload, s402PaymentRequirements, s402SettleResponse } from "./types.mjs";
2
+
3
+ //#region src/http.d.ts
4
+ /** Encode payment requirements for the `payment-required` header */
5
+ declare function encodePaymentRequired(requirements: s402PaymentRequirements): string;
6
+ /** Encode payment payload for the `x-payment` header */
7
+ declare function encodePaymentPayload(payload: s402PaymentPayload): string;
8
+ /** Encode settlement response for the `payment-response` header */
9
+ declare function encodeSettleResponse(response: s402SettleResponse): string;
10
+ /** Return a clean object with only known s402 requirement fields. */
11
+ declare function pickRequirementsFields(obj: Record<string, unknown>): s402PaymentRequirements;
12
+ /** Decode payment requirements from the `payment-required` header */
13
+ declare function decodePaymentRequired(header: string): s402PaymentRequirements;
14
+ /** Decode payment payload from the `x-payment` header */
15
+ declare function decodePaymentPayload(header: string): s402PaymentPayload;
16
+ /** Decode settlement response from the `payment-response` header */
17
+ declare function decodeSettleResponse(header: string): s402SettleResponse;
18
+ /**
19
+ * Check that a string represents a canonical non-negative integer (valid for Sui MIST amounts).
20
+ * Rejects leading zeros ("007"), empty strings, negatives, decimals.
21
+ * Accepts "0" as the only zero representation.
22
+ */
23
+ declare function isValidAmount(s: string): boolean;
24
+ /**
25
+ * Validate mandate requirements sub-object.
26
+ * Mandate is protocol-level (used for authorization decisions), so we validate fully.
27
+ */
28
+ declare function validateMandateShape(value: unknown): void;
29
+ /**
30
+ * Validate stream sub-object.
31
+ * Checks required fields are present and correctly typed.
32
+ */
33
+ declare function validateStreamShape(value: unknown): void;
34
+ /**
35
+ * Validate escrow sub-object.
36
+ */
37
+ declare function validateEscrowShape(value: unknown): void;
38
+ /**
39
+ * Validate unlock sub-object (pay-to-decrypt encrypted content).
40
+ */
41
+ declare function validateUnlockShape(value: unknown): void;
42
+ /**
43
+ * Validate prepaid sub-object.
44
+ */
45
+ declare function validatePrepaidShape(value: unknown): void;
46
+ /**
47
+ * Validate all optional sub-objects on a requirements record.
48
+ * Called from validateRequirementsShape during wire decode and compat normalization.
49
+ */
50
+ declare function validateSubObjects(record: Record<string, unknown>): void;
51
+ /** Validate that decoded payment requirements have the required shape. */
52
+ declare function validateRequirementsShape(obj: unknown): void;
53
+ /**
54
+ * Detect whether a 402 response uses s402 or x402 protocol.
55
+ *
56
+ * Check: decode the `payment-required` header and look for `s402Version`.
57
+ * If present → s402. If absent → x402 (or raw 402).
58
+ */
59
+ declare function detectProtocol(headers: Headers): 's402' | 'x402' | 'unknown';
60
+ /**
61
+ * Extract s402 payment requirements from a 402 Response.
62
+ * Returns null if the header is missing, malformed, or not s402 format.
63
+ * For x402 responses, decode the header manually and use normalizeRequirements().
64
+ */
65
+ declare function extractRequirementsFromResponse(response: Response): s402PaymentRequirements | null;
66
+ //#endregion
67
+ export { decodePaymentPayload, decodePaymentRequired, decodeSettleResponse, detectProtocol, encodePaymentPayload, encodePaymentRequired, encodeSettleResponse, extractRequirementsFromResponse, isValidAmount, pickRequirementsFields, validateEscrowShape, validateMandateShape, validatePrepaidShape, validateRequirementsShape, validateStreamShape, validateSubObjects, validateUnlockShape };
package/dist/http.mjs ADDED
@@ -0,0 +1,274 @@
1
+ import { S402_HEADERS } from "./types.mjs";
2
+ import { s402Error } from "./errors.mjs";
3
+
4
+ //#region src/http.ts
5
+ /** Encode a UTF-8 string to base64. Safe for any Unicode content. */
6
+ function toBase64(str) {
7
+ const bytes = new TextEncoder().encode(str);
8
+ return btoa(Array.from(bytes, (b) => String.fromCharCode(b)).join(""));
9
+ }
10
+ /** Decode base64 to a UTF-8 string. Safe for any Unicode content. */
11
+ function fromBase64(b64) {
12
+ const binary = atob(b64);
13
+ const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
14
+ return new TextDecoder().decode(bytes);
15
+ }
16
+ /** Encode payment requirements for the `payment-required` header */
17
+ function encodePaymentRequired(requirements) {
18
+ return toBase64(JSON.stringify(requirements));
19
+ }
20
+ /** Encode payment payload for the `x-payment` header */
21
+ function encodePaymentPayload(payload) {
22
+ return toBase64(JSON.stringify(payload));
23
+ }
24
+ /** Encode settlement response for the `payment-response` header */
25
+ function encodeSettleResponse(response) {
26
+ return toBase64(JSON.stringify(response));
27
+ }
28
+ /**
29
+ * Maximum base64 header size (64KB). Defense-in-depth against oversized payloads.
30
+ * Most HTTP servers enforce smaller limits (Node: 16KB, CF Workers: 128KB),
31
+ * but a wire format library should not rely on runtime enforcement.
32
+ */
33
+ const MAX_HEADER_BYTES = 64 * 1024;
34
+ /**
35
+ * Known top-level keys on s402PaymentRequirements.
36
+ * Single source of truth — compat.ts imports pickRequirementsFields from here.
37
+ * Used by decodePaymentRequired to strip unknown keys at the HTTP trust boundary.
38
+ */
39
+ const S402_REQUIREMENTS_KEYS = new Set([
40
+ "s402Version",
41
+ "accepts",
42
+ "network",
43
+ "asset",
44
+ "amount",
45
+ "payTo",
46
+ "facilitatorUrl",
47
+ "mandate",
48
+ "protocolFeeBps",
49
+ "protocolFeeAddress",
50
+ "receiptRequired",
51
+ "settlementMode",
52
+ "expiresAt",
53
+ "stream",
54
+ "escrow",
55
+ "unlock",
56
+ "prepaid",
57
+ "extensions"
58
+ ]);
59
+ /** Return a clean object with only known s402 requirement fields. */
60
+ function pickRequirementsFields(obj) {
61
+ const result = {};
62
+ for (const key of S402_REQUIREMENTS_KEYS) if (key in obj) result[key] = obj[key];
63
+ return result;
64
+ }
65
+ /** Decode payment requirements from the `payment-required` header */
66
+ function decodePaymentRequired(header) {
67
+ if (header.length > MAX_HEADER_BYTES) throw new s402Error("INVALID_PAYLOAD", `payment-required header exceeds maximum size (${header.length} > ${MAX_HEADER_BYTES})`);
68
+ let parsed;
69
+ try {
70
+ parsed = JSON.parse(fromBase64(header));
71
+ } catch (e) {
72
+ throw new s402Error("INVALID_PAYLOAD", `Failed to decode payment-required header: ${e instanceof Error ? e.message : "invalid base64 or JSON"}`);
73
+ }
74
+ validateRequirementsShape(parsed);
75
+ return pickRequirementsFields(parsed);
76
+ }
77
+ /** Decode payment payload from the `x-payment` header */
78
+ function decodePaymentPayload(header) {
79
+ if (header.length > MAX_HEADER_BYTES) throw new s402Error("INVALID_PAYLOAD", `x-payment header exceeds maximum size (${header.length} > ${MAX_HEADER_BYTES})`);
80
+ let parsed;
81
+ try {
82
+ parsed = JSON.parse(fromBase64(header));
83
+ } catch (e) {
84
+ throw new s402Error("INVALID_PAYLOAD", `Failed to decode x-payment header: ${e instanceof Error ? e.message : "invalid base64 or JSON"}`);
85
+ }
86
+ validatePayloadShape(parsed);
87
+ return parsed;
88
+ }
89
+ /** Decode settlement response from the `payment-response` header */
90
+ function decodeSettleResponse(header) {
91
+ if (header.length > MAX_HEADER_BYTES) throw new s402Error("INVALID_PAYLOAD", `payment-response header exceeds maximum size (${header.length} > ${MAX_HEADER_BYTES})`);
92
+ let parsed;
93
+ try {
94
+ parsed = JSON.parse(fromBase64(header));
95
+ } catch (e) {
96
+ throw new s402Error("INVALID_PAYLOAD", `Failed to decode payment-response header: ${e instanceof Error ? e.message : "invalid base64 or JSON"}`);
97
+ }
98
+ validateSettleShape(parsed);
99
+ return parsed;
100
+ }
101
+ /** Valid s402 payment scheme values */
102
+ const VALID_SCHEMES = new Set([
103
+ "exact",
104
+ "stream",
105
+ "escrow",
106
+ "unlock",
107
+ "prepaid"
108
+ ]);
109
+ /**
110
+ * Check that a string represents a canonical non-negative integer (valid for Sui MIST amounts).
111
+ * Rejects leading zeros ("007"), empty strings, negatives, decimals.
112
+ * Accepts "0" as the only zero representation.
113
+ */
114
+ function isValidAmount(s) {
115
+ return /^(0|[1-9][0-9]*)$/.test(s);
116
+ }
117
+ /** Helper: assert obj is a plain object (not null/array/primitive). */
118
+ function assertPlainObject(value, label) {
119
+ if (value == null || typeof value !== "object" || Array.isArray(value)) throw new s402Error("INVALID_PAYLOAD", `${label} must be a plain object, got ${Array.isArray(value) ? "array" : typeof value}`);
120
+ }
121
+ /** Helper: assert field is a string. */
122
+ function assertString(obj, field, label) {
123
+ if (typeof obj[field] !== "string") throw new s402Error("INVALID_PAYLOAD", `${label}.${field} must be a string, got ${typeof obj[field]}`);
124
+ }
125
+ /** Helper: assert optional field is a string if present. */
126
+ function assertOptionalString(obj, field, label) {
127
+ if (obj[field] !== void 0 && typeof obj[field] !== "string") throw new s402Error("INVALID_PAYLOAD", `${label}.${field} must be a string if provided, got ${typeof obj[field]}`);
128
+ }
129
+ /**
130
+ * Validate mandate requirements sub-object.
131
+ * Mandate is protocol-level (used for authorization decisions), so we validate fully.
132
+ */
133
+ function validateMandateShape(value) {
134
+ assertPlainObject(value, "mandate");
135
+ const obj = value;
136
+ if (typeof obj.required !== "boolean") throw new s402Error("INVALID_PAYLOAD", `mandate.required must be a boolean, got ${typeof obj.required}`);
137
+ assertOptionalString(obj, "minPerTx", "mandate");
138
+ assertOptionalString(obj, "coinType", "mandate");
139
+ }
140
+ /**
141
+ * Validate stream sub-object.
142
+ * Checks required fields are present and correctly typed.
143
+ */
144
+ function validateStreamShape(value) {
145
+ assertPlainObject(value, "stream");
146
+ const obj = value;
147
+ assertString(obj, "ratePerSecond", "stream");
148
+ assertString(obj, "budgetCap", "stream");
149
+ assertString(obj, "minDeposit", "stream");
150
+ assertOptionalString(obj, "streamSetupUrl", "stream");
151
+ }
152
+ /**
153
+ * Validate escrow sub-object.
154
+ */
155
+ function validateEscrowShape(value) {
156
+ assertPlainObject(value, "escrow");
157
+ const obj = value;
158
+ assertString(obj, "seller", "escrow");
159
+ assertString(obj, "deadlineMs", "escrow");
160
+ assertOptionalString(obj, "arbiter", "escrow");
161
+ }
162
+ /**
163
+ * Validate unlock sub-object (pay-to-decrypt encrypted content).
164
+ */
165
+ function validateUnlockShape(value) {
166
+ assertPlainObject(value, "unlock");
167
+ const obj = value;
168
+ assertString(obj, "encryptionId", "unlock");
169
+ assertString(obj, "walrusBlobId", "unlock");
170
+ assertString(obj, "encryptionPackageId", "unlock");
171
+ }
172
+ /**
173
+ * Validate prepaid sub-object.
174
+ */
175
+ function validatePrepaidShape(value) {
176
+ assertPlainObject(value, "prepaid");
177
+ const obj = value;
178
+ assertString(obj, "ratePerCall", "prepaid");
179
+ assertString(obj, "minDeposit", "prepaid");
180
+ assertString(obj, "withdrawalDelayMs", "prepaid");
181
+ assertOptionalString(obj, "maxCalls", "prepaid");
182
+ }
183
+ /**
184
+ * Validate all optional sub-objects on a requirements record.
185
+ * Called from validateRequirementsShape during wire decode and compat normalization.
186
+ */
187
+ function validateSubObjects(record) {
188
+ if (record.mandate !== void 0) validateMandateShape(record.mandate);
189
+ if (record.stream !== void 0) validateStreamShape(record.stream);
190
+ if (record.escrow !== void 0) validateEscrowShape(record.escrow);
191
+ if (record.unlock !== void 0) validateUnlockShape(record.unlock);
192
+ if (record.prepaid !== void 0) validatePrepaidShape(record.prepaid);
193
+ }
194
+ /** Validate that decoded payment requirements have the required shape. */
195
+ function validateRequirementsShape(obj) {
196
+ if (obj == null || typeof obj !== "object") throw new s402Error("INVALID_PAYLOAD", "Payment requirements is not an object");
197
+ const record = obj;
198
+ if (record.s402Version === void 0) throw new s402Error("INVALID_PAYLOAD", "Missing s402Version. For x402 format, use normalizeRequirements() from s402/compat.");
199
+ if (record.s402Version !== "1") throw new s402Error("INVALID_PAYLOAD", `Unsupported s402 version "${record.s402Version}". This library supports version "1".`);
200
+ const missing = [];
201
+ if (!Array.isArray(record.accepts)) missing.push("accepts (array)");
202
+ if (typeof record.network !== "string") missing.push("network (string)");
203
+ if (typeof record.asset !== "string") missing.push("asset (string)");
204
+ if (typeof record.amount !== "string") missing.push("amount (string)");
205
+ else if (!isValidAmount(record.amount)) throw new s402Error("INVALID_PAYLOAD", `Invalid amount "${record.amount}": must be a non-negative integer string`);
206
+ if (typeof record.payTo !== "string") missing.push("payTo (string)");
207
+ if (missing.length > 0) throw new s402Error("INVALID_PAYLOAD", `Malformed payment requirements: missing ${missing.join(", ")}`);
208
+ if (Array.isArray(record.accepts) && record.accepts.length === 0) throw new s402Error("INVALID_PAYLOAD", "accepts array must contain at least one scheme");
209
+ const accepts = record.accepts;
210
+ for (const scheme of accepts) if (typeof scheme !== "string") throw new s402Error("INVALID_PAYLOAD", `Invalid entry in accepts array: expected string, got ${typeof scheme}`);
211
+ if (record.protocolFeeBps !== void 0) {
212
+ if (typeof record.protocolFeeBps !== "number" || !Number.isFinite(record.protocolFeeBps) || record.protocolFeeBps < 0 || record.protocolFeeBps > 1e4) throw new s402Error("INVALID_PAYLOAD", `protocolFeeBps must be a finite number between 0 and 10000, got ${record.protocolFeeBps}`);
213
+ }
214
+ if (record.expiresAt !== void 0) {
215
+ if (typeof record.expiresAt !== "number" || !Number.isFinite(record.expiresAt)) throw new s402Error("INVALID_PAYLOAD", `expiresAt must be a finite number (Unix timestamp ms), got ${typeof record.expiresAt}`);
216
+ }
217
+ validateSubObjects(record);
218
+ }
219
+ /** Validate that a decoded payment payload has the required shape. */
220
+ function validatePayloadShape(obj) {
221
+ if (obj == null || typeof obj !== "object") throw new s402Error("INVALID_PAYLOAD", "Payment payload is not an object");
222
+ const record = obj;
223
+ if (record.s402Version !== void 0 && record.s402Version !== "1") throw new s402Error("INVALID_PAYLOAD", `Unsupported s402 version "${record.s402Version}" in payment payload. This library supports version "1".`);
224
+ const missing = [];
225
+ if (typeof record.scheme !== "string") missing.push("scheme");
226
+ else if (!VALID_SCHEMES.has(record.scheme)) throw new s402Error("INVALID_PAYLOAD", `Unknown payment scheme "${record.scheme}". Valid: ${[...VALID_SCHEMES].join(", ")}`);
227
+ if (record.payload == null || typeof record.payload !== "object") missing.push("payload");
228
+ if (missing.length > 0) throw new s402Error("INVALID_PAYLOAD", `Malformed payment payload: missing ${missing.join(", ")}`);
229
+ const inner = record.payload;
230
+ if (typeof inner.transaction !== "string") throw new s402Error("INVALID_PAYLOAD", `payload.transaction must be a string, got ${typeof inner.transaction}`);
231
+ if (typeof inner.signature !== "string") throw new s402Error("INVALID_PAYLOAD", `payload.signature must be a string, got ${typeof inner.signature}`);
232
+ if (record.scheme === "unlock" && typeof inner.encryptionId !== "string") throw new s402Error("INVALID_PAYLOAD", `unlock payload requires encryptionId (string), got ${typeof inner.encryptionId}`);
233
+ if (record.scheme === "prepaid" && typeof inner.ratePerCall !== "string") throw new s402Error("INVALID_PAYLOAD", `prepaid payload requires ratePerCall (string), got ${typeof inner.ratePerCall}`);
234
+ }
235
+ /** Validate that a decoded settle response has the required shape. */
236
+ function validateSettleShape(obj) {
237
+ if (obj == null || typeof obj !== "object") throw new s402Error("INVALID_PAYLOAD", "Settle response is not an object");
238
+ if (typeof obj.success !== "boolean") throw new s402Error("INVALID_PAYLOAD", "Malformed settle response: missing or invalid \"success\" (boolean)");
239
+ }
240
+ /**
241
+ * Detect whether a 402 response uses s402 or x402 protocol.
242
+ *
243
+ * Check: decode the `payment-required` header and look for `s402Version`.
244
+ * If present → s402. If absent → x402 (or raw 402).
245
+ */
246
+ function detectProtocol(headers) {
247
+ const paymentRequired = headers.get(S402_HEADERS.PAYMENT_REQUIRED);
248
+ if (!paymentRequired) return "unknown";
249
+ try {
250
+ const decoded = JSON.parse(fromBase64(paymentRequired));
251
+ if (decoded != null && typeof decoded === "object" && "s402Version" in decoded) return "s402";
252
+ if (decoded != null && typeof decoded === "object" && "x402Version" in decoded) return "x402";
253
+ return "unknown";
254
+ } catch {
255
+ return "unknown";
256
+ }
257
+ }
258
+ /**
259
+ * Extract s402 payment requirements from a 402 Response.
260
+ * Returns null if the header is missing, malformed, or not s402 format.
261
+ * For x402 responses, decode the header manually and use normalizeRequirements().
262
+ */
263
+ function extractRequirementsFromResponse(response) {
264
+ const header = response.headers.get(S402_HEADERS.PAYMENT_REQUIRED);
265
+ if (!header) return null;
266
+ try {
267
+ return decodePaymentRequired(header);
268
+ } catch {
269
+ return null;
270
+ }
271
+ }
272
+
273
+ //#endregion
274
+ export { decodePaymentPayload, decodePaymentRequired, decodeSettleResponse, detectProtocol, encodePaymentPayload, encodePaymentRequired, encodeSettleResponse, extractRequirementsFromResponse, isValidAmount, pickRequirementsFields, validateEscrowShape, validateMandateShape, validatePrepaidShape, validateRequirementsShape, validateStreamShape, validateSubObjects, validateUnlockShape };