@primitivedotdev/sdk 1.7.0 → 1.9.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-CwpE8_17.js} +178 -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-DZ7eZB3M.d.ts} +680 -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-03WHPOSm.js} +1508 -57
- 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 +2 -2
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@primitivedotdev/sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.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",
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
"dependencies": {
|
|
94
94
|
"ajv": "^8.17.1",
|
|
95
95
|
"mailparser": "^3.9.0",
|
|
96
|
-
"nodemailer": "^
|
|
96
|
+
"nodemailer": "^9.0.1",
|
|
97
97
|
"sanitize-html": "^2.14.0",
|
|
98
98
|
"tar-stream": "^3.1.8",
|
|
99
99
|
"validator": "^13.15.35",
|