payid 0.6.0 → 1.0.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.
Files changed (38) hide show
  1. package/dist/{chunk-BLIRABV7.js → chunk-AUW7WDAB.js} +11 -20
  2. package/dist/{chunk-SSO66YQI.js → chunk-E6VQETBC.js} +13 -0
  3. package/dist/{chunk-TQJUKEFO.js → chunk-ESTGPUEQ.js} +24 -21
  4. package/dist/{chunk-2VO4XLTT.js → chunk-EZ3BGZ7G.js} +19 -14
  5. package/dist/chunk-FZNMDGVK.js +24 -0
  6. package/dist/chunk-HKHRYRD6.js +752 -0
  7. package/dist/chunk-X7NYQ47Y.js +27 -0
  8. package/dist/{chunk-Q4UZCGU4.js → chunk-XMUHMJRD.js} +1 -1
  9. package/dist/context/index.d.ts +3 -2
  10. package/dist/context.v1-C1m-tz0o.d.ts +39 -0
  11. package/dist/context.v2-DIzPotmW.d.ts +37 -0
  12. package/dist/core/client/index.d.ts +5 -4
  13. package/dist/core/client/index.js +9 -5
  14. package/dist/core/server/index.d.ts +4 -3
  15. package/dist/core/server/index.js +7 -4
  16. package/dist/{index-2JCvey4-.d.ts → index-CDnE3SGM.d.ts} +18 -3
  17. package/dist/index-CsynGAGv.d.ts +53 -0
  18. package/dist/{index-Dj9IEios.d.ts → index-CubM9whW.d.ts} +4 -17
  19. package/dist/{index-C1DHMQA0.d.ts → index-DSxDlF9J.d.ts} +45 -68
  20. package/dist/{index-BEvnPzzt.d.ts → index-Dm2VdDEB.d.ts} +2 -1
  21. package/dist/{index-DSHZvYii.d.ts → index-G_1SiZJo.d.ts} +8 -7
  22. package/dist/index.d.ts +407 -72
  23. package/dist/index.js +584 -79
  24. package/dist/issuer/index.d.ts +3 -2
  25. package/dist/issuer/index.js +4 -1
  26. package/dist/rule/index.d.ts +2 -2
  27. package/dist/rule/index.js +4 -3
  28. package/dist/rule-a_5ed-93.d.ts +39 -0
  29. package/dist/sessionPolicy/index.d.ts +3 -3
  30. package/dist/sessionPolicy/index.js +2 -2
  31. package/dist/{types-CpXiPRYs.d.ts → types-D2o6XS7a.d.ts} +1 -1
  32. package/dist/types-i4eTkhWa.d.ts +50 -0
  33. package/package.json +22 -9
  34. package/src/rule/engine/rule_engine.wasm +0 -0
  35. package/dist/chunk-DZ6GVRER.js +0 -309
  36. package/dist/chunk-QC24X74O.js +0 -41
  37. package/dist/index-CtdogR8X.d.ts +0 -82
  38. package/dist/types-B8pJQdMQ.d.ts +0 -26
@@ -0,0 +1,752 @@
1
+ import {
2
+ hashContext,
3
+ hashRuleSet
4
+ } from "./chunk-X7NYQ47Y.js";
5
+ import {
6
+ randomHex
7
+ } from "./chunk-KDC67LIN.js";
8
+
9
+ // src/attestation/verify.ts
10
+ import { ethers, keccak256, toUtf8Bytes, toBeArray } from "ethers";
11
+ function verifyAttestation(payload, proof, trustedIssuers) {
12
+ const now = Math.floor(Date.now() / 1e3);
13
+ if (!trustedIssuers.has(proof.issuer)) {
14
+ throw new Error("UNTRUSTED_ATTESTATION_ISSUER");
15
+ }
16
+ if (proof.expiresAt < now) {
17
+ throw new Error("ATTESTATION_EXPIRED");
18
+ }
19
+ if (proof.issuedAt > now) {
20
+ throw new Error("ATTESTATION_ISSUED_IN_FUTURE");
21
+ }
22
+ const payloadHash = keccak256(toUtf8Bytes(JSON.stringify(payload)));
23
+ const messageHash = ethers.hashMessage(toBeArray(payloadHash));
24
+ const recovered = ethers.recoverAddress(messageHash, proof.signature);
25
+ if (recovered.toLowerCase() !== proof.issuer.toLowerCase()) {
26
+ throw new Error("INVALID_ATTESTATION_SIGNATURE");
27
+ }
28
+ }
29
+
30
+ // src/rule/engine/wasm.ts
31
+ var _wasmUrl = "https://gateway.pinata.cloud/ipfs/bafkreigwfxsb7oot7v55x7vxslvj23csxl2fhk2w7hsnboe55o26s2mgfy";
32
+ var _instance = null;
33
+ var _loading = null;
34
+ var wasiStub = {
35
+ fd_write: () => 8,
36
+ fd_read: () => 8,
37
+ fd_close: () => 0,
38
+ fd_seek: () => 8,
39
+ fd_fdstat_get: () => 8,
40
+ fd_prestat_get: () => 8,
41
+ fd_prestat_dir_name: () => 8,
42
+ environ_get: () => 0,
43
+ environ_sizes_get: () => 0,
44
+ args_get: () => 0,
45
+ args_sizes_get: () => 0,
46
+ clock_time_get: () => 0,
47
+ proc_exit: () => {
48
+ },
49
+ random_get: () => 0
50
+ };
51
+ async function compile(binary) {
52
+ const module = await WebAssembly.compile(binary);
53
+ const instance = await WebAssembly.instantiate(module, {
54
+ wasi_snapshot_preview1: wasiStub
55
+ });
56
+ const _init = instance.exports._initialize;
57
+ if (_init) _init();
58
+ return instance;
59
+ }
60
+ async function loadWasm(binary) {
61
+ if (binary && binary.length > 0) {
62
+ return compile(binary instanceof Uint8Array ? binary : new Uint8Array(binary));
63
+ }
64
+ if (_instance) return _instance;
65
+ if (_loading) return _loading;
66
+ _loading = (async () => {
67
+ let wasmBinary;
68
+ if (typeof window === "undefined" && typeof process !== "undefined" && process.versions?.node) {
69
+ const { readFileSync } = await import(
70
+ /* webpackIgnore: true */
71
+ "fs"
72
+ );
73
+ const { join, dirname } = await import(
74
+ /* webpackIgnore: true */
75
+ "path"
76
+ );
77
+ const { fileURLToPath } = await import(
78
+ /* webpackIgnore: true */
79
+ "url"
80
+ );
81
+ const __dir = dirname(fileURLToPath(import.meta.url));
82
+ const buf = readFileSync(join(__dir, "rule_engine.wasm"));
83
+ wasmBinary = new Uint8Array(buf);
84
+ } else {
85
+ const res = await fetch(_wasmUrl);
86
+ if (!res.ok) throw new Error(
87
+ `[PAY.ID] Failed to fetch WASM from "${_wasmUrl}": HTTP ${res.status}`
88
+ );
89
+ wasmBinary = new Uint8Array(await res.arrayBuffer());
90
+ }
91
+ const instance = await compile(wasmBinary);
92
+ _instance = instance;
93
+ _loading = null;
94
+ return instance;
95
+ })();
96
+ return _loading;
97
+ }
98
+
99
+ // src/rule/engine/sandbox.ts
100
+ var enc = new TextEncoder();
101
+ var dec = new TextDecoder();
102
+ async function runWasmRule(context, config, wasmBinary) {
103
+ const instance = await loadWasm(wasmBinary);
104
+ const memory = instance.exports.memory;
105
+ const alloc = instance.exports.alloc;
106
+ const free_ = instance.exports.free;
107
+ const evaluate2 = instance.exports.evaluate;
108
+ if (!alloc || !evaluate2) {
109
+ throw new Error(`WASM missing exports: alloc=${!!alloc} evaluate=${!!evaluate2}`);
110
+ }
111
+ const ctxBuf = enc.encode(JSON.stringify(context));
112
+ const cfgBuf = enc.encode(JSON.stringify(config));
113
+ const OUT_SIZE = 4096;
114
+ const ctxPtr = alloc(ctxBuf.length);
115
+ const cfgPtr = alloc(cfgBuf.length);
116
+ const outPtr = alloc(OUT_SIZE);
117
+ new Uint8Array(memory.buffer).set(ctxBuf, ctxPtr);
118
+ new Uint8Array(memory.buffer).set(cfgBuf, cfgPtr);
119
+ let rc;
120
+ try {
121
+ rc = evaluate2(ctxPtr, ctxBuf.length, cfgPtr, cfgBuf.length, outPtr, OUT_SIZE);
122
+ } catch (err) {
123
+ throw new Error(`WASM evaluate threw: ${err}`);
124
+ }
125
+ if (rc < 0) throw new Error(`WASM evaluate failed rc=${rc}`);
126
+ const out = new Uint8Array(memory.buffer).slice(outPtr, outPtr + rc);
127
+ const result = JSON.parse(dec.decode(out));
128
+ if (free_) {
129
+ free_(ctxPtr, ctxBuf.length);
130
+ free_(cfgPtr, cfgBuf.length);
131
+ free_(outPtr, OUT_SIZE);
132
+ }
133
+ return result;
134
+ }
135
+
136
+ // src/rule/engine/tsSandbox.ts
137
+ async function runWasmRule2(context, config, _wasmBinary) {
138
+ return evaluateRule(context, config);
139
+ }
140
+ function evaluateRule(context, config) {
141
+ const rules = config?.rules;
142
+ if (!Array.isArray(rules) || rules.length === 0) {
143
+ return { decision: "ALLOW", code: "NO_RULES", reason: "no rules defined" };
144
+ }
145
+ const logic = config?.logic ?? "AND";
146
+ return evalRules(context, rules, logic);
147
+ }
148
+ function evalRules(context, rules, logic) {
149
+ for (const rule of rules) {
150
+ const res = evalOneRule(context, rule);
151
+ if (res.decision === "REJECT" && logic === "AND") return res;
152
+ if (res.decision === "ALLOW" && logic === "OR") return res;
153
+ }
154
+ if (logic === "AND") return { decision: "ALLOW", code: "OK", reason: "all rules passed" };
155
+ return { decision: "REJECT", code: "NO_RULE_MATCH", reason: "no rule matched in OR group" };
156
+ }
157
+ function evalOneRule(context, rule) {
158
+ const ruleId = rule?.id ?? "UNKNOWN_RULE";
159
+ const message = rule?.message ?? "";
160
+ if (Array.isArray(rule?.rules)) {
161
+ const subLogic = rule?.logic ?? "AND";
162
+ const res = evalRules(context, rule.rules, subLogic);
163
+ if (res.decision === "REJECT" && message) {
164
+ return { decision: "REJECT", code: ruleId, reason: message };
165
+ }
166
+ return res;
167
+ }
168
+ if (Array.isArray(rule?.conditions)) {
169
+ const inner = rule?.logic ?? "AND";
170
+ for (const cond of rule.conditions) {
171
+ const passed = evalCondition(context, cond);
172
+ if (!passed && inner === "AND") {
173
+ const reason = message || cond?.field || ruleId;
174
+ return { decision: "REJECT", code: ruleId, reason };
175
+ }
176
+ if (passed && inner === "OR") {
177
+ return { decision: "ALLOW", code: ruleId };
178
+ }
179
+ }
180
+ if (inner === "AND") return { decision: "ALLOW", code: ruleId };
181
+ return { decision: "REJECT", code: ruleId, reason: message || "no condition matched in OR" };
182
+ }
183
+ if (rule?.if !== void 0) {
184
+ const passed = evalCondition(context, rule.if);
185
+ if (!passed) {
186
+ const reason = message || rule.if?.field || ruleId;
187
+ return {
188
+ decision: "REJECT",
189
+ code: ruleId,
190
+ reason: interpolate(reason, context)
191
+ };
192
+ }
193
+ return { decision: "ALLOW", code: ruleId };
194
+ }
195
+ return { decision: "REJECT", code: ruleId, reason: "rule has no evaluable condition" };
196
+ }
197
+ function evalCondition(context, cond) {
198
+ const fieldExpr = cond?.field;
199
+ const op = cond?.op;
200
+ if (!fieldExpr || !op) return false;
201
+ const baseField = splitTransform(fieldExpr)[0];
202
+ if (op === "exists") return resolveField(context, baseField) !== void 0;
203
+ if (op === "not_exists") return resolveField(context, baseField) === void 0;
204
+ const actualRaw = resolveField(context, baseField);
205
+ if (actualRaw === void 0) return false;
206
+ const actual = applyTransform(actualRaw, fieldExpr);
207
+ let expected = cond.value;
208
+ if (typeof expected === "string" && expected.startsWith("$")) {
209
+ const refField = expected.slice(1);
210
+ const refBase = splitTransform(refField)[0];
211
+ const refRaw = resolveField(context, refBase);
212
+ if (refRaw === void 0) return false;
213
+ expected = applyTransform(refRaw, refField);
214
+ }
215
+ return applyOp(actual, op, expected);
216
+ }
217
+ function resolveField(ctx, path) {
218
+ const base = splitTransform(path)[0];
219
+ return base.split(".").reduce((o, k) => o?.[k], ctx);
220
+ }
221
+ function splitTransform(expr) {
222
+ const i = expr.indexOf("|");
223
+ if (i === -1) return [expr, null];
224
+ return [expr.slice(0, i), expr.slice(i + 1)];
225
+ }
226
+ function applyTransform(val, expr) {
227
+ const transform = splitTransform(expr)[1];
228
+ if (!transform) return val;
229
+ const colonIdx = transform.indexOf(":");
230
+ const name = colonIdx === -1 ? transform : transform.slice(0, colonIdx);
231
+ const arg = colonIdx === -1 ? null : transform.slice(colonIdx + 1);
232
+ const n = toU128(val);
233
+ switch (name) {
234
+ case "div": {
235
+ if (n === null) return val;
236
+ const d = arg ? BigInt(arg) : 1n;
237
+ if (d === 0n) return val;
238
+ return Number(n / d);
239
+ }
240
+ case "mod": {
241
+ if (n === null) return val;
242
+ const m = arg ? BigInt(arg) : 1n;
243
+ if (m === 0n) return val;
244
+ return Number(n % m);
245
+ }
246
+ case "abs":
247
+ return n !== null ? Number(n < 0n ? -n : n) : val;
248
+ case "hour":
249
+ return n !== null ? Number(n % 86400n / 3600n) : val;
250
+ case "day":
251
+ return n !== null ? Number((n / 86400n + 4n) % 7n) : val;
252
+ case "date":
253
+ return n !== null ? dayOfMonth(Number(n / 86400n)) : val;
254
+ case "month":
255
+ return n !== null ? monthOfYear(Number(n / 86400n)) : val;
256
+ case "len":
257
+ return String(val).length;
258
+ case "lower":
259
+ return String(val).toLowerCase();
260
+ case "upper":
261
+ return String(val).toUpperCase();
262
+ default:
263
+ return val;
264
+ }
265
+ }
266
+ function toU128(v) {
267
+ try {
268
+ if (typeof v === "bigint") return v;
269
+ if (typeof v === "number") return BigInt(Math.trunc(v));
270
+ if (typeof v === "string" && v !== "") return BigInt(v);
271
+ return null;
272
+ } catch {
273
+ return null;
274
+ }
275
+ }
276
+ function isLeap(y) {
277
+ return y % 4 === 0 && y % 100 !== 0 || y % 400 === 0;
278
+ }
279
+ function daysToYMD(days) {
280
+ let y = 1970;
281
+ while (true) {
282
+ const dy = isLeap(y) ? 366 : 365;
283
+ if (days < dy) break;
284
+ days -= dy;
285
+ y++;
286
+ }
287
+ const months = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
288
+ if (isLeap(y)) months[2] = 29;
289
+ let m = 1;
290
+ while (true) {
291
+ if (days < months[m]) break;
292
+ days -= months[m];
293
+ m++;
294
+ }
295
+ return [y, m, days + 1];
296
+ }
297
+ function dayOfMonth(days) {
298
+ return daysToYMD(days)[2];
299
+ }
300
+ function monthOfYear(days) {
301
+ return daysToYMD(days)[1];
302
+ }
303
+ function isSafeRegex(pattern) {
304
+ if (pattern.length > 200) return false;
305
+ if (/\([^)]*[+*]\s*[+*]/.test(pattern)) return false;
306
+ if (/(\([^)]*[+*][^)]*\))[+*{]/.test(pattern)) return false;
307
+ if (/\((?:[^|)]+\|)+[^)]*\)[+*{]/.test(pattern)) return false;
308
+ return true;
309
+ }
310
+ function applyOp(actual, op, expected) {
311
+ const a = toU128(actual);
312
+ const b = toU128(expected);
313
+ switch (op) {
314
+ case ">=":
315
+ return a !== null && b !== null && a >= b;
316
+ case "<=":
317
+ return a !== null && b !== null && a <= b;
318
+ case ">":
319
+ return a !== null && b !== null && a > b;
320
+ case "<":
321
+ return a !== null && b !== null && a < b;
322
+ case "==":
323
+ return String(actual) === String(expected) || actual == expected;
324
+ case "!=":
325
+ return String(actual) !== String(expected) && actual != expected;
326
+ case "in":
327
+ return Array.isArray(expected) && expected.some((e) => looseEq(actual, e));
328
+ case "not_in":
329
+ return Array.isArray(expected) && !expected.some((e) => looseEq(actual, e));
330
+ case "between":
331
+ return Array.isArray(expected) && expected.length === 2 && a !== null && toU128(expected[0]) !== null && toU128(expected[1]) !== null && a >= toU128(expected[0]) && a <= toU128(expected[1]);
332
+ case "not_between":
333
+ return Array.isArray(expected) && expected.length === 2 && a !== null && (a < toU128(expected[0]) || a > toU128(expected[1]));
334
+ case "mod_eq":
335
+ return Array.isArray(expected) && expected.length === 2 && a !== null && toU128(expected[0]) !== null && toU128(expected[0]) > 0n && a % toU128(expected[0]) === toU128(expected[1]);
336
+ case "mod_ne":
337
+ return Array.isArray(expected) && expected.length === 2 && a !== null && toU128(expected[0]) !== null && toU128(expected[0]) > 0n && a % toU128(expected[0]) !== toU128(expected[1]);
338
+ case "contains":
339
+ return typeof actual === "string" && typeof expected === "string" && actual.includes(expected);
340
+ case "not_contains":
341
+ return typeof actual === "string" && typeof expected === "string" && !actual.includes(expected);
342
+ case "starts_with":
343
+ return typeof actual === "string" && typeof expected === "string" && actual.startsWith(expected);
344
+ case "ends_with":
345
+ return typeof actual === "string" && typeof expected === "string" && actual.endsWith(expected);
346
+ case "exists":
347
+ return actual !== void 0 && actual !== null;
348
+ case "not_exists":
349
+ return actual === void 0 || actual === null;
350
+ case "regex": {
351
+ if (typeof actual !== "string" || typeof expected !== "string") return false;
352
+ if (!isSafeRegex(expected)) throw new Error(`UNSAFE_REGEX_PATTERN: ${expected.slice(0, 40)}`);
353
+ return new RegExp(expected).test(actual);
354
+ }
355
+ case "not_regex": {
356
+ if (typeof actual !== "string" || typeof expected !== "string") return false;
357
+ if (!isSafeRegex(expected)) throw new Error(`UNSAFE_REGEX_PATTERN: ${expected.slice(0, 40)}`);
358
+ return !new RegExp(expected).test(actual);
359
+ }
360
+ default:
361
+ return false;
362
+ }
363
+ }
364
+ function looseEq(a, b) {
365
+ if (a == b) return true;
366
+ if (String(a) === String(b)) return true;
367
+ const ba = toU128(a), bb = toU128(b);
368
+ return ba !== null && bb !== null && ba === bb;
369
+ }
370
+ function interpolate(template, context) {
371
+ return template.replace(/\{([^}]+)\}/g, (_, key) => {
372
+ const base = splitTransform(key)[0];
373
+ const raw = resolveField(context, base);
374
+ if (raw === void 0) return `{${key}}`;
375
+ const val = applyTransform(raw, key);
376
+ return String(val);
377
+ });
378
+ }
379
+
380
+ // src/rule/engine/validateRequires.ts
381
+ function getByPath(obj, path) {
382
+ return path.split(".").reduce((acc, key) => acc?.[key], obj);
383
+ }
384
+ function validateRequiredContext(context, requires) {
385
+ if (!requires) return;
386
+ for (const path of requires) {
387
+ const value = getByPath(context, path);
388
+ if (value === void 0 || value === null) {
389
+ throw new Error(`Missing required context field: ${path}`);
390
+ }
391
+ }
392
+ }
393
+
394
+ // src/rule/engine/preprocess.ts
395
+ function preprocessContextV2(context, ruleConfig, trustedIssuers) {
396
+ validateRequiredContext(context, ruleConfig.requires);
397
+ if (context.env) {
398
+ verifyAttestation(
399
+ { timestamp: context.env.timestamp },
400
+ context.env.proof,
401
+ trustedIssuers
402
+ );
403
+ }
404
+ if (context.state) {
405
+ verifyAttestation(
406
+ {
407
+ spentToday: context.state.spentToday,
408
+ period: context.state.period
409
+ },
410
+ context.state.proof,
411
+ trustedIssuers
412
+ );
413
+ }
414
+ if (context.oracle) {
415
+ const { proof, ...data } = context.oracle;
416
+ verifyAttestation(data, proof, trustedIssuers);
417
+ }
418
+ if (context.risk) {
419
+ verifyAttestation(
420
+ {
421
+ score: context.risk.score,
422
+ category: context.risk.category,
423
+ modelHash: context.risk.proof.modelHash
424
+ },
425
+ context.risk.proof,
426
+ trustedIssuers
427
+ );
428
+ }
429
+ return context;
430
+ }
431
+
432
+ // src/rule/engine/index.ts
433
+ async function executeRule(context, ruleConfig, wasmBinary) {
434
+ try {
435
+ return await runWasmRule(context, ruleConfig, wasmBinary);
436
+ } catch {
437
+ return runWasmRule2(context, ruleConfig, wasmBinary);
438
+ }
439
+ }
440
+
441
+ // src/normalize.ts
442
+ function normalizeContext(ctx) {
443
+ return {
444
+ ...ctx,
445
+ tx: {
446
+ ...ctx.tx,
447
+ sender: ctx.tx.sender,
448
+ receiver: ctx.tx.receiver,
449
+ asset: ctx.tx.asset,
450
+ amountUsd: ctx.tx.amountUsd
451
+ }
452
+ };
453
+ }
454
+
455
+ // src/core/decisionTrace.ts
456
+ function toBigIntSafe(v) {
457
+ try {
458
+ if (typeof v === "bigint") return v;
459
+ if (typeof v === "number" && Number.isFinite(v)) return BigInt(Math.trunc(v));
460
+ if (typeof v === "string" && v !== "") return BigInt(v);
461
+ return null;
462
+ } catch {
463
+ return null;
464
+ }
465
+ }
466
+ function resolveField2(obj, fieldExpr) {
467
+ const [path, ...transforms] = fieldExpr.split("|");
468
+ let value = path?.split(".").reduce((o, k) => o?.[k], obj);
469
+ for (const t of transforms) {
470
+ if (value === void 0 || value === null) break;
471
+ if (t.startsWith("div:")) {
472
+ const n = Number(t.slice(4));
473
+ value = Number(value) / n;
474
+ } else if (t.startsWith("mod:")) {
475
+ const n = BigInt(t.slice(4));
476
+ value = BigInt(value) % n;
477
+ } else if (t === "abs") {
478
+ value = Math.abs(Number(value));
479
+ } else if (t === "hour") {
480
+ value = new Date(Number(value) * 1e3).getUTCHours();
481
+ } else if (t === "day") {
482
+ value = new Date(Number(value) * 1e3).getUTCDay();
483
+ } else if (t === "date") {
484
+ value = new Date(Number(value) * 1e3).getUTCDate();
485
+ } else if (t === "month") {
486
+ value = new Date(Number(value) * 1e3).getUTCMonth() + 1;
487
+ } else if (t === "len") {
488
+ value = String(value).length;
489
+ } else if (t === "lower") {
490
+ value = String(value).toLowerCase();
491
+ } else if (t === "upper") {
492
+ value = String(value).toUpperCase();
493
+ }
494
+ }
495
+ return value;
496
+ }
497
+ function resolveValue(context, value) {
498
+ if (typeof value === "string" && value.startsWith("$")) {
499
+ return resolveField2(context, value.slice(1));
500
+ }
501
+ return value;
502
+ }
503
+ function evaluateCondition(actual, op, expected) {
504
+ switch (op) {
505
+ case ">=":
506
+ case "<=":
507
+ case ">":
508
+ case "<": {
509
+ const a = toBigIntSafe(actual);
510
+ const b = toBigIntSafe(expected);
511
+ if (a === null || b === null) return false;
512
+ if (op === ">=") return a >= b;
513
+ if (op === "<=") return a <= b;
514
+ if (op === ">") return a > b;
515
+ if (op === "<") return a < b;
516
+ return false;
517
+ }
518
+ case "==":
519
+ return actual == expected;
520
+ case "!=":
521
+ return actual != expected;
522
+ case "in":
523
+ return Array.isArray(expected) && expected.includes(actual);
524
+ case "not_in":
525
+ return Array.isArray(expected) && !expected.includes(actual);
526
+ case "between":
527
+ return Array.isArray(expected) && actual >= expected[0] && actual <= expected[1];
528
+ case "not_between":
529
+ return Array.isArray(expected) && !(actual >= expected[0] && actual <= expected[1]);
530
+ case "exists":
531
+ return actual !== void 0 && actual !== null;
532
+ case "not_exists":
533
+ return actual === void 0 || actual === null;
534
+ default:
535
+ return false;
536
+ }
537
+ }
538
+ function traceCondition(context, ruleId, cond) {
539
+ const actual = resolveField2(context, cond.field);
540
+ const expected = resolveValue(context, cond.value);
541
+ const pass = evaluateCondition(actual, cond.op, expected);
542
+ return {
543
+ ruleId,
544
+ field: cond.field,
545
+ op: cond.op,
546
+ expected: cond.value,
547
+ actual,
548
+ result: actual === void 0 ? "FAIL" : pass ? "PASS" : "FAIL"
549
+ };
550
+ }
551
+ function traceRule(context, rule) {
552
+ if ("if" in rule) {
553
+ return [traceCondition(context, rule.id, rule.if)];
554
+ }
555
+ if ("conditions" in rule) {
556
+ return rule.conditions.map((cond) => traceCondition(context, rule.id, cond));
557
+ }
558
+ if ("rules" in rule) {
559
+ return rule.rules.flatMap((child) => traceRule(context, child));
560
+ }
561
+ return [];
562
+ }
563
+ function buildDecisionTrace(context, ruleConfig) {
564
+ return ruleConfig.rules.flatMap((rule) => traceRule(context, rule));
565
+ }
566
+
567
+ // src/evaluate.ts
568
+ async function evaluate(context, ruleConfig, options, wasmBinary) {
569
+ if (!context || typeof context !== "object") {
570
+ throw new Error("evaluate(): context is required");
571
+ }
572
+ if (!context.tx) {
573
+ throw new Error("evaluate(): context.tx is required");
574
+ }
575
+ if (!ruleConfig || typeof ruleConfig !== "object") {
576
+ throw new Error("evaluate(): ruleConfig is required");
577
+ }
578
+ let result;
579
+ try {
580
+ const preparedContext = options?.trustedIssuers ? preprocessContextV2(context, ruleConfig, options.trustedIssuers) : context;
581
+ const normalized = normalizeContext(preparedContext);
582
+ result = await executeRule(normalized, ruleConfig, wasmBinary);
583
+ } catch (err) {
584
+ return {
585
+ decision: "REJECT",
586
+ code: "CONTEXT_OR_ENGINE_ERROR",
587
+ reason: err?.message ?? "rule evaluation failed"
588
+ };
589
+ }
590
+ if (result.decision !== "ALLOW" && result.decision !== "REJECT") {
591
+ return {
592
+ decision: "REJECT",
593
+ code: "INVALID_ENGINE_OUTPUT",
594
+ reason: "invalid decision value"
595
+ };
596
+ }
597
+ const baseResult = {
598
+ decision: result.decision,
599
+ code: result.code || "UNKNOWN",
600
+ reason: result.reason
601
+ };
602
+ if (options?.debug) {
603
+ return {
604
+ ...baseResult,
605
+ debug: {
606
+ trace: buildDecisionTrace(context, ruleConfig)
607
+ }
608
+ };
609
+ }
610
+ return baseResult;
611
+ }
612
+
613
+ // src/decision-proof/generate.ts
614
+ import { ethers as ethers2, ZeroAddress } from "ethers";
615
+ var hash = (v) => ethers2.keccak256(ethers2.toUtf8Bytes(v));
616
+ async function generateDecisionProof(params) {
617
+ if (!ethers2.isAddress(params.payer) || params.payer === ethers2.ZeroAddress) {
618
+ throw new Error("GENERATE_PROOF: payer address tidak valid atau zero");
619
+ }
620
+ if (!ethers2.isAddress(params.receiver) || params.receiver === ethers2.ZeroAddress) {
621
+ throw new Error("GENERATE_PROOF: receiver address tidak valid atau zero");
622
+ }
623
+ if (!ethers2.isAddress(params.verifyingContract) || params.verifyingContract === ethers2.ZeroAddress) {
624
+ throw new Error("GENERATE_PROOF: verifyingContract tidak valid atau zero");
625
+ }
626
+ if (params.amount <= 0n) {
627
+ throw new Error("GENERATE_PROOF: amount harus > 0");
628
+ }
629
+ const now = params.blockTimestamp ?? Math.floor(Date.now() / 1e3);
630
+ const issuedAt = now - 30;
631
+ const expiresAt = now + (params.ttlSeconds ?? 300);
632
+ let chainId = params.chainId;
633
+ if (!chainId && params.signer.provider) {
634
+ const network = await params.signer.provider.getNetwork();
635
+ chainId = Number(network.chainId);
636
+ }
637
+ if (!chainId || chainId <= 0 || !Number.isInteger(chainId)) {
638
+ throw new Error(`GENERATE_PROOF: chainId tidak valid: ${chainId}`);
639
+ }
640
+ const requiresAttestation = Array.isArray(params.ruleConfig?.requires) && params.ruleConfig.requires.length > 0;
641
+ const attestationUIDsHash = params.attestationUIDs ? ethers2.keccak256(ethers2.AbiCoder.defaultAbiCoder().encode(["bytes32[]"], [params.attestationUIDs])) : ethers2.ZeroHash;
642
+ const payload = {
643
+ version: hash("2"),
644
+ payId: hash(params.payId),
645
+ payer: params.payer,
646
+ receiver: params.receiver,
647
+ asset: params.asset,
648
+ amount: params.amount,
649
+ contextHash: hashContext(params.context),
650
+ ruleSetHash: params.ruleSetHashOverride ?? hashRuleSet(params.ruleConfig),
651
+ ruleAuthority: params.ruleAuthority ?? ZeroAddress,
652
+ issuedAt: BigInt(issuedAt),
653
+ expiresAt: BigInt(expiresAt),
654
+ nonce: randomHex(32),
655
+ requiresAttestation,
656
+ attestationUIDsHash
657
+ };
658
+ const domain = {
659
+ name: "PAY.ID Decision",
660
+ version: "2",
661
+ chainId,
662
+ verifyingContract: params.verifyingContract
663
+ };
664
+ const types = {
665
+ Decision: [
666
+ { name: "version", type: "bytes32" },
667
+ { name: "payId", type: "bytes32" },
668
+ { name: "payer", type: "address" },
669
+ { name: "receiver", type: "address" },
670
+ { name: "asset", type: "address" },
671
+ { name: "amount", type: "uint256" },
672
+ { name: "contextHash", type: "bytes32" },
673
+ { name: "ruleSetHash", type: "bytes32" },
674
+ { name: "ruleAuthority", type: "address" },
675
+ { name: "issuedAt", type: "uint64" },
676
+ { name: "expiresAt", type: "uint64" },
677
+ { name: "nonce", type: "bytes32" },
678
+ { name: "requiresAttestation", type: "bool" },
679
+ { name: "attestationUIDsHash", type: "bytes32" }
680
+ ]
681
+ };
682
+ const signature = await params.signer.signTypedData(domain, types, payload);
683
+ const recovered = ethers2.verifyTypedData(domain, types, payload, signature);
684
+ const signerAddress = await params.signer.getAddress();
685
+ if (recovered.toLowerCase() !== signerAddress.toLowerCase()) {
686
+ throw new Error("SIGNATURE_MISMATCH");
687
+ }
688
+ return { payload, signature };
689
+ }
690
+
691
+ // src/utils/subtle.ts
692
+ var subtleCrypto = globalThis.crypto.subtle;
693
+
694
+ // src/utils/fetchJson.ts
695
+ async function fetchJsonWithHashCheck(url, expectedHash) {
696
+ const res = await fetch(url);
697
+ if (!res.ok) {
698
+ throw new Error("RULE_FETCH_FAILED");
699
+ }
700
+ const buffer = await res.arrayBuffer();
701
+ if (expectedHash) {
702
+ const digest = await subtleCrypto.digest(
703
+ "SHA-256",
704
+ buffer
705
+ );
706
+ const actualHash = bufferToHex(digest);
707
+ if (actualHash !== expectedHash) {
708
+ throw new Error("RULE_HASH_MISMATCH");
709
+ }
710
+ }
711
+ return JSON.parse(new TextDecoder().decode(buffer));
712
+ }
713
+ function bufferToHex(buffer) {
714
+ return [...new Uint8Array(buffer)].map((b) => b.toString(16).padStart(2, "0")).join("");
715
+ }
716
+
717
+ // src/resolver/resolver.ts
718
+ var DEFAULT_ZG_INDEXER = "https://indexer-testnet.0g.ai";
719
+ async function resolveRule(source, options) {
720
+ const { uri, hash: hash2 } = source;
721
+ if (uri.startsWith("inline://")) {
722
+ const encoded = uri.replace("inline://", "");
723
+ const json = JSON.parse(atob(encoded));
724
+ return { config: json, source };
725
+ }
726
+ if (uri.startsWith("ipfs://")) {
727
+ const cid = uri.replace("ipfs://", "");
728
+ const url = `https://ipfs.io/ipfs/${cid}`;
729
+ const config = await fetchJsonWithHashCheck(url, hash2);
730
+ return { config, source };
731
+ }
732
+ if (uri.startsWith("http://") || uri.startsWith("https://")) {
733
+ const config = await fetchJsonWithHashCheck(uri, hash2);
734
+ return { config, source };
735
+ }
736
+ if (uri.startsWith("0g://")) {
737
+ const rootHash = uri.replace("0g://", "");
738
+ const indexerUrl = options?.zgIndexerUrl ?? globalThis.PAYID_ZGS_INDEXER_URL ?? DEFAULT_ZG_INDEXER;
739
+ const url = `${indexerUrl}/blob/${rootHash}`;
740
+ const config = await fetchJsonWithHashCheck(url, hash2);
741
+ return { config, source };
742
+ }
743
+ throw new Error("UNSUPPORTED_RULE_URI");
744
+ }
745
+
746
+ export {
747
+ loadWasm,
748
+ verifyAttestation,
749
+ evaluate,
750
+ generateDecisionProof,
751
+ resolveRule
752
+ };