@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/LICENSE +21 -0
- package/README.md +36 -2
- package/dist/index.cjs +328 -0
- package/dist/index.d.cts +129 -0
- package/dist/index.d.mts +104 -0
- package/dist/index.d.ts +129 -0
- package/dist/index.js +303 -0
- package/dist/index.mjs +295 -0
- package/package.json +43 -3
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": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
}
|