@primitivedotdev/sdk 1.7.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -0
- package/dist/api/index.d.ts +4 -4
- package/dist/api/index.js +3 -3
- package/dist/{api-ClLMDF81.js → api-26ENR8Ph.js} +30 -1
- package/dist/contract/index.d.ts +2 -2
- package/dist/contract/index.js +1 -1
- package/dist/{errors-DyuAXctD.d.ts → errors-bXUNXAlf.d.ts} +1 -1
- package/dist/{index-iZWfb98V.d.ts → index-BDnY9HH-.d.ts} +30 -48
- package/dist/{index-BnbrY8kp.d.ts → index-C3ahy3ls.d.ts} +161 -3
- package/dist/index.d.ts +5 -5
- package/dist/index.js +3 -3
- package/dist/openapi/index.js +1 -1
- package/dist/{operations.generated-DQIrhCT5.js → operations.generated-Bu20uPOw.js} +304 -0
- package/dist/parser/index.d.ts +1 -1
- package/dist/{types-QT2ss9ho.d.ts → types-BjnIxPED.d.ts} +97 -2
- package/dist/webhook/index.d.ts +4 -4
- package/dist/webhook/index.js +2 -2
- package/dist/{webhook-CwjCyFv-.js → webhook-CiIPtegj.js} +155 -26
- package/dist/x402/index.d.ts +136 -1
- package/dist/x402/index.js +178 -2
- package/package.json +1 -1
|
@@ -8132,6 +8132,94 @@ function verifyDownloadToken(params) {
|
|
|
8132
8132
|
return { valid: true };
|
|
8133
8133
|
}
|
|
8134
8134
|
//#endregion
|
|
8135
|
+
//#region src/webhook/events.ts
|
|
8136
|
+
/**
|
|
8137
|
+
* The five first-party email events (subject = an email).
|
|
8138
|
+
*/
|
|
8139
|
+
const EMAIL_EVENT_TYPES = [
|
|
8140
|
+
"email.received",
|
|
8141
|
+
"email.bounced",
|
|
8142
|
+
"email.tls_report",
|
|
8143
|
+
"email.dmarc_report",
|
|
8144
|
+
"email.dmarc_failure"
|
|
8145
|
+
];
|
|
8146
|
+
/**
|
|
8147
|
+
* The two x402 settlement-notification events (subject = a payment). Emitted for
|
|
8148
|
+
* both the synthetic API pay flow and the email-native settle path.
|
|
8149
|
+
*/
|
|
8150
|
+
const PAYMENT_EVENT_TYPES = ["payment.settled", "payment.failed"];
|
|
8151
|
+
/**
|
|
8152
|
+
* The interaction step events (subject = an interaction). One event per accepted
|
|
8153
|
+
* protocol step, named `interaction.<protocolShort>.<suffix>`.
|
|
8154
|
+
*
|
|
8155
|
+
* The x402 slice covers the payment lifecycle a payee/payer cares about; the ack
|
|
8156
|
+
* slice covers the acknowledgement protocols.
|
|
8157
|
+
*/
|
|
8158
|
+
const INTERACTION_EVENT_TYPES = [
|
|
8159
|
+
"interaction.ack.acked",
|
|
8160
|
+
"interaction.ack.canceled",
|
|
8161
|
+
"interaction.ack.expired",
|
|
8162
|
+
"interaction.ack.received",
|
|
8163
|
+
"interaction.ack.requested",
|
|
8164
|
+
"interaction.x402.challenge",
|
|
8165
|
+
"interaction.x402.declined",
|
|
8166
|
+
"interaction.x402.expired",
|
|
8167
|
+
"interaction.x402.payment",
|
|
8168
|
+
"interaction.x402.rejected",
|
|
8169
|
+
"interaction.x402.settled",
|
|
8170
|
+
"interaction.x402.verify_timeout"
|
|
8171
|
+
];
|
|
8172
|
+
/**
|
|
8173
|
+
* The full enumerated catalog of every current webhook event type: the five
|
|
8174
|
+
* email.*, the two payment.*, and every interaction.<protocol>.<suffix>.
|
|
8175
|
+
*/
|
|
8176
|
+
const WEBHOOK_EVENT_TYPES = [
|
|
8177
|
+
...EMAIL_EVENT_TYPES,
|
|
8178
|
+
...PAYMENT_EVENT_TYPES,
|
|
8179
|
+
...INTERACTION_EVENT_TYPES
|
|
8180
|
+
];
|
|
8181
|
+
const WEBHOOK_EVENT_TYPE_SET = new Set(WEBHOOK_EVENT_TYPES);
|
|
8182
|
+
/** True if `eventType` is a known current catalog value. */
|
|
8183
|
+
function isKnownWebhookEventType(eventType) {
|
|
8184
|
+
return eventType != null && WEBHOOK_EVENT_TYPE_SET.has(eventType);
|
|
8185
|
+
}
|
|
8186
|
+
function eventName(event) {
|
|
8187
|
+
if (typeof event !== "object" || event === null) return void 0;
|
|
8188
|
+
const value = event.event;
|
|
8189
|
+
return typeof value === "string" ? value : void 0;
|
|
8190
|
+
}
|
|
8191
|
+
/**
|
|
8192
|
+
* Type guard for the `email.received` event. Confirms the discriminator AND
|
|
8193
|
+
* that the body validates against the canonical schema, so a payload that names
|
|
8194
|
+
* itself `email.received` but is malformed does not narrow.
|
|
8195
|
+
*/
|
|
8196
|
+
function isEmailReceivedEvent(event) {
|
|
8197
|
+
if (eventName(event) !== "email.received") return false;
|
|
8198
|
+
try {
|
|
8199
|
+
validateEmailReceivedEvent(event);
|
|
8200
|
+
return true;
|
|
8201
|
+
} catch {
|
|
8202
|
+
return false;
|
|
8203
|
+
}
|
|
8204
|
+
}
|
|
8205
|
+
/** Type guard for any `payment.*` event. */
|
|
8206
|
+
function isPaymentEvent(event) {
|
|
8207
|
+
const name = eventName(event);
|
|
8208
|
+
return name === "payment.settled" || name === "payment.failed";
|
|
8209
|
+
}
|
|
8210
|
+
/** Type guard for the `payment.settled` event. */
|
|
8211
|
+
function isPaymentSettledEvent(event) {
|
|
8212
|
+
return eventName(event) === "payment.settled";
|
|
8213
|
+
}
|
|
8214
|
+
/** Type guard for the `payment.failed` event. */
|
|
8215
|
+
function isPaymentFailedEvent(event) {
|
|
8216
|
+
return eventName(event) === "payment.failed";
|
|
8217
|
+
}
|
|
8218
|
+
/** Type guard for any `interaction.x402.*` event. */
|
|
8219
|
+
function isInteractionX402Event(event) {
|
|
8220
|
+
return eventName(event)?.startsWith("interaction.x402.") ?? false;
|
|
8221
|
+
}
|
|
8222
|
+
//#endregion
|
|
8135
8223
|
//#region src/webhook/encoding.ts
|
|
8136
8224
|
/**
|
|
8137
8225
|
* Buffer encoding utilities
|
|
@@ -10014,40 +10102,52 @@ const BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/
|
|
|
10014
10102
|
* }
|
|
10015
10103
|
* ```
|
|
10016
10104
|
*/
|
|
10017
|
-
function parseWebhookEvent(input) {
|
|
10105
|
+
function parseWebhookEvent(input, eventType) {
|
|
10018
10106
|
if (input === null) throw new WebhookPayloadError("PAYLOAD_NULL", "Received null instead of webhook payload", "Check that your request body variable is defined.");
|
|
10019
10107
|
if (input === void 0) throw new WebhookPayloadError("PAYLOAD_UNDEFINED", "Received undefined instead of webhook payload", "Make sure you're passing the request body to parseWebhookEvent()");
|
|
10020
10108
|
if (Array.isArray(input)) throw new WebhookPayloadError("PAYLOAD_IS_ARRAY", "Received array instead of webhook payload object", "Webhook payloads must be objects, not arrays.");
|
|
10021
10109
|
if (typeof input !== "object") throw new WebhookPayloadError("PAYLOAD_WRONG_TYPE", `Received ${typeof input} instead of webhook payload object`, "Webhook payloads must be objects.");
|
|
10022
10110
|
const obj = input;
|
|
10023
|
-
|
|
10024
|
-
|
|
10111
|
+
const resolvedEvent = typeof eventType === "string" && eventType || (typeof obj.event === "string" ? obj.event : void 0);
|
|
10112
|
+
if (!resolvedEvent) throw new WebhookPayloadError("PAYLOAD_MISSING_EVENT", "Missing event discriminator: no X-Webhook-Event header and no 'event' field in payload", "Pass the X-Webhook-Event header (the canonical discriminator) or call handleWebhookEvent, which reads it for you.");
|
|
10113
|
+
switch (resolvedEvent) {
|
|
10025
10114
|
case "email.received": return validateEmailReceivedEvent(input);
|
|
10026
|
-
|
|
10115
|
+
case "payment.settled":
|
|
10116
|
+
case "payment.failed": return {
|
|
10117
|
+
...obj,
|
|
10118
|
+
event: resolvedEvent
|
|
10119
|
+
};
|
|
10120
|
+
default:
|
|
10121
|
+
if (isKnownWebhookEventType(resolvedEvent)) return {
|
|
10122
|
+
...obj,
|
|
10123
|
+
event: resolvedEvent
|
|
10124
|
+
};
|
|
10125
|
+
return {
|
|
10126
|
+
...obj,
|
|
10127
|
+
event: resolvedEvent
|
|
10128
|
+
};
|
|
10027
10129
|
}
|
|
10028
10130
|
}
|
|
10029
10131
|
/**
|
|
10030
|
-
*
|
|
10031
|
-
*
|
|
10032
|
-
*
|
|
10033
|
-
* ```typescript
|
|
10034
|
-
* const event = parseWebhookEvent(payload);
|
|
10035
|
-
* if (isEmailReceivedEvent(event)) {
|
|
10036
|
-
* // TypeScript knows event is EmailReceivedEvent
|
|
10037
|
-
* console.log(event.email.headers.subject);
|
|
10038
|
-
* }
|
|
10039
|
-
* ```
|
|
10132
|
+
* The header that names the webhook event for ALL event families
|
|
10133
|
+
* (`email.*`, `payment.*`, `interaction.*`). It is the primary discriminator
|
|
10134
|
+
* the parser keys on, because the stored body is sent verbatim with no envelope.
|
|
10040
10135
|
*/
|
|
10041
|
-
|
|
10042
|
-
if (typeof event !== "object" || event === null || !("event" in event) || event.event !== "email.received") return false;
|
|
10043
|
-
try {
|
|
10044
|
-
validateEmailReceivedEvent(event);
|
|
10045
|
-
return true;
|
|
10046
|
-
} catch {
|
|
10047
|
-
return false;
|
|
10048
|
-
}
|
|
10049
|
-
}
|
|
10136
|
+
const WEBHOOK_EVENT_HEADER = "X-Webhook-Event";
|
|
10050
10137
|
const SIGNATURE_HEADER_NAMES = ["primitive-signature", "mymx-signature"];
|
|
10138
|
+
/**
|
|
10139
|
+
* Read the `X-Webhook-Event` header value (case-insensitive). Returns null when
|
|
10140
|
+
* the header is absent.
|
|
10141
|
+
*/
|
|
10142
|
+
function getEventHeader(headers) {
|
|
10143
|
+
if (headers instanceof Headers) return headers.get("x-webhook-event");
|
|
10144
|
+
const obj = headers;
|
|
10145
|
+
const key = Object.keys(obj).find((k) => k.toLowerCase() === "x-webhook-event");
|
|
10146
|
+
if (!key) return null;
|
|
10147
|
+
const value = obj[key];
|
|
10148
|
+
if (Array.isArray(value)) return value[0] ?? null;
|
|
10149
|
+
return value ?? null;
|
|
10150
|
+
}
|
|
10051
10151
|
const STANDARD_WEBHOOKS_HEADER_NAMES = [
|
|
10052
10152
|
"webhook-signature",
|
|
10053
10153
|
"webhook-id",
|
|
@@ -10160,7 +10260,7 @@ function getStandardWebhooksHeaders(headers) {
|
|
|
10160
10260
|
* });
|
|
10161
10261
|
* ```
|
|
10162
10262
|
*/
|
|
10163
|
-
function
|
|
10263
|
+
function verifyWebhookRequest(options) {
|
|
10164
10264
|
const { body, headers, secret, toleranceSeconds } = options;
|
|
10165
10265
|
const swHeaders = getStandardWebhooksHeaders(headers);
|
|
10166
10266
|
if (swHeaders) verifyStandardWebhooksSignature({
|
|
@@ -10177,7 +10277,36 @@ function handleWebhook(options) {
|
|
|
10177
10277
|
secret,
|
|
10178
10278
|
toleranceSeconds
|
|
10179
10279
|
});
|
|
10180
|
-
|
|
10280
|
+
}
|
|
10281
|
+
/**
|
|
10282
|
+
* Verify, then parse any webhook event into a typed value.
|
|
10283
|
+
*
|
|
10284
|
+
* Unlike {@link handleWebhook}, this returns the full {@link WebhookEvent}
|
|
10285
|
+
* union, so it handles `payment.*` and `interaction.x402.*` events in addition
|
|
10286
|
+
* to `email.*`. The flow is:
|
|
10287
|
+
*
|
|
10288
|
+
* 1. Verify the signature over the RAW body (works for every event family).
|
|
10289
|
+
* 2. Parse the JSON body.
|
|
10290
|
+
* 3. Classify on the `X-Webhook-Event` HEADER (the primary discriminator),
|
|
10291
|
+
* returning a typed event for known types and an UnknownEvent for the rest.
|
|
10292
|
+
*
|
|
10293
|
+
* @example
|
|
10294
|
+
* ```typescript
|
|
10295
|
+
* const event = handleWebhookEvent({ body, headers, secret });
|
|
10296
|
+
* if (isPaymentSettledEvent(event)) {
|
|
10297
|
+
* // typed PaymentSettledEvent
|
|
10298
|
+
* } else if (isInteractionX402Event(event)) {
|
|
10299
|
+
* // typed interaction.x402.* event
|
|
10300
|
+
* }
|
|
10301
|
+
* ```
|
|
10302
|
+
*/
|
|
10303
|
+
function handleWebhookEvent(options) {
|
|
10304
|
+
verifyWebhookRequest(options);
|
|
10305
|
+
return parseWebhookEvent(parseJsonBody(options.body), getEventHeader(options.headers));
|
|
10306
|
+
}
|
|
10307
|
+
function handleWebhook(options) {
|
|
10308
|
+
verifyWebhookRequest(options);
|
|
10309
|
+
return validateEmailReceivedEvent(parseJsonBody(options.body));
|
|
10181
10310
|
}
|
|
10182
10311
|
function receive(input, options) {
|
|
10183
10312
|
if (input instanceof Request) return receiveFromRequest(input, options);
|
|
@@ -10364,4 +10493,4 @@ function verifyRawEmailDownload(downloaded, event) {
|
|
|
10364
10493
|
return buffer;
|
|
10365
10494
|
}
|
|
10366
10495
|
//#endregion
|
|
10367
|
-
export {
|
|
10496
|
+
export { LEGACY_CONFIRMED_HEADER as A, isEmailReceivedEvent as B, SpfResult as C, STANDARD_WEBHOOK_TIMESTAMP_HEADER as D, STANDARD_WEBHOOK_SIGNATURE_HEADER as E, verifyWebhookSignature as F, isPaymentSettledEvent as G, isKnownWebhookEventType as H, EMAIL_EVENT_TYPES as I, safeValidateEmailReceivedEvent as J, generateDownloadToken as K, INTERACTION_EVENT_TYPES as L, PRIMITIVE_CONFIRMED_HEADER as M, PRIMITIVE_SIGNATURE_HEADER as N, signStandardWebhooksPayload as O, signWebhookPayload as P, PAYMENT_EVENT_TYPES as R, ParsedStatus as S, STANDARD_WEBHOOK_ID_HEADER as T, isPaymentEvent as U, isInteractionX402Event as V, isPaymentFailedEvent as W, validateEmailReceivedEvent as Y, DkimResult as _, getEventHeader as a, EventType as b, isDownloadExpired as c, receive as d, verifyRawEmailDownload as f, AuthVerdict as g, AuthConfidence as h, getDownloadTimeRemaining as i, LEGACY_SIGNATURE_HEADER as j, verifyStandardWebhooksSignature as k, isRawIncluded as l, validateEmailAuth as m, confirmedHeaders as n, handleWebhook as o, WEBHOOK_VERSION as p, verifyDownloadToken as q, decodeRawEmail as r, handleWebhookEvent as s, WEBHOOK_EVENT_HEADER as t, parseWebhookEvent as u, DmarcPolicy as v, emailReceivedEventJsonSchema as w, ForwardVerdict as x, DmarcResult as y, WEBHOOK_EVENT_TYPES as z };
|
package/dist/x402/index.d.ts
CHANGED
|
@@ -130,6 +130,62 @@ interface X402PaymentPayload {
|
|
|
130
130
|
};
|
|
131
131
|
};
|
|
132
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* The protocol the email-native payment interaction runs (`x402.payment/1`).
|
|
135
|
+
* The payer's reply carries the `payment` step of this protocol.
|
|
136
|
+
*/
|
|
137
|
+
declare const X402_INTERACTION_PROTOCOL = "x402.payment";
|
|
138
|
+
declare const X402_INTERACTION_PROTOCOL_VERSION = 1;
|
|
139
|
+
/**
|
|
140
|
+
* The interaction.json envelope for one step of an email-carried interaction.
|
|
141
|
+
* The payer's `payment` step is sent as an `interaction.json` MIME attachment
|
|
142
|
+
* in the reply; the platform parses this envelope, validates the step against
|
|
143
|
+
* the `x402.payment` protocol, and re-verifies the embedded payment.
|
|
144
|
+
*/
|
|
145
|
+
interface InteractionEnvelope<P = unknown> {
|
|
146
|
+
interaction_version: 1;
|
|
147
|
+
/** The thread id (`uuid@domain`) the step belongs to. */
|
|
148
|
+
interaction_id: string;
|
|
149
|
+
protocol: string;
|
|
150
|
+
protocol_version: number;
|
|
151
|
+
/** The protocol step name (e.g. `"payment"`). */
|
|
152
|
+
step: string;
|
|
153
|
+
/** This step's id (a fresh UUID). */
|
|
154
|
+
step_id: string;
|
|
155
|
+
/** The id of the step this one answers (the challenge step), or null. */
|
|
156
|
+
prev_step_id: string | null;
|
|
157
|
+
expires_at: string | null;
|
|
158
|
+
payload: P;
|
|
159
|
+
}
|
|
160
|
+
/** The `payload` of an `x402.payment` `payment` step: the signed x402 payload. */
|
|
161
|
+
interface X402PaymentStepPayload {
|
|
162
|
+
payment: X402PaymentPayload;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* A built, signed payment-step envelope plus its canonical JSON bytes. The
|
|
166
|
+
* caller attaches `json` as the `interaction.json` part of the reply email; the
|
|
167
|
+
* platform reads `envelope` back from those exact bytes.
|
|
168
|
+
*/
|
|
169
|
+
interface BuiltPaymentStep {
|
|
170
|
+
envelope: InteractionEnvelope<X402PaymentStepPayload>;
|
|
171
|
+
/** The canonical interaction.json body (what to attach to the reply). */
|
|
172
|
+
json: string;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Build the section-2.3 interaction.json envelope for a `payment` step. Pure: no
|
|
176
|
+
* I/O. `payment` is the signed exact-EVM payload (from
|
|
177
|
+
* `buildExactEvmPaymentPayload`); `prevStepId` is the challenge step id this
|
|
178
|
+
* payment answers, and `stepId` is a fresh UUID for the payment step. Returns
|
|
179
|
+
* the envelope and its canonical JSON, so the bytes the platform reads back are
|
|
180
|
+
* exactly the ones produced here.
|
|
181
|
+
*/
|
|
182
|
+
declare function buildPaymentStepEnvelope(params: {
|
|
183
|
+
/** The thread id (`uuid@domain`). */interactionId: string; /** A fresh UUID identifying this payment step. */
|
|
184
|
+
stepId: string; /** The challenge step id this payment answers. */
|
|
185
|
+
prevStepId: string;
|
|
186
|
+
payment: X402PaymentPayload; /** Optional ISO-8601 step expiry. */
|
|
187
|
+
expiresAt?: string | null;
|
|
188
|
+
}): BuiltPaymentStep;
|
|
133
189
|
/** Assemble the wire payload from a signed authorization. */
|
|
134
190
|
declare function toPaymentPayload(network: string, auth: TransferAuthorization, signature: Hex): X402PaymentPayload;
|
|
135
191
|
/**
|
|
@@ -218,6 +274,33 @@ interface X402Challenge {
|
|
|
218
274
|
payment_requirements: X402PaymentRequirements;
|
|
219
275
|
expires_at: string;
|
|
220
276
|
}
|
|
277
|
+
/** The nonce binding the payer hashes into the EIP-3009 nonce. */
|
|
278
|
+
interface X402NonceBinding {
|
|
279
|
+
interaction_id: string;
|
|
280
|
+
challenge_step_id: string;
|
|
281
|
+
challenge_nonce: string;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* The challenge details carried inside an email-native challenge: what the
|
|
285
|
+
* payer needs to sign and pay. Distinct from the synthetic `X402Challenge` in
|
|
286
|
+
* that it has no top-level `id`/`amount`; everything is in the nested objects.
|
|
287
|
+
*/
|
|
288
|
+
interface X402EmailChallengeDetails {
|
|
289
|
+
payment_requirements: X402PaymentRequirements;
|
|
290
|
+
nonce_binding: X402NonceBinding;
|
|
291
|
+
expires_at: string;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* The result of issuing an email-native challenge (`createEmailChallenge`).
|
|
295
|
+
* `interaction_id` is the real email thread id (`uuid@domain`) the payment is
|
|
296
|
+
* bound to. Hand the whole object to the payer; the payer calls
|
|
297
|
+
* `payEmailChallenge` with it to build the signed payment step.
|
|
298
|
+
*/
|
|
299
|
+
interface X402EmailChallenge {
|
|
300
|
+
interaction_id: string;
|
|
301
|
+
challenge_id: string;
|
|
302
|
+
challenge: X402EmailChallengeDetails;
|
|
303
|
+
}
|
|
221
304
|
interface X402Receipt {
|
|
222
305
|
id: string;
|
|
223
306
|
status: string;
|
|
@@ -279,6 +362,34 @@ interface X402ChargeInput {
|
|
|
279
362
|
*/
|
|
280
363
|
idempotencyKey?: string;
|
|
281
364
|
}
|
|
365
|
+
interface X402EmailChargeInput {
|
|
366
|
+
/** Your sending address (the payee / funds receiver). */
|
|
367
|
+
from: string;
|
|
368
|
+
/** The payer's email address the challenge is sent to. */
|
|
369
|
+
to: string;
|
|
370
|
+
/**
|
|
371
|
+
* Amount in token base units (USDC has 6 decimals, so "10000" = 0.01).
|
|
372
|
+
* Provide exactly one of `amount` or `amountUsdc`.
|
|
373
|
+
*/
|
|
374
|
+
amount?: string;
|
|
375
|
+
/**
|
|
376
|
+
* Amount as human USDC (e.g. "0.01"), converted to base units for you.
|
|
377
|
+
* Provide exactly one of `amount` or `amountUsdc`.
|
|
378
|
+
*/
|
|
379
|
+
amountUsdc?: string;
|
|
380
|
+
/** Defaults to "base-sepolia". */
|
|
381
|
+
network?: string;
|
|
382
|
+
description?: string;
|
|
383
|
+
/** A URL identifying the thing being paid for. */
|
|
384
|
+
resource?: string;
|
|
385
|
+
/** Seconds until the challenge expires (default 1h). */
|
|
386
|
+
expiresIn?: number;
|
|
387
|
+
/**
|
|
388
|
+
* Optional idempotency key. Retrying `createEmailChallenge()` with the same
|
|
389
|
+
* key returns the original challenge without sending a second email.
|
|
390
|
+
*/
|
|
391
|
+
idempotencyKey?: string;
|
|
392
|
+
}
|
|
282
393
|
declare class X402Error extends Error {
|
|
283
394
|
/** HTTP status, or 0 for a client-side / transport error that never reached the server. */
|
|
284
395
|
readonly status: number;
|
|
@@ -305,6 +416,30 @@ declare class X402Client {
|
|
|
305
416
|
constructor(options?: X402ClientOptions);
|
|
306
417
|
/** Request a payment (payee side). Returns the challenge to hand to the payer. */
|
|
307
418
|
charge(input: X402ChargeInput): Promise<X402Challenge>;
|
|
419
|
+
/**
|
|
420
|
+
* Issue a payment challenge over an email thread (payee side). Sends the
|
|
421
|
+
* challenge as an email from `from` to `to` and binds the payment to that
|
|
422
|
+
* thread. Returns the challenge (including the real `interaction_id`); deliver
|
|
423
|
+
* it to the payer, who calls `payEmailChallenge` to build the signed payment.
|
|
424
|
+
*
|
|
425
|
+
* Provide exactly one of `amount` (base units) or `amountUsdc` (human USDC).
|
|
426
|
+
*/
|
|
427
|
+
createEmailChallenge(input: X402EmailChargeInput): Promise<X402EmailChallenge>;
|
|
428
|
+
/**
|
|
429
|
+
* Build the signed payment step for an email-native challenge (payer side).
|
|
430
|
+
* Given a received `X402EmailChallenge` and the caller's signer, this derives
|
|
431
|
+
* the interaction-bound authorization, signs it locally, and returns the
|
|
432
|
+
* signed `interaction.json` payment-step envelope plus its canonical JSON
|
|
433
|
+
* bytes. It does NOT send anything.
|
|
434
|
+
*
|
|
435
|
+
* The caller sends `result.json` back as an `interaction.json` attachment on a
|
|
436
|
+
* reply to the challenge email (e.g. via the SDK's `send` / `reply`); the
|
|
437
|
+
* platform reads the envelope from those exact bytes, re-derives the bound
|
|
438
|
+
* nonce, and settles.
|
|
439
|
+
*/
|
|
440
|
+
payEmailChallenge(challenge: X402EmailChallenge, options: {
|
|
441
|
+
signer: X402Signer;
|
|
442
|
+
}): Promise<BuiltPaymentStep>;
|
|
308
443
|
/**
|
|
309
444
|
* Pay a challenge (payer side). Derives the interaction-bound authorization,
|
|
310
445
|
* signs it locally with the caller's key, and submits it for settlement.
|
|
@@ -351,4 +486,4 @@ declare class X402Client {
|
|
|
351
486
|
}
|
|
352
487
|
declare function createX402Client(options?: X402ClientOptions): X402Client;
|
|
353
488
|
//#endregion
|
|
354
|
-
export { DEFAULT_MAX_WINDOW_SEC, NonceBinding, PayoutRegistrationMessageInput, TRANSFER_WITH_AUTHORIZATION_TYPES, TokenDomain, TransferAuthorization, TransferWithAuthorizationTypedData, X402Challenge, X402ChargeInput, X402Client, X402ClientOptions, X402DeclinedPayment, X402Error, X402Network, X402PaymentPayload, X402PaymentRequirements, X402PayoutAddress, X402Receipt, X402Signer, X402SpendPolicy, buildExactEvmPaymentPayload, buildPayoutRegistrationMessage, computePaymentValidityWindow, createX402Client, deriveEip3009Nonce, signInteractionPayment, toPaymentPayload, transferWithAuthorizationTypedData };
|
|
489
|
+
export { BuiltPaymentStep, DEFAULT_MAX_WINDOW_SEC, InteractionEnvelope, NonceBinding, PayoutRegistrationMessageInput, TRANSFER_WITH_AUTHORIZATION_TYPES, TokenDomain, TransferAuthorization, TransferWithAuthorizationTypedData, X402Challenge, X402ChargeInput, X402Client, X402ClientOptions, X402DeclinedPayment, X402EmailChallenge, X402EmailChallengeDetails, X402EmailChargeInput, X402Error, X402Network, X402NonceBinding, X402PaymentPayload, X402PaymentRequirements, X402PaymentStepPayload, X402PayoutAddress, X402Receipt, X402Signer, X402SpendPolicy, X402_INTERACTION_PROTOCOL, X402_INTERACTION_PROTOCOL_VERSION, buildExactEvmPaymentPayload, buildPaymentStepEnvelope, buildPayoutRegistrationMessage, computePaymentValidityWindow, createX402Client, deriveEip3009Nonce, signInteractionPayment, toPaymentPayload, transferWithAuthorizationTypedData };
|
package/dist/x402/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { concat, getAddress, hexToBytes, keccak256, stringToBytes } from "viem";
|
|
2
3
|
//#region src/x402/sign.ts
|
|
3
4
|
/**
|
|
@@ -97,6 +98,44 @@ function buildPayoutRegistrationMessage(input) {
|
|
|
97
98
|
`issued: ${input.issuedAt}`
|
|
98
99
|
].join("\n");
|
|
99
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* The protocol the email-native payment interaction runs (`x402.payment/1`).
|
|
103
|
+
* The payer's reply carries the `payment` step of this protocol.
|
|
104
|
+
*/
|
|
105
|
+
const X402_INTERACTION_PROTOCOL = "x402.payment";
|
|
106
|
+
const X402_INTERACTION_PROTOCOL_VERSION = 1;
|
|
107
|
+
/** A UUID (used for `interaction_id`'s local part and the step ids). */
|
|
108
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
109
|
+
/** An interaction id is `uuid@domain`. */
|
|
110
|
+
const WIRE_ID_RE = new RegExp(`^${UUID_RE.source.slice(1, -1)}@[^\\s@]+$`, "i");
|
|
111
|
+
/**
|
|
112
|
+
* Build the section-2.3 interaction.json envelope for a `payment` step. Pure: no
|
|
113
|
+
* I/O. `payment` is the signed exact-EVM payload (from
|
|
114
|
+
* `buildExactEvmPaymentPayload`); `prevStepId` is the challenge step id this
|
|
115
|
+
* payment answers, and `stepId` is a fresh UUID for the payment step. Returns
|
|
116
|
+
* the envelope and its canonical JSON, so the bytes the platform reads back are
|
|
117
|
+
* exactly the ones produced here.
|
|
118
|
+
*/
|
|
119
|
+
function buildPaymentStepEnvelope(params) {
|
|
120
|
+
if (!WIRE_ID_RE.test(params.interactionId)) throw new Error("buildPaymentStepEnvelope: interactionId must be uuid@domain");
|
|
121
|
+
if (!UUID_RE.test(params.stepId)) throw new Error("buildPaymentStepEnvelope: stepId must be a uuid");
|
|
122
|
+
if (!UUID_RE.test(params.prevStepId)) throw new Error("buildPaymentStepEnvelope: prevStepId must be a uuid");
|
|
123
|
+
const envelope = {
|
|
124
|
+
interaction_version: 1,
|
|
125
|
+
interaction_id: params.interactionId,
|
|
126
|
+
protocol: X402_INTERACTION_PROTOCOL,
|
|
127
|
+
protocol_version: 1,
|
|
128
|
+
step: "payment",
|
|
129
|
+
step_id: params.stepId,
|
|
130
|
+
prev_step_id: params.prevStepId,
|
|
131
|
+
expires_at: params.expiresAt ?? null,
|
|
132
|
+
payload: { payment: params.payment }
|
|
133
|
+
};
|
|
134
|
+
return {
|
|
135
|
+
envelope,
|
|
136
|
+
json: JSON.stringify(envelope)
|
|
137
|
+
};
|
|
138
|
+
}
|
|
100
139
|
/** Assemble the wire payload from a signed authorization. */
|
|
101
140
|
function toPaymentPayload(network, auth, signature) {
|
|
102
141
|
return {
|
|
@@ -184,6 +223,15 @@ function buildExactEvmPaymentPayload(params) {
|
|
|
184
223
|
}
|
|
185
224
|
//#endregion
|
|
186
225
|
//#region src/x402/client.ts
|
|
226
|
+
/**
|
|
227
|
+
* x402 agent-to-agent payments.
|
|
228
|
+
*
|
|
229
|
+
* `charge()` (payee) asks for a payment; `pay()` (payer) signs and settles it
|
|
230
|
+
* with the customer's own key. The signing is local and non-custodial; the key
|
|
231
|
+
* never leaves the caller. The server resolves the real payee address, verifies
|
|
232
|
+
* every signed field against its own records, and enforces the spend policy, so
|
|
233
|
+
* the SDK's job is just: derive the bound authorization, sign, and submit.
|
|
234
|
+
*/
|
|
187
235
|
const CHAIN_IDS = {
|
|
188
236
|
"base-sepolia": 84532,
|
|
189
237
|
base: 8453
|
|
@@ -199,6 +247,17 @@ const CHARGE_INPUT_KEYS = {
|
|
|
199
247
|
expiresIn: true,
|
|
200
248
|
idempotencyKey: true
|
|
201
249
|
};
|
|
250
|
+
const EMAIL_CHARGE_INPUT_KEYS = {
|
|
251
|
+
from: true,
|
|
252
|
+
to: true,
|
|
253
|
+
amount: true,
|
|
254
|
+
amountUsdc: true,
|
|
255
|
+
network: true,
|
|
256
|
+
description: true,
|
|
257
|
+
resource: true,
|
|
258
|
+
expiresIn: true,
|
|
259
|
+
idempotencyKey: true
|
|
260
|
+
};
|
|
202
261
|
function usdcToBaseUnits(human) {
|
|
203
262
|
const trimmed = human.trim();
|
|
204
263
|
if (!/^\d+(\.\d+)?$/.test(trimmed)) return null;
|
|
@@ -235,13 +294,37 @@ function validateChallenge(c) {
|
|
|
235
294
|
if (!c.expires_at) bad("expires_at");
|
|
236
295
|
const nb = c.nonce_binding;
|
|
237
296
|
if (!nb?.interaction_id || !nb.challenge_step_id || !nb.challenge_nonce) bad("nonce_binding");
|
|
238
|
-
|
|
297
|
+
validatePaymentRequirements(c.payment_requirements, bad);
|
|
298
|
+
}
|
|
299
|
+
/** Validate the x402 PaymentRequirements shared by both challenge shapes. */
|
|
300
|
+
function validatePaymentRequirements(pr, bad) {
|
|
239
301
|
if (!pr) bad("payment_requirements");
|
|
240
302
|
if (!/^[1-9][0-9]{0,38}$/.test(pr.maxAmountRequired ?? "")) bad("payment_requirements.maxAmountRequired (expected a positive integer string in token base units)");
|
|
241
303
|
if (!/^0x[0-9a-fA-F]{40}$/.test(pr.payTo ?? "")) bad("payment_requirements.payTo (expected a 0x address)");
|
|
242
304
|
if (!/^0x[0-9a-fA-F]{40}$/.test(pr.asset ?? "")) bad("payment_requirements.asset (expected a 0x address)");
|
|
243
305
|
if (!pr.extra?.name || !pr.extra.version) bad("payment_requirements.extra (name/version)");
|
|
244
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* Assert an email-native challenge is fully hydrated before signing, so a
|
|
309
|
+
* missing field fails with a named X402Error instead of an opaque error
|
|
310
|
+
* mid-sign. The interaction_id and the challenge step id (the nonce binding's
|
|
311
|
+
* fields) drive both the bound nonce and the payment-step envelope, so they are
|
|
312
|
+
* checked here.
|
|
313
|
+
*/
|
|
314
|
+
function validateEmailChallenge(c) {
|
|
315
|
+
const bad = (field) => {
|
|
316
|
+
throw new X402Error(`email challenge is missing or malformed: ${field}`, 0);
|
|
317
|
+
};
|
|
318
|
+
if (!c || typeof c !== "object") bad("email challenge");
|
|
319
|
+
if (!c.interaction_id) bad("interaction_id");
|
|
320
|
+
const ch = c.challenge;
|
|
321
|
+
if (!ch || typeof ch !== "object") bad("challenge");
|
|
322
|
+
if (!ch.expires_at) bad("challenge.expires_at");
|
|
323
|
+
const nb = ch.nonce_binding;
|
|
324
|
+
if (!nb?.interaction_id || !nb.challenge_step_id || !nb.challenge_nonce) bad("challenge.nonce_binding");
|
|
325
|
+
if (nb.interaction_id !== c.interaction_id) bad("interaction_id (mismatch with challenge.nonce_binding.interaction_id)");
|
|
326
|
+
validatePaymentRequirements(ch.payment_requirements, bad);
|
|
327
|
+
}
|
|
245
328
|
var X402Client = class {
|
|
246
329
|
#apiKey;
|
|
247
330
|
#baseUrl;
|
|
@@ -301,6 +384,99 @@ var X402Client = class {
|
|
|
301
384
|
return this.#request("POST", "/v1/x402/challenges", body, { headers: input.idempotencyKey ? { "idempotency-key": input.idempotencyKey } : void 0 });
|
|
302
385
|
}
|
|
303
386
|
/**
|
|
387
|
+
* Issue a payment challenge over an email thread (payee side). Sends the
|
|
388
|
+
* challenge as an email from `from` to `to` and binds the payment to that
|
|
389
|
+
* thread. Returns the challenge (including the real `interaction_id`); deliver
|
|
390
|
+
* it to the payer, who calls `payEmailChallenge` to build the signed payment.
|
|
391
|
+
*
|
|
392
|
+
* Provide exactly one of `amount` (base units) or `amountUsdc` (human USDC).
|
|
393
|
+
*/
|
|
394
|
+
async createEmailChallenge(input) {
|
|
395
|
+
for (const key of Object.keys(input)) if (!(key in EMAIL_CHARGE_INPUT_KEYS)) throw new X402Error(`unknown createEmailChallenge() option "${key}"; expected one of: ${Object.keys(EMAIL_CHARGE_INPUT_KEYS).join(", ")}`, 0);
|
|
396
|
+
if (!input.from) throw new X402Error("createEmailChallenge() requires `from`", 0);
|
|
397
|
+
if (!input.to) throw new X402Error("createEmailChallenge() requires `to`", 0);
|
|
398
|
+
if (input.amount !== void 0 && input.amountUsdc !== void 0) throw new X402Error("createEmailChallenge() takes exactly one of `amount` (base units) or `amountUsdc` (human USDC), not both", 0);
|
|
399
|
+
const amount = input.amountUsdc !== void 0 ? usdcToBaseUnits(input.amountUsdc) : input.amount ?? null;
|
|
400
|
+
if (!amount || !/^[1-9][0-9]{0,38}$/.test(amount)) throw new X402Error("createEmailChallenge() requires `amount` as a positive integer string in token base units (e.g. \"10000\"), or `amountUsdc` as a positive USDC amount with at most 6 decimals (e.g. \"0.01\")", 0);
|
|
401
|
+
const body = {
|
|
402
|
+
from: input.from,
|
|
403
|
+
to: input.to,
|
|
404
|
+
amount,
|
|
405
|
+
network: input.network ?? "base-sepolia"
|
|
406
|
+
};
|
|
407
|
+
if (input.description) body.description = input.description;
|
|
408
|
+
if (input.resource) body.resource = input.resource;
|
|
409
|
+
if (input.expiresIn !== void 0) body.expires_in = input.expiresIn;
|
|
410
|
+
return this.#request("POST", "/v1/x402/email-challenges", body, { headers: input.idempotencyKey ? { "idempotency-key": input.idempotencyKey } : void 0 });
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Build the signed payment step for an email-native challenge (payer side).
|
|
414
|
+
* Given a received `X402EmailChallenge` and the caller's signer, this derives
|
|
415
|
+
* the interaction-bound authorization, signs it locally, and returns the
|
|
416
|
+
* signed `interaction.json` payment-step envelope plus its canonical JSON
|
|
417
|
+
* bytes. It does NOT send anything.
|
|
418
|
+
*
|
|
419
|
+
* The caller sends `result.json` back as an `interaction.json` attachment on a
|
|
420
|
+
* reply to the challenge email (e.g. via the SDK's `send` / `reply`); the
|
|
421
|
+
* platform reads the envelope from those exact bytes, re-derives the bound
|
|
422
|
+
* nonce, and settles.
|
|
423
|
+
*/
|
|
424
|
+
async payEmailChallenge(challenge, options) {
|
|
425
|
+
if (!options?.signer?.address || typeof options.signer.signTypedData !== "function") throw new X402Error("payEmailChallenge() requires options.signer with { address, signTypedData } (e.g. a viem LocalAccount)", 0);
|
|
426
|
+
validateEmailChallenge(challenge);
|
|
427
|
+
const details = challenge.challenge;
|
|
428
|
+
const pr = details.payment_requirements;
|
|
429
|
+
const network = pr.network;
|
|
430
|
+
const chainId = CHAIN_IDS[network];
|
|
431
|
+
if (chainId === void 0) throw new X402Error(`unsupported network: ${network}`, 0);
|
|
432
|
+
if (pr.scheme !== "exact") throw new X402Error(`unsupported payment scheme: ${pr.scheme}`, 0);
|
|
433
|
+
const nowSec = Math.floor(Date.now() / 1e3);
|
|
434
|
+
const expiresAtMs = Date.parse(details.expires_at);
|
|
435
|
+
if (Number.isNaN(expiresAtMs)) throw new X402Error(`challenge has an invalid expires_at: ${details.expires_at}`, 0);
|
|
436
|
+
const expiresAtSec = Math.floor(expiresAtMs / 1e3);
|
|
437
|
+
if (expiresAtSec <= nowSec) throw new X402Error(`challenge has already expired (expires_at ${details.expires_at}); not signing`, 0);
|
|
438
|
+
let validAfter;
|
|
439
|
+
let validBefore;
|
|
440
|
+
try {
|
|
441
|
+
({validAfter, validBefore} = computePaymentValidityWindow({
|
|
442
|
+
challengeExpiresAtSec: expiresAtSec,
|
|
443
|
+
nowSec
|
|
444
|
+
}));
|
|
445
|
+
} catch (cause) {
|
|
446
|
+
throw new X402Error(cause instanceof Error ? cause.message : String(cause), 0, void 0, { cause });
|
|
447
|
+
}
|
|
448
|
+
const { authorization, signature } = await signInteractionPayment({
|
|
449
|
+
sign: (typedData) => options.signer.signTypedData(typedData),
|
|
450
|
+
payer: options.signer.address,
|
|
451
|
+
domain: {
|
|
452
|
+
name: pr.extra.name,
|
|
453
|
+
version: pr.extra.version,
|
|
454
|
+
chainId,
|
|
455
|
+
verifyingContract: pr.asset
|
|
456
|
+
},
|
|
457
|
+
payTo: pr.payTo,
|
|
458
|
+
amount: BigInt(pr.maxAmountRequired),
|
|
459
|
+
nonceBinding: {
|
|
460
|
+
interactionId: details.nonce_binding.interaction_id,
|
|
461
|
+
challengeStepId: details.nonce_binding.challenge_step_id,
|
|
462
|
+
challengeNonce: details.nonce_binding.challenge_nonce
|
|
463
|
+
},
|
|
464
|
+
validAfter,
|
|
465
|
+
validBefore
|
|
466
|
+
});
|
|
467
|
+
const payment = buildExactEvmPaymentPayload({
|
|
468
|
+
network,
|
|
469
|
+
authorization,
|
|
470
|
+
signature
|
|
471
|
+
});
|
|
472
|
+
return buildPaymentStepEnvelope({
|
|
473
|
+
interactionId: challenge.interaction_id,
|
|
474
|
+
stepId: randomUUID(),
|
|
475
|
+
prevStepId: details.nonce_binding.challenge_step_id,
|
|
476
|
+
payment
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
304
480
|
* Pay a challenge (payer side). Derives the interaction-bound authorization,
|
|
305
481
|
* signs it locally with the caller's key, and submits it for settlement.
|
|
306
482
|
*/
|
|
@@ -423,4 +599,4 @@ function createX402Client(options = {}) {
|
|
|
423
599
|
return new X402Client(options);
|
|
424
600
|
}
|
|
425
601
|
//#endregion
|
|
426
|
-
export { DEFAULT_MAX_WINDOW_SEC, TRANSFER_WITH_AUTHORIZATION_TYPES, X402Client, X402Error, buildExactEvmPaymentPayload, buildPayoutRegistrationMessage, computePaymentValidityWindow, createX402Client, deriveEip3009Nonce, signInteractionPayment, toPaymentPayload, transferWithAuthorizationTypedData };
|
|
602
|
+
export { DEFAULT_MAX_WINDOW_SEC, TRANSFER_WITH_AUTHORIZATION_TYPES, X402Client, X402Error, X402_INTERACTION_PROTOCOL, X402_INTERACTION_PROTOCOL_VERSION, buildExactEvmPaymentPayload, buildPaymentStepEnvelope, buildPayoutRegistrationMessage, computePaymentValidityWindow, createX402Client, deriveEip3009Nonce, signInteractionPayment, toPaymentPayload, transferWithAuthorizationTypedData };
|
package/package.json
CHANGED