@nullclear/x402-arbiter 0.0.0-stage → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,295 @@
1
+ // src/signer.ts
2
+ import { privateKeyToAccount } from "viem/accounts";
3
+ import {
4
+ EIP3009_TYPES,
5
+ getChainConfig,
6
+ encodeBase64Json
7
+ } from "@nullclear/shared";
8
+ async function signX402Payment(signer, option, resourceUrl) {
9
+ let accountAddress;
10
+ let signTypedDataFn;
11
+ if (typeof signer === "string" && signer.startsWith("0x")) {
12
+ const account = privateKeyToAccount(signer);
13
+ accountAddress = account.address;
14
+ signTypedDataFn = (params) => account.signTypedData(params);
15
+ } else if (typeof signer === "object" && signer !== null && "signTypedData" in signer && "account" in signer && signer.account) {
16
+ const rawAccount = signer.account;
17
+ accountAddress = typeof rawAccount === "string" ? rawAccount : rawAccount.address;
18
+ signTypedDataFn = (params) => signer.signTypedData(params);
19
+ } else {
20
+ throw new Error("Invalid signer provided to signX402Payment: must be private key or viem account/wallet client");
21
+ }
22
+ const randomBytes = new Uint8Array(32);
23
+ crypto.getRandomValues(randomBytes);
24
+ const nonce = `0x` + Array.from(randomBytes).map((b) => b.toString(16).padStart(2, "0")).join("");
25
+ const validAfter = "0";
26
+ const timeoutSeconds = option.maxTimeoutSeconds && option.maxTimeoutSeconds > 0 ? option.maxTimeoutSeconds : 300;
27
+ const validBefore = String(Math.floor(Date.now() / 1e3) + timeoutSeconds);
28
+ const chainConfig = getChainConfig(option.network);
29
+ const domain = {
30
+ name: chainConfig.usdcDomain.name,
31
+ version: chainConfig.usdcDomain.version,
32
+ chainId: chainConfig.chainId,
33
+ verifyingContract: chainConfig.usdc
34
+ };
35
+ const authorization = {
36
+ from: accountAddress,
37
+ to: option.payTo,
38
+ value: option.amount,
39
+ validAfter,
40
+ validBefore,
41
+ nonce
42
+ };
43
+ const signature = await signTypedDataFn({
44
+ domain,
45
+ types: EIP3009_TYPES,
46
+ primaryType: "TransferWithAuthorization",
47
+ message: {
48
+ from: authorization.from,
49
+ to: authorization.to,
50
+ value: BigInt(authorization.value),
51
+ validAfter: BigInt(authorization.validAfter),
52
+ validBefore: BigInt(authorization.validBefore),
53
+ nonce: authorization.nonce
54
+ }
55
+ });
56
+ const payload = {
57
+ x402Version: 2,
58
+ resource: resourceUrl ? { url: resourceUrl } : void 0,
59
+ accepted: option,
60
+ payload: {
61
+ signature,
62
+ authorization
63
+ }
64
+ };
65
+ return encodeBase64Json(payload);
66
+ }
67
+
68
+ // src/client.ts
69
+ var NullClearClient = class {
70
+ arbiterUrl;
71
+ defaultChainId;
72
+ customFetch;
73
+ constructor(config = {}) {
74
+ this.arbiterUrl = (config.arbiterUrl || "https://arbiter.nullclear.dev").replace(/\/$/, "");
75
+ this.defaultChainId = config.defaultChainId || 8453;
76
+ this.customFetch = config.fetch || fetch;
77
+ }
78
+ async getScore(agentId, chainId) {
79
+ const targetChainId = chainId || this.defaultChainId;
80
+ const res = await this.customFetch(`${this.arbiterUrl}/v1/score/${targetChainId}/${agentId}`);
81
+ if (!res.ok) {
82
+ const err = await res.text();
83
+ throw new Error(`Failed to fetch score for agent ${agentId}: ${res.status} ${err}`);
84
+ }
85
+ const data = await res.json();
86
+ return data.data;
87
+ }
88
+ async clear(request) {
89
+ const res = await this.customFetch(`${this.arbiterUrl}/v1/clear`, {
90
+ method: "POST",
91
+ headers: { "Content-Type": "application/json" },
92
+ body: JSON.stringify(request)
93
+ });
94
+ const data = await res.json();
95
+ return data;
96
+ }
97
+ async getKya(agentId, chainId) {
98
+ const targetChainId = chainId || this.defaultChainId;
99
+ const res = await this.customFetch(`${this.arbiterUrl}/v1/kya/${targetChainId}/${agentId}`);
100
+ if (!res.ok) {
101
+ if (res.status === 404) return null;
102
+ const err = await res.text();
103
+ throw new Error(`Failed to fetch KYA mandate for agent ${agentId}: ${res.status} ${err}`);
104
+ }
105
+ const data = await res.json();
106
+ return data.data;
107
+ }
108
+ async attestKya(mandate, chainId, signature, signerAddress) {
109
+ const targetChainId = chainId || this.defaultChainId;
110
+ const res = await this.customFetch(`${this.arbiterUrl}/v1/kya/attest`, {
111
+ method: "POST",
112
+ headers: { "Content-Type": "application/json" },
113
+ body: JSON.stringify({
114
+ mandate,
115
+ chainId: targetChainId,
116
+ signature,
117
+ signerAddress
118
+ })
119
+ });
120
+ if (!res.ok) {
121
+ const err = await res.text();
122
+ throw new Error(`Failed to attest KYA mandate: ${res.status} ${err}`);
123
+ }
124
+ const data = await res.json();
125
+ return data.data;
126
+ }
127
+ };
128
+
129
+ // src/fetch-wrapper.ts
130
+ import { z } from "zod";
131
+ import {
132
+ decodeBase64Json
133
+ } from "@nullclear/shared";
134
+ var NullClearEscrowAbortedError = class extends Error {
135
+ stage;
136
+ buyerCharged;
137
+ validationErrors;
138
+ score;
139
+ upstreamStatus;
140
+ reason;
141
+ constructor(message, details) {
142
+ super(message);
143
+ this.name = "NullClearEscrowAbortedError";
144
+ this.stage = details.stage;
145
+ this.buyerCharged = details.buyerCharged || "0";
146
+ this.validationErrors = details.validationErrors || [];
147
+ this.score = details.score;
148
+ this.upstreamStatus = details.upstreamStatus;
149
+ this.reason = details.reason;
150
+ }
151
+ };
152
+ function zodToJsonSchema(schema) {
153
+ const def = schema._def;
154
+ if (!def) return {};
155
+ const typeName = def.typeName;
156
+ switch (typeName) {
157
+ case "ZodString":
158
+ return { type: "string" };
159
+ case "ZodNumber":
160
+ return { type: "number" };
161
+ case "ZodBoolean":
162
+ return { type: "boolean" };
163
+ case "ZodArray":
164
+ return {
165
+ type: "array",
166
+ items: def.type ? zodToJsonSchema(def.type) : {}
167
+ };
168
+ case "ZodObject": {
169
+ const shape = def.shape();
170
+ const properties = {};
171
+ const required = [];
172
+ for (const [key, value] of Object.entries(shape)) {
173
+ properties[key] = zodToJsonSchema(value);
174
+ const subDef = value._def;
175
+ if (subDef.typeName !== "ZodOptional" && subDef.typeName !== "ZodNullable") {
176
+ required.push(key);
177
+ }
178
+ }
179
+ return {
180
+ type: "object",
181
+ properties,
182
+ required: required.length > 0 ? required : void 0
183
+ };
184
+ }
185
+ case "ZodEnum":
186
+ return {
187
+ type: "string",
188
+ enum: def.values
189
+ };
190
+ case "ZodOptional":
191
+ case "ZodNullable":
192
+ return def.innerType ? zodToJsonSchema(def.innerType) : {};
193
+ default:
194
+ return {};
195
+ }
196
+ }
197
+ function createNullClearFetch(config) {
198
+ const chainId = config.chainId || 8453;
199
+ const arbiterUrl = (config.arbiterUrl || "https://arbiter.nullclear.dev").replace(/\/$/, "");
200
+ const defaultMinTrustScore = config.defaultMinTrustScore || 50;
201
+ const customFetch = config.customFetch || fetch;
202
+ return async function nullclearFetch(input, init) {
203
+ const targetUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
204
+ const fetchInit = { ...init };
205
+ delete fetchInit.nullclear;
206
+ const initialRes = await customFetch(input, fetchInit);
207
+ if (initialRes.status !== 402) {
208
+ return initialRes;
209
+ }
210
+ const paymentRequiredHeader = initialRes.headers.get("PAYMENT-REQUIRED") || initialRes.headers.get("payment-required");
211
+ if (!paymentRequiredHeader) {
212
+ return initialRes;
213
+ }
214
+ let paymentRequired;
215
+ try {
216
+ paymentRequired = decodeBase64Json(paymentRequiredHeader);
217
+ } catch {
218
+ return initialRes;
219
+ }
220
+ const targetNetwork = `eip155:${chainId}`;
221
+ const selectedOption = paymentRequired.accepts.find(
222
+ (opt) => opt.network === targetNetwork && opt.scheme === "exact"
223
+ );
224
+ if (!selectedOption) {
225
+ throw new Error(
226
+ `Seller does not accept payments on network ${targetNetwork}. Accepted: ${paymentRequired.accepts.map((a) => a.network).join(", ")}`
227
+ );
228
+ }
229
+ const paymentSignatureHeader = await signX402Payment(
230
+ config.accountOrPrivateKey,
231
+ selectedOption,
232
+ targetUrl
233
+ );
234
+ let jsonSchema;
235
+ if (init?.nullclear?.schema) {
236
+ if (init.nullclear.schema instanceof z.ZodType) {
237
+ jsonSchema = zodToJsonSchema(init.nullclear.schema);
238
+ } else if (typeof init.nullclear.schema === "object") {
239
+ jsonSchema = init.nullclear.schema;
240
+ }
241
+ }
242
+ const clearPayload = {
243
+ targetUrl,
244
+ method: init?.method || "GET",
245
+ headers: init?.headers || {},
246
+ body: init?.body,
247
+ paymentSignatureHeader,
248
+ sellerAgentId: init?.nullclear?.sellerAgentId,
249
+ minTrustScore: init?.nullclear?.minTrustScore ?? defaultMinTrustScore,
250
+ validation: {
251
+ expectedStatus: [200],
252
+ jsonSchema,
253
+ requiredFields: init?.nullclear?.requiredFields,
254
+ sha256Match: init?.nullclear?.sha256Match
255
+ }
256
+ };
257
+ const arbiterRes = await customFetch(`${arbiterUrl}/v1/clear`, {
258
+ method: "POST",
259
+ headers: { "Content-Type": "application/json" },
260
+ body: JSON.stringify(clearPayload)
261
+ });
262
+ const arbiterData = await arbiterRes.json();
263
+ if (arbiterRes.status === 200 && arbiterData.cleared) {
264
+ const resBody = typeof arbiterData.data === "string" ? arbiterData.data : JSON.stringify(arbiterData.data);
265
+ const resHeaders = new Headers();
266
+ resHeaders.set("Content-Type", "application/json");
267
+ const paymentResponseHdr = arbiterRes.headers.get("PAYMENT-RESPONSE");
268
+ if (paymentResponseHdr) resHeaders.set("PAYMENT-RESPONSE", paymentResponseHdr);
269
+ const attestationHdr = arbiterRes.headers.get("X-NullClear-Attestation");
270
+ if (attestationHdr) resHeaders.set("X-NullClear-Attestation", attestationHdr);
271
+ return new Response(resBody, {
272
+ status: 200,
273
+ headers: resHeaders
274
+ });
275
+ }
276
+ throw new NullClearEscrowAbortedError(
277
+ `NullClear Escrow Aborted: ${arbiterData.stage || "FAILED"} - ${arbiterData.reason || arbiterData.error || (arbiterData.validationErrors || []).join("; ")}`,
278
+ {
279
+ stage: arbiterData.stage || "UNKNOWN_FAILURE",
280
+ buyerCharged: arbiterData.buyerCharged || "0",
281
+ validationErrors: arbiterData.validationErrors || [],
282
+ score: arbiterData.score,
283
+ upstreamStatus: arbiterData.upstreamStatus,
284
+ reason: arbiterData.reason || arbiterData.error
285
+ }
286
+ );
287
+ };
288
+ }
289
+ export {
290
+ NullClearClient,
291
+ NullClearEscrowAbortedError,
292
+ createNullClearFetch,
293
+ signX402Payment,
294
+ zodToJsonSchema
295
+ };
package/package.json CHANGED
@@ -1,6 +1,46 @@
1
1
  {
2
2
  "name": "@nullclear/x402-arbiter",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.1.1",
4
+ "description": "NullClear Developer and Agent SDK for safe x402 calls with deferred schema escrow",
5
+ "license": "MIT",
6
+ "homepage": "https://nullclear.dev",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/Nullclear-Protocol/nullclear.git"
10
+ },
11
+ "type": "module",
12
+ "main": "./dist/index.cjs",
13
+ "module": "./dist/index.js",
14
+ "types": "./dist/index.d.ts",
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "import": "./dist/index.js",
26
+ "require": "./dist/index.cjs"
27
+ }
28
+ },
29
+ "dependencies": {
30
+ "@nullclear/shared": "1.1.1",
31
+ "viem": "^2.23.0",
32
+ "zod": "^3.24.2"
33
+ },
34
+ "devDependencies": {
35
+ "@nullclear/worker-arbiter": "1.1.1",
36
+ "tsup": "^8.3.6",
37
+ "typescript": "^5.7.3",
38
+ "vitest": "^3.0.5"
39
+ },
40
+ "scripts": {
41
+ "build": "tsup src/index.ts --format esm,cjs --dts",
42
+ "typecheck": "tsc --noEmit",
43
+ "test": "vitest run",
44
+ "lint": "echo \"@nullclear/x402-arbiter clean\""
45
+ }
6
46
  }