@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.
@@ -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
- if (!("event" in obj) || typeof obj.event !== "string") throw new WebhookPayloadError("PAYLOAD_MISSING_EVENT", "Missing 'event' field in payload", "This doesn't look like a Primitive webhook payload.");
10024
- switch (obj.event) {
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
- default: return input;
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
- * Type guard to check if a webhook event is an EmailReceivedEvent.
10031
- *
10032
- * @example
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
- function isEmailReceivedEvent(event) {
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 handleWebhook(options) {
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
- return validateEmailReceivedEvent(parseJsonBody(body));
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 { PRIMITIVE_CONFIRMED_HEADER as A, STANDARD_WEBHOOK_ID_HEADER as C, verifyStandardWebhooksSignature as D, signStandardWebhooksPayload as E, verifyDownloadToken as F, safeValidateEmailReceivedEvent as I, validateEmailReceivedEvent as L, signWebhookPayload as M, verifyWebhookSignature as N, LEGACY_CONFIRMED_HEADER as O, generateDownloadToken as P, emailReceivedEventJsonSchema as S, STANDARD_WEBHOOK_TIMESTAMP_HEADER as T, DmarcResult as _, isDownloadExpired as a, ParsedStatus as b, parseWebhookEvent as c, WEBHOOK_VERSION as d, validateEmailAuth as f, DmarcPolicy as g, DkimResult as h, handleWebhook as i, PRIMITIVE_SIGNATURE_HEADER as j, LEGACY_SIGNATURE_HEADER as k, receive as l, AuthVerdict as m, decodeRawEmail as n, isEmailReceivedEvent as o, AuthConfidence as p, getDownloadTimeRemaining as r, isRawIncluded as s, confirmedHeaders as t, verifyRawEmailDownload as u, EventType as v, STANDARD_WEBHOOK_SIGNATURE_HEADER as w, SpfResult as x, ForwardVerdict as y };
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 };
@@ -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 };
@@ -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
- const pr = c.payment_requirements;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primitivedotdev/sdk",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "Official Primitive Node.js SDK: webhook, api, openapi, contract, and parser runtime modules.",
5
5
  "type": "module",
6
6
  "module": "./dist/index.js",