payid 1.0.5 → 2.0.3

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 CHANGED
@@ -1,15 +1,478 @@
1
- # sdk-core
2
-
3
- To install dependencies:
4
-
5
- ```bash
6
- bun install
7
- ```
8
-
9
- To run:
10
-
11
- ```bash
12
- bun run src/index.ts
13
- ```
14
-
15
- This project was created using `bun init` in bun v1.2.21. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
1
+ # PayID SDK — `payid`
2
+
3
+ Policy-driven payment engine for EVM chains. Define **rules** that evaluate payment context off-chain, then submit a cryptographic **Decision Proof** on-chain for verification.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ bun add payid ethers
9
+ # or
10
+ npm install payid ethers
11
+ ```
12
+
13
+ ---
14
+
15
+ ## Quick Start
16
+
17
+ ```typescript
18
+ import { PayIDClient } from "payid/client";
19
+ import { ethers } from "ethers";
20
+
21
+ const client = new PayIDClient();
22
+
23
+ const result = await client.evaluateAndProve({
24
+ context: {
25
+ tx: {
26
+ sender: "0xSender...",
27
+ receiver: "0xReceiver...",
28
+ asset: "0x0000000000000000000000000000000000000000", // native token
29
+ amount: "1000000000000000000", // 1 ETH in wei
30
+ chainId: 42161, // Arbitrum
31
+ },
32
+ },
33
+ authorityRule: myRuleConfig,
34
+ payId: "user@pay.id",
35
+ payer: "0xSender...",
36
+ receiver: "0xReceiver...",
37
+ asset: "0x0000000000000000000000000000000000000000",
38
+ amount: 1_000_000_000_000_000_000n,
39
+ signer: wallet,
40
+ verifyingContract: "0xPayWithPayID...",
41
+ ruleAuthority: "0xRuleAuthority...",
42
+ chainId: 42161,
43
+ blockTimestamp: Math.floor(Date.now() / 1000),
44
+ });
45
+
46
+ if (result.result.decision === "ALLOW") {
47
+ // submit result.proof to PayWithPayID.payNative() on-chain
48
+ }
49
+ ```
50
+
51
+ ---
52
+
53
+ ## Rule Language
54
+
55
+ Rules define **what conditions must be true** for a payment to be allowed. They are evaluated off-chain, and the outcome is signed into a Decision Proof that the smart contract verifies.
56
+
57
+ ### Rule Config
58
+
59
+ Every rule set starts with a `RuleConfig`:
60
+
61
+ ```typescript
62
+ interface RuleConfig {
63
+ logic: "AND" | "OR"; // how top-level rules are combined
64
+ rules: AnyRule[]; // list of rules
65
+ requires?: string[]; // required context namespaces (e.g. ["state", "risk"])
66
+ message?: string; // custom message on root rejection
67
+ }
68
+ ```
69
+
70
+ - **`AND`** — all rules must pass
71
+ - **`OR`** — at least one rule must pass
72
+ - Max nesting depth: **10 levels**
73
+
74
+ ---
75
+
76
+ ### Rule Formats
77
+
78
+ There are three rule formats that can be mixed freely.
79
+
80
+ #### Format A — Simple Rule
81
+
82
+ One condition, one rule.
83
+
84
+ ```typescript
85
+ {
86
+ id: "min-amount",
87
+ if: { field: "tx.amount", op: ">=", value: "100000000000000000" },
88
+ message: "Minimum transfer is 0.1 ETH"
89
+ }
90
+ ```
91
+
92
+ #### Format B — Multi-Condition Rule
93
+
94
+ Multiple conditions combined with `AND` / `OR`.
95
+
96
+ ```typescript
97
+ {
98
+ id: "business-hours",
99
+ logic: "AND",
100
+ conditions: [
101
+ { field: "env.timestamp|hour", op: ">=", value: "9" },
102
+ { field: "env.timestamp|hour", op: "<", value: "17" },
103
+ ],
104
+ message: "Transfers only allowed during business hours (9–17 UTC)"
105
+ }
106
+ ```
107
+
108
+ #### Format C — Nested Rule
109
+
110
+ Groups of rules combined with `AND` / `OR`. Can be nested up to 10 levels deep.
111
+
112
+ ```typescript
113
+ {
114
+ id: "whitelist-or-small",
115
+ logic: "OR",
116
+ rules: [
117
+ { id: "in-whitelist", if: { field: "tx.sender", op: "in", value: ["0xAlice", "0xBob"] } },
118
+ { id: "small-amount", if: { field: "tx.amount", op: "<=", value: "50000000000000000" } }
119
+ ]
120
+ }
121
+ ```
122
+
123
+ ---
124
+
125
+ ### Conditions
126
+
127
+ ```typescript
128
+ interface RuleCondition {
129
+ field: string; // dot-notation path into the context
130
+ op: string; // operator
131
+ value: any; // literal value or "$field.reference"
132
+ }
133
+ ```
134
+
135
+ ---
136
+
137
+ ### Fields (Context Paths)
138
+
139
+ Fields use dot-notation to navigate the payment context.
140
+
141
+ #### Core Context (`tx`)
142
+
143
+ | Field | Type | Description |
144
+ |---|---|---|
145
+ | `tx.amount` | `string` (wei) | Transfer amount in smallest unit |
146
+ | `tx.amountUsd` | `string` | USD equivalent (if oracle provided) |
147
+ | `tx.asset` | `string` | Token contract address; `0x000...000` for native |
148
+ | `tx.sender` | `string` | Sender address |
149
+ | `tx.receiver` | `string` | Receiver address |
150
+ | `tx.chainId` | `number` | EVM chain ID |
151
+
152
+ #### Pay.ID Context (`payId`)
153
+
154
+ | Field | Type | Description |
155
+ |---|---|---|
156
+ | `payId.id` | `string` | The Pay.ID handle (e.g. `user@pay.id`) |
157
+ | `payId.owner` | `string` | Address that owns this Pay.ID |
158
+
159
+ #### Intent Context (`intent`)
160
+
161
+ | Field | Type | Description |
162
+ |---|---|---|
163
+ | `intent.type` | `"QR" \| "DIRECT" \| "API"` | How payment was initiated |
164
+ | `intent.expiresAt` | `number` | Unix timestamp when intent expires |
165
+ | `intent.issuer` | `string` | Who issued the intent |
166
+
167
+ #### Environment Context (`env`) — V2
168
+
169
+ | Field | Type | Description |
170
+ |---|---|---|
171
+ | `env.timestamp` | `number` | Current block timestamp (Unix seconds) |
172
+
173
+ #### State Context (`state`) — V2
174
+
175
+ | Field | Type | Description |
176
+ |---|---|---|
177
+ | `state.spentToday` | `string` | Total amount spent today (wei) |
178
+ | `state.period` | `string` | Current period identifier |
179
+
180
+ #### Oracle Context (`oracle`) — V2
181
+
182
+ Custom key-value data from an off-chain oracle. Example: `oracle.ethPrice`, `oracle.gasPrice`.
183
+
184
+ #### Risk Context (`risk`) — V2
185
+
186
+ | Field | Type | Description |
187
+ |---|---|---|
188
+ | `risk.score` | `number` | Risk score 0–1000 |
189
+ | `risk.category` | `string` | Risk category label |
190
+
191
+ ---
192
+
193
+ ### Field Transforms
194
+
195
+ Append `|transform` to a field to transform its value before comparison.
196
+
197
+ ```
198
+ "env.timestamp|hour" → hour of day (0–23)
199
+ "tx.amount|div:1e18" → amount divided by 1e18
200
+ ```
201
+
202
+ | Transform | Syntax | Description |
203
+ |---|---|---|
204
+ | `div` | `field\|div:N` | Divide by N (integer) |
205
+ | `mod` | `field\|mod:N` | Modulo N |
206
+ | `abs` | `field\|abs` | Absolute value |
207
+ | `hour` | `field\|hour` | Hour of day from Unix timestamp (0–23) |
208
+ | `day` | `field\|day` | Day of week from Unix timestamp (0=Mon, 6=Sun) |
209
+ | `date` | `field\|date` | Day of month (1–31) |
210
+ | `month` | `field\|month` | Month of year (1–12) |
211
+ | `len` | `field\|len` | String length |
212
+ | `lower` | `field\|lower` | Lowercase string |
213
+ | `upper` | `field\|upper` | Uppercase string |
214
+
215
+ ---
216
+
217
+ ### Operators
218
+
219
+ #### Numeric
220
+
221
+ | Operator | Description | Example |
222
+ |---|---|---|
223
+ | `>=` | Greater than or equal | `amount >= 1000` |
224
+ | `<=` | Less than or equal | `amount <= 50000` |
225
+ | `>` | Greater than | `amount > 0` |
226
+ | `<` | Less than | `risk.score < 700` |
227
+ | `between` | Inclusive range `[min, max]` | `value: ["100", "5000"]` |
228
+ | `not_between` | Outside range | `value: ["100", "5000"]` |
229
+ | `mod_eq` | `field % divisor == remainder` | `value: ["7", "0"]` (divisible by 7) |
230
+ | `mod_ne` | `field % divisor != remainder` | |
231
+
232
+ #### Equality
233
+
234
+ | Operator | Description |
235
+ |---|---|
236
+ | `==` | Loose equality (string + numeric coercion) |
237
+ | `!=` | Not equal |
238
+ | `in` | Value is in array |
239
+ | `not_in` | Value is not in array |
240
+
241
+ #### String
242
+
243
+ | Operator | Description |
244
+ |---|---|
245
+ | `contains` | String contains substring |
246
+ | `not_contains` | String does not contain substring |
247
+ | `starts_with` | String starts with prefix |
248
+ | `ends_with` | String ends with suffix |
249
+ | `regex` | Matches regex pattern (ReDoS-safe, max 200 chars) |
250
+ | `not_regex` | Does not match regex |
251
+
252
+ #### Existence
253
+
254
+ | Operator | Description |
255
+ |---|---|
256
+ | `exists` | Field is present and not null |
257
+ | `not_exists` | Field is absent or null |
258
+
259
+ ---
260
+
261
+ ### Cross-Field References
262
+
263
+ Prefix a value with `$` to compare against another field in the context.
264
+
265
+ ```typescript
266
+ // Reject if spending more than today's limit
267
+ { field: "tx.amount", op: "<=", value: "$state.dailyLimit" }
268
+
269
+ // Only allow if sender equals the Pay.ID owner
270
+ { field: "tx.sender", op: "==", value: "$payId.owner" }
271
+ ```
272
+
273
+ ---
274
+
275
+ ### Message Interpolation
276
+
277
+ Rule messages support `{field}` interpolation using the same dot-notation and transforms.
278
+
279
+ ```typescript
280
+ {
281
+ id: "amount-cap",
282
+ if: { field: "tx.amount", op: "<=", value: "1000000000000000000" },
283
+ message: "Rejected: amount {tx.amount} exceeds 1 ETH cap"
284
+ }
285
+ ```
286
+
287
+ ---
288
+
289
+ ## Complete Examples
290
+
291
+ ### 1. Simple Amount Cap
292
+
293
+ ```typescript
294
+ const rule: RuleConfig = {
295
+ logic: "AND",
296
+ rules: [
297
+ {
298
+ id: "max-per-tx",
299
+ if: { field: "tx.amount", op: "<=", value: "5000000000000000000" },
300
+ message: "Max 5 ETH per transaction"
301
+ }
302
+ ]
303
+ };
304
+ ```
305
+
306
+ ### 2. Daily Spending Limit
307
+
308
+ ```typescript
309
+ const rule: RuleConfig = {
310
+ logic: "AND",
311
+ requires: ["state"],
312
+ rules: [
313
+ {
314
+ id: "daily-limit",
315
+ if: { field: "state.spentToday", op: "<=", value: "10000000000000000000" },
316
+ message: "Daily limit of 10 ETH exceeded"
317
+ }
318
+ ]
319
+ };
320
+ ```
321
+
322
+ ### 3. Business Hours Only
323
+
324
+ ```typescript
325
+ const rule: RuleConfig = {
326
+ logic: "AND",
327
+ requires: ["env"],
328
+ rules: [
329
+ {
330
+ id: "business-hours",
331
+ logic: "AND",
332
+ conditions: [
333
+ { field: "env.timestamp|hour", op: ">=", value: "9" },
334
+ { field: "env.timestamp|hour", op: "<", value: "17" }
335
+ ],
336
+ message: "Transfers only allowed 09:00–17:00 UTC"
337
+ }
338
+ ]
339
+ };
340
+ ```
341
+
342
+ ### 4. Whitelist OR Small Amount
343
+
344
+ ```typescript
345
+ const rule: RuleConfig = {
346
+ logic: "AND",
347
+ rules: [
348
+ {
349
+ id: "whitelist-or-small",
350
+ logic: "OR",
351
+ rules: [
352
+ {
353
+ id: "is-whitelisted",
354
+ if: {
355
+ field: "tx.sender",
356
+ op: "in",
357
+ value: ["0xAlice...", "0xBob...", "0xCharlie..."]
358
+ }
359
+ },
360
+ {
361
+ id: "small-amount",
362
+ if: { field: "tx.amount", op: "<=", value: "100000000000000000" }
363
+ }
364
+ ],
365
+ message: "Sender not whitelisted and amount exceeds 0.1 ETH"
366
+ }
367
+ ]
368
+ };
369
+ ```
370
+
371
+ ### 5. Multi-Condition Risk Gate
372
+
373
+ ```typescript
374
+ const rule: RuleConfig = {
375
+ logic: "AND",
376
+ requires: ["risk"],
377
+ rules: [
378
+ {
379
+ id: "risk-gate",
380
+ logic: "AND",
381
+ conditions: [
382
+ { field: "risk.score", op: "<", value: "700" },
383
+ { field: "risk.category", op: "!=", value: "SANCTIONED" }
384
+ ],
385
+ message: "Payment blocked: risk score too high or sender sanctioned"
386
+ }
387
+ ]
388
+ };
389
+ ```
390
+
391
+ ### 6. Recurring Payment (Weekdays Only)
392
+
393
+ ```typescript
394
+ const rule: RuleConfig = {
395
+ logic: "AND",
396
+ requires: ["env"],
397
+ rules: [
398
+ {
399
+ id: "weekday-only",
400
+ // day transform: 0=Mon, 4=Fri, 5=Sat, 6=Sun
401
+ logic: "AND",
402
+ conditions: [
403
+ { field: "env.timestamp|day", op: ">=", value: "0" },
404
+ { field: "env.timestamp|day", op: "<=", value: "4" }
405
+ ],
406
+ message: "Only weekday payments allowed"
407
+ }
408
+ ]
409
+ };
410
+ ```
411
+
412
+ ---
413
+
414
+ ## Evaluate Only (No Proof)
415
+
416
+ ```typescript
417
+ import { evaluate } from "payid";
418
+
419
+ const result = await evaluate(context, ruleConfig);
420
+ // result.decision === "ALLOW" | "REJECT"
421
+ // result.code — rule ID that triggered the decision
422
+ // result.reason — human-readable message
423
+ ```
424
+
425
+ ---
426
+
427
+ ## Server-Side Usage
428
+
429
+ ```typescript
430
+ import { PayIDServer } from "payid/server";
431
+ import { ethers } from "ethers";
432
+
433
+ const server = new PayIDServer(
434
+ new ethers.Wallet(process.env.SIGNER_KEY!),
435
+ new Set(["0xTrustedIssuer..."]) // trusted attestation issuers
436
+ );
437
+
438
+ const { result, proof } = await server.evaluateAndProve({
439
+ context,
440
+ authorityRule: ruleConfig,
441
+ payId: "merchant@pay.id",
442
+ payer: "0xPayer...",
443
+ receiver: "0xMerchant...",
444
+ asset: "0x0000000000000000000000000000000000000000",
445
+ amount: 1_000_000_000_000_000_000n,
446
+ verifyingContract: "0xPayWithPayID...",
447
+ ruleAuthority: "0xRuleAuthority...",
448
+ chainId: 42161,
449
+ blockTimestamp: Math.floor(Date.now() / 1000),
450
+ });
451
+ ```
452
+
453
+ ---
454
+
455
+ ## Exports
456
+
457
+ | Import path | Contents |
458
+ |---|---|
459
+ | `payid` | `evaluate()` |
460
+ | `payid/client` | `PayIDClient` |
461
+ | `payid/server` | `PayIDServer` |
462
+ | `payid/decision-proof` | `generateDecisionProof()` |
463
+ | `payid/rule` | `combineRules()`, `canonicalizeRuleSet()` |
464
+ | `payid/issuer` | `signAttestation()` |
465
+ | `payid/context` | Context types |
466
+ | `payid/sessionPolicy` | `decodeSessionPolicy()`, `decodeSessionPolicyV2()` |
467
+
468
+ ---
469
+
470
+ ## Limits
471
+
472
+ | Constraint | Value |
473
+ |---|---|
474
+ | Max rule nesting depth | 10 levels |
475
+ | Max regex pattern length | 200 chars |
476
+ | Nested quantifiers in regex | Rejected (ReDoS protection) |
477
+ | Decision Proof TTL (default) | 300 seconds |
478
+ | Decision Proof TTL (max recommended) | 3600 seconds |
@@ -0,0 +1,104 @@
1
+ import {
2
+ hashContext,
3
+ hashRuleSet
4
+ } from "./chunk-X7NYQ47Y.js";
5
+ import {
6
+ randomHex
7
+ } from "./chunk-KDC67LIN.js";
8
+
9
+ // src/decision-proof/generate.ts
10
+ import { ethers, ZeroAddress } from "ethers";
11
+ var hash = (v) => ethers.keccak256(ethers.toUtf8Bytes(v));
12
+ async function generateDecisionProof(params) {
13
+ if (!params.payId || typeof params.payId !== "string" || params.payId.trim() === "") {
14
+ throw new Error("payId must not be empty");
15
+ }
16
+ if (!ethers.isAddress(params.payer) || params.payer === ethers.ZeroAddress) {
17
+ throw new Error("GENERATE_PROOF: payer address is invalid or zero");
18
+ }
19
+ if (!ethers.isAddress(params.receiver) || params.receiver === ethers.ZeroAddress) {
20
+ throw new Error("GENERATE_PROOF: receiver address is invalid or zero");
21
+ }
22
+ if (!ethers.isAddress(params.verifyingContract) || params.verifyingContract === ethers.ZeroAddress) {
23
+ throw new Error("GENERATE_PROOF: verifyingContract is invalid or zero");
24
+ }
25
+ if (!ethers.isAddress(params.ruleAuthority)) {
26
+ throw new Error("GENERATE_PROOF: ruleAuthority is not a valid address");
27
+ }
28
+ if (params.asset !== void 0 && !ethers.isAddress(params.asset)) {
29
+ throw new Error("GENERATE_PROOF: asset is not a valid address");
30
+ }
31
+ if (params.amount <= 0n) {
32
+ throw new Error("amount must be > 0");
33
+ }
34
+ if (params.ttlSeconds !== void 0 && (params.ttlSeconds <= 0 || !Number.isInteger(params.ttlSeconds))) {
35
+ throw new Error("GENERATE_PROOF: ttlSeconds must be a positive integer");
36
+ }
37
+ if (params.blockTimestamp !== void 0 && params.blockTimestamp <= 0) {
38
+ throw new Error("GENERATE_PROOF: blockTimestamp is invalid");
39
+ }
40
+ const now = params.blockTimestamp ?? Math.floor(Date.now() / 1e3);
41
+ const issuedAt = now - 30;
42
+ const expiresAt = now + (params.ttlSeconds ?? 300);
43
+ let chainId = params.chainId;
44
+ if (!chainId && params.signer.provider) {
45
+ const network = await params.signer.provider.getNetwork();
46
+ chainId = Number(network.chainId);
47
+ }
48
+ if (!chainId || chainId <= 0 || !Number.isInteger(chainId)) {
49
+ throw new Error(`GENERATE_PROOF: chainId is invalid: ${chainId}`);
50
+ }
51
+ const requiresAttestation = Array.isArray(params.ruleConfig?.requires) && params.ruleConfig.requires.length > 0;
52
+ const attestationUIDsHash = params.attestationUIDs ? ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(["bytes32[]"], [params.attestationUIDs])) : ethers.ZeroHash;
53
+ const payload = {
54
+ version: hash("2"),
55
+ payId: hash(params.payId),
56
+ payer: params.payer,
57
+ receiver: params.receiver,
58
+ asset: params.asset,
59
+ amount: params.amount,
60
+ contextHash: hashContext(params.context),
61
+ ruleSetHash: params.ruleSetHashOverride ?? hashRuleSet(params.ruleConfig),
62
+ ruleAuthority: params.ruleAuthority ?? ZeroAddress,
63
+ issuedAt: BigInt(issuedAt),
64
+ expiresAt: BigInt(expiresAt),
65
+ nonce: randomHex(32),
66
+ requiresAttestation,
67
+ attestationUIDsHash
68
+ };
69
+ const domain = {
70
+ name: "PAY.ID Decision",
71
+ version: "2",
72
+ chainId,
73
+ verifyingContract: params.verifyingContract
74
+ };
75
+ const types = {
76
+ Decision: [
77
+ { name: "version", type: "bytes32" },
78
+ { name: "payId", type: "bytes32" },
79
+ { name: "payer", type: "address" },
80
+ { name: "receiver", type: "address" },
81
+ { name: "asset", type: "address" },
82
+ { name: "amount", type: "uint256" },
83
+ { name: "contextHash", type: "bytes32" },
84
+ { name: "ruleSetHash", type: "bytes32" },
85
+ { name: "ruleAuthority", type: "address" },
86
+ { name: "issuedAt", type: "uint64" },
87
+ { name: "expiresAt", type: "uint64" },
88
+ { name: "nonce", type: "bytes32" },
89
+ { name: "requiresAttestation", type: "bool" },
90
+ { name: "attestationUIDsHash", type: "bytes32" }
91
+ ]
92
+ };
93
+ const signature = await params.signer.signTypedData(domain, types, payload);
94
+ const recovered = ethers.verifyTypedData(domain, types, payload, signature);
95
+ const signerAddress = await params.signer.getAddress();
96
+ if (recovered.toLowerCase() !== signerAddress.toLowerCase()) {
97
+ throw new Error("SIGNATURE_MISMATCH");
98
+ }
99
+ return { payload, signature };
100
+ }
101
+
102
+ export {
103
+ generateDecisionProof
104
+ };
@@ -1,11 +1,3 @@
1
- import {
2
- hashContext,
3
- hashRuleSet
4
- } from "./chunk-X7NYQ47Y.js";
5
- import {
6
- randomHex
7
- } from "./chunk-KDC67LIN.js";
8
-
9
1
  // src/attestation/verify.ts
10
2
  import { ethers, keccak256, toUtf8Bytes, toBeArray } from "ethers";
11
3
  function verifyAttestation(payload, proof, trustedIssuers) {
@@ -137,29 +129,33 @@ async function runWasmRule(context, config, wasmBinary) {
137
129
  async function runWasmRule2(context, config, _wasmBinary) {
138
130
  return evaluateRule(context, config);
139
131
  }
132
+ var MAX_RULE_DEPTH = 10;
140
133
  function evaluateRule(context, config) {
141
134
  const rules = config?.rules;
142
135
  if (!Array.isArray(rules) || rules.length === 0) {
143
136
  return { decision: "ALLOW", code: "NO_RULES", reason: "no rules defined" };
144
137
  }
145
138
  const logic = config?.logic ?? "AND";
146
- return evalRules(context, rules, logic);
139
+ return evalRules(context, rules, logic, 0);
147
140
  }
148
- function evalRules(context, rules, logic) {
141
+ function evalRules(context, rules, logic, depth) {
142
+ if (depth > MAX_RULE_DEPTH) {
143
+ return { decision: "REJECT", code: "MAX_DEPTH_EXCEEDED", reason: `rule nesting exceeds the limit of ${MAX_RULE_DEPTH}` };
144
+ }
149
145
  for (const rule of rules) {
150
- const res = evalOneRule(context, rule);
146
+ const res = evalOneRule(context, rule, depth);
151
147
  if (res.decision === "REJECT" && logic === "AND") return res;
152
148
  if (res.decision === "ALLOW" && logic === "OR") return res;
153
149
  }
154
150
  if (logic === "AND") return { decision: "ALLOW", code: "OK", reason: "all rules passed" };
155
151
  return { decision: "REJECT", code: "NO_RULE_MATCH", reason: "no rule matched in OR group" };
156
152
  }
157
- function evalOneRule(context, rule) {
153
+ function evalOneRule(context, rule, depth) {
158
154
  const ruleId = rule?.id ?? "UNKNOWN_RULE";
159
155
  const message = rule?.message ?? "";
160
156
  if (Array.isArray(rule?.rules)) {
161
157
  const subLogic = rule?.logic ?? "AND";
162
- const res = evalRules(context, rule.rules, subLogic);
158
+ const res = evalRules(context, rule.rules, subLogic, depth + 1);
163
159
  if (res.decision === "REJECT" && message) {
164
160
  return { decision: "REJECT", code: ruleId, reason: message };
165
161
  }
@@ -572,9 +568,22 @@ async function evaluate(context, ruleConfig, options, wasmBinary) {
572
568
  if (!context.tx) {
573
569
  throw new Error("evaluate(): context.tx is required");
574
570
  }
571
+ if (context.tx.chainId !== void 0 && (!Number.isInteger(context.tx.chainId) || context.tx.chainId <= 0)) {
572
+ throw new Error(`chainId is invalid: ${context.tx.chainId}`);
573
+ }
574
+ if (context.tx.amount !== void 0) {
575
+ const amt = BigInt(context.tx.amount);
576
+ if (amt <= 0n) throw new Error("amount must be > 0");
577
+ }
575
578
  if (!ruleConfig || typeof ruleConfig !== "object") {
576
579
  throw new Error("evaluate(): ruleConfig is required");
577
580
  }
581
+ if (ruleConfig.logic !== "AND" && ruleConfig.logic !== "OR") {
582
+ throw new Error(`ruleConfig.logic must be "AND" or "OR", got: ${ruleConfig.logic}`);
583
+ }
584
+ if (!Array.isArray(ruleConfig.rules) || ruleConfig.rules.length === 0) {
585
+ throw new Error("ruleConfig.rules must be a non-empty array");
586
+ }
578
587
  let result;
579
588
  try {
580
589
  const preparedContext = options?.trustedIssuers ? preprocessContextV2(context, ruleConfig, options.trustedIssuers) : context;
@@ -610,84 +619,6 @@ async function evaluate(context, ruleConfig, options, wasmBinary) {
610
619
  return baseResult;
611
620
  }
612
621
 
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
622
  // src/utils/subtle.ts
692
623
  var subtleCrypto = globalThis.crypto.subtle;
693
624
 
@@ -717,7 +648,7 @@ function bufferToHex(buffer) {
717
648
  // src/resolver/resolver.ts
718
649
  var DEFAULT_ZG_INDEXER = "https://indexer-testnet.0g.ai";
719
650
  async function resolveRule(source, options) {
720
- const { uri, hash: hash2 } = source;
651
+ const { uri, hash } = source;
721
652
  if (uri.startsWith("inline://")) {
722
653
  const encoded = uri.replace("inline://", "");
723
654
  const json = JSON.parse(atob(encoded));
@@ -726,18 +657,18 @@ async function resolveRule(source, options) {
726
657
  if (uri.startsWith("ipfs://")) {
727
658
  const cid = uri.replace("ipfs://", "");
728
659
  const url = `https://ipfs.io/ipfs/${cid}`;
729
- const config = await fetchJsonWithHashCheck(url, hash2);
660
+ const config = await fetchJsonWithHashCheck(url, hash);
730
661
  return { config, source };
731
662
  }
732
663
  if (uri.startsWith("http://") || uri.startsWith("https://")) {
733
- const config = await fetchJsonWithHashCheck(uri, hash2);
664
+ const config = await fetchJsonWithHashCheck(uri, hash);
734
665
  return { config, source };
735
666
  }
736
667
  if (uri.startsWith("0g://")) {
737
668
  const rootHash = uri.replace("0g://", "");
738
669
  const indexerUrl = options?.zgIndexerUrl ?? globalThis.PAYID_ZGS_INDEXER_URL ?? DEFAULT_ZG_INDEXER;
739
670
  const url = `${indexerUrl}/blob/${rootHash}`;
740
- const config = await fetchJsonWithHashCheck(url, hash2);
671
+ const config = await fetchJsonWithHashCheck(url, hash);
741
672
  return { config, source };
742
673
  }
743
674
  throw new Error("UNSUPPORTED_RULE_URI");
@@ -747,6 +678,5 @@ export {
747
678
  loadWasm,
748
679
  verifyAttestation,
749
680
  evaluate,
750
- generateDecisionProof,
751
681
  resolveRule
752
682
  };
@@ -7,10 +7,12 @@ import {
7
7
  } from "./chunk-BC77BLFK.js";
8
8
  import {
9
9
  evaluate,
10
- generateDecisionProof,
11
10
  loadWasm,
12
11
  resolveRule
13
- } from "./chunk-HKHRYRD6.js";
12
+ } from "./chunk-F2KQGW76.js";
13
+ import {
14
+ generateDecisionProof
15
+ } from "./chunk-AXLZUAYL.js";
14
16
  import {
15
17
  __export
16
18
  } from "./chunk-MLKGABMK.js";
@@ -1,8 +1,10 @@
1
1
  import {
2
2
  evaluate,
3
- generateDecisionProof,
4
3
  resolveRule
5
- } from "./chunk-HKHRYRD6.js";
4
+ } from "./chunk-F2KQGW76.js";
5
+ import {
6
+ generateDecisionProof
7
+ } from "./chunk-AXLZUAYL.js";
6
8
  import {
7
9
  __export
8
10
  } from "./chunk-MLKGABMK.js";
@@ -1,6 +1,7 @@
1
- export { P as PayIDClient, c as createPayID, c as createPayIDClient } from '../../index-CsynGAGv.js';
1
+ export { P as PayIDClient, c as createPayID, c as createPayIDClient } from '../../index-IjgtzYO2.js';
2
2
  import '../../rule-a_5ed-93.js';
3
3
  import '../../context.v1-C1m-tz0o.js';
4
- import '../../types-i4eTkhWa.js';
4
+ import '../../types-cnCPtnaV.js';
5
5
  import 'ethers';
6
+ import '../../types-CSZ5F9J7.js';
6
7
  import '../../types-D2o6XS7a.js';
@@ -1,11 +1,12 @@
1
1
  import {
2
2
  PayIDClient,
3
3
  createPayIDClient
4
- } from "../../chunk-4UQKUVO5.js";
4
+ } from "../../chunk-K2B5UHYA.js";
5
5
  import "../../chunk-GG34PNTF.js";
6
6
  import "../../chunk-BC77BLFK.js";
7
7
  import "../../chunk-6VPSJFO4.js";
8
- import "../../chunk-HKHRYRD6.js";
8
+ import "../../chunk-F2KQGW76.js";
9
+ import "../../chunk-AXLZUAYL.js";
9
10
  import "../../chunk-X7NYQ47Y.js";
10
11
  import "../../chunk-KDC67LIN.js";
11
12
  import "../../chunk-MLKGABMK.js";
@@ -1,5 +1,6 @@
1
- export { P as PayIDServer, c as createPayIDServer } from '../../index-DSxDlF9J.js';
1
+ export { P as PayIDServer, c as createPayIDServer } from '../../index-DpGIg0Hu.js';
2
2
  import 'ethers';
3
3
  import '../../rule-a_5ed-93.js';
4
4
  import '../../context.v1-C1m-tz0o.js';
5
- import '../../types-i4eTkhWa.js';
5
+ import '../../types-cnCPtnaV.js';
6
+ import '../../types-CSZ5F9J7.js';
@@ -1,8 +1,9 @@
1
1
  import {
2
2
  PayIDServer,
3
3
  createPayIDServer
4
- } from "../../chunk-ESTGPUEQ.js";
5
- import "../../chunk-HKHRYRD6.js";
4
+ } from "../../chunk-WQEZYX4X.js";
5
+ import "../../chunk-F2KQGW76.js";
6
+ import "../../chunk-AXLZUAYL.js";
6
7
  import "../../chunk-X7NYQ47Y.js";
7
8
  import "../../chunk-KDC67LIN.js";
8
9
  import "../../chunk-MLKGABMK.js";
@@ -0,0 +1,23 @@
1
+ import { a as DecisionProof } from '../types-CSZ5F9J7.js';
2
+ export { D as DecisionPayload, b as DecisionValue } from '../types-CSZ5F9J7.js';
3
+ import { ethers } from 'ethers';
4
+
5
+ declare function generateDecisionProof(params: {
6
+ payId: string;
7
+ payer: string;
8
+ receiver: string;
9
+ asset: string;
10
+ amount: bigint;
11
+ context: any;
12
+ ruleConfig: any;
13
+ ruleSetHashOverride?: string;
14
+ signer: ethers.Signer;
15
+ ruleAuthority: string;
16
+ verifyingContract: string;
17
+ ttlSeconds?: number;
18
+ chainId?: number;
19
+ blockTimestamp?: number;
20
+ attestationUIDs?: string[];
21
+ }): Promise<DecisionProof>;
22
+
23
+ export { DecisionProof, generateDecisionProof };
@@ -0,0 +1,9 @@
1
+ import {
2
+ generateDecisionProof
3
+ } from "../chunk-AXLZUAYL.js";
4
+ import "../chunk-X7NYQ47Y.js";
5
+ import "../chunk-KDC67LIN.js";
6
+ import "../chunk-MLKGABMK.js";
7
+ export {
8
+ generateDecisionProof
9
+ };
@@ -1,7 +1,8 @@
1
1
  import { ethers } from 'ethers';
2
2
  import { b as RuleConfig } from './rule-a_5ed-93.js';
3
3
  import { R as RuleContext } from './context.v1-C1m-tz0o.js';
4
- import { R as ResolverOptions, e as RuleSource, c as RuleResult, a as DecisionProof } from './types-i4eTkhWa.js';
4
+ import { R as ResolverOptions, d as RuleSource, b as RuleResult } from './types-cnCPtnaV.js';
5
+ import { a as DecisionProof } from './types-CSZ5F9J7.js';
5
6
 
6
7
  interface UserOperation {
7
8
  sender: string;
@@ -1,7 +1,8 @@
1
1
  import { b as RuleConfig } from './rule-a_5ed-93.js';
2
2
  import { R as RuleContext } from './context.v1-C1m-tz0o.js';
3
- import { R as ResolverOptions, e as RuleSource, c as RuleResult, a as DecisionProof } from './types-i4eTkhWa.js';
3
+ import { R as ResolverOptions, d as RuleSource, b as RuleResult } from './types-cnCPtnaV.js';
4
4
  import { ethers } from 'ethers';
5
+ import { a as DecisionProof } from './types-CSZ5F9J7.js';
5
6
  import { P as PayIDSessionPolicyPayloadV1, S as SessionPolicyV2 } from './types-D2o6XS7a.js';
6
7
 
7
8
  declare class PayIDClient {
package/dist/index.d.ts CHANGED
@@ -1,15 +1,18 @@
1
- import { P as PayIDClient$1 } from './index-CsynGAGv.js';
2
- export { i as client } from './index-CsynGAGv.js';
3
- import { Z as ZGStorage, P as PayIDServer$1, U as UserOperation } from './index-DSxDlF9J.js';
4
- export { i as server, z as storage } from './index-DSxDlF9J.js';
1
+ import { P as PayIDClient$1 } from './index-IjgtzYO2.js';
2
+ export { i as client } from './index-IjgtzYO2.js';
3
+ import { Z as ZGStorage, P as PayIDServer$1, U as UserOperation } from './index-DpGIg0Hu.js';
4
+ export { i as server, z as storage } from './index-DpGIg0Hu.js';
5
5
  import { ethers } from 'ethers';
6
- import { R as ResolverOptions, e as RuleSource, c as RuleResult, a as DecisionProof } from './types-i4eTkhWa.js';
7
- export { D as DecisionPayload, b as RuleDecisionDebug, d as RuleResultDebug, f as RuleTraceEntry } from './types-i4eTkhWa.js';
6
+ import { R as ResolverOptions, d as RuleSource, b as RuleResult } from './types-cnCPtnaV.js';
7
+ export { a as RuleDecisionDebug, c as RuleResultDebug, e as RuleTraceEntry } from './types-cnCPtnaV.js';
8
8
  import { b as RuleConfig } from './rule-a_5ed-93.js';
9
9
  export { A as AnyRule, M as MultiConditionRule, N as NestedRule, R as Rule, a as RuleCondition, S as SimpleRule, i as isMultiConditionRule, c as isNestedRule, d as isSimpleRule } from './rule-a_5ed-93.js';
10
10
  import { R as RuleContext } from './context.v1-C1m-tz0o.js';
11
11
  export { C as ContextV1, P as PayIdContext, T as TxContext } from './context.v1-C1m-tz0o.js';
12
+ import { a as DecisionProof } from './types-CSZ5F9J7.js';
13
+ export { D as DecisionPayload } from './types-CSZ5F9J7.js';
12
14
  import { S as SessionPolicyV2 } from './types-D2o6XS7a.js';
15
+ export { generateDecisionProof } from './decision-proof/index.js';
13
16
  import { A as Attestation } from './context.v2-DIzPotmW.js';
14
17
  export { C as ContextV2, E as EnvContext, O as OracleContext, R as RiskContext, S as StateContext } from './context.v2-DIzPotmW.js';
15
18
  export { i as sessionPolicy } from './index-G_1SiZJo.js';
package/dist/index.js CHANGED
@@ -1,30 +1,33 @@
1
1
  import {
2
2
  context_exports
3
3
  } from "./chunk-BFCPKJ46.js";
4
- import {
5
- issuer_exports
6
- } from "./chunk-E6VQETBC.js";
7
- import "./chunk-YKCMGGYB.js";
8
4
  import {
9
5
  rule_exports
10
6
  } from "./chunk-FZNMDGVK.js";
11
- import {
12
- sessionPolicy_exports
13
- } from "./chunk-R674DZJS.js";
14
7
  import {
15
8
  PayIDClient,
16
9
  client_exports
17
- } from "./chunk-4UQKUVO5.js";
10
+ } from "./chunk-K2B5UHYA.js";
18
11
  import "./chunk-GG34PNTF.js";
12
+ import {
13
+ sessionPolicy_exports
14
+ } from "./chunk-R674DZJS.js";
19
15
  import "./chunk-BC77BLFK.js";
20
16
  import "./chunk-6VPSJFO4.js";
17
+ import {
18
+ issuer_exports
19
+ } from "./chunk-E6VQETBC.js";
20
+ import "./chunk-YKCMGGYB.js";
21
21
  import {
22
22
  PayIDServer,
23
23
  server_exports
24
- } from "./chunk-ESTGPUEQ.js";
24
+ } from "./chunk-WQEZYX4X.js";
25
25
  import {
26
26
  verifyAttestation
27
- } from "./chunk-HKHRYRD6.js";
27
+ } from "./chunk-F2KQGW76.js";
28
+ import {
29
+ generateDecisionProof
30
+ } from "./chunk-AXLZUAYL.js";
28
31
  import "./chunk-X7NYQ47Y.js";
29
32
  import "./chunk-KDC67LIN.js";
30
33
  import {
@@ -696,6 +699,7 @@ export {
696
699
  draftCache,
697
700
  eas_exports as eas,
698
701
  formatUsdValue,
702
+ generateDecisionProof,
699
703
  getCacheStats,
700
704
  historyCache,
701
705
  isMultiConditionRule,
@@ -0,0 +1,23 @@
1
+ type DecisionValue = 0 | 1;
2
+ interface DecisionPayload {
3
+ version: string;
4
+ payId: string;
5
+ payer: string;
6
+ receiver: string;
7
+ asset: string;
8
+ amount: bigint;
9
+ contextHash: string;
10
+ ruleSetHash: string;
11
+ ruleAuthority: string;
12
+ issuedAt: bigint;
13
+ expiresAt: bigint;
14
+ nonce: string;
15
+ requiresAttestation: boolean;
16
+ attestationUIDsHash: string;
17
+ }
18
+ interface DecisionProof {
19
+ payload: DecisionPayload;
20
+ signature: string;
21
+ }
22
+
23
+ export type { DecisionPayload as D, DecisionProof as a, DecisionValue as b };
@@ -0,0 +1,29 @@
1
+ interface RuleResult {
2
+ decision: "ALLOW" | "REJECT";
3
+ code: string;
4
+ reason?: string;
5
+ }
6
+ interface RuleTraceEntry {
7
+ ruleId: string;
8
+ field: string;
9
+ op: string;
10
+ expected: any;
11
+ actual: any;
12
+ result: "PASS" | "FAIL";
13
+ }
14
+ interface RuleDecisionDebug {
15
+ trace: RuleTraceEntry[];
16
+ }
17
+ interface RuleResultDebug extends RuleResult {
18
+ debug?: RuleDecisionDebug;
19
+ }
20
+
21
+ interface RuleSource {
22
+ uri: string;
23
+ hash?: string;
24
+ }
25
+ interface ResolverOptions {
26
+ zgIndexerUrl?: string;
27
+ }
28
+
29
+ export type { ResolverOptions as R, RuleDecisionDebug as a, RuleResult as b, RuleResultDebug as c, RuleSource as d, RuleTraceEntry as e };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payid",
3
- "version": "1.0.5",
3
+ "version": "2.0.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -47,6 +47,12 @@
47
47
  "import": "./dist/core/server/index.js",
48
48
  "require": "./dist/core/server/index.js",
49
49
  "default": "./dist/core/server/index.js"
50
+ },
51
+ "./decision-proof": {
52
+ "types": "./dist/decision-proof/index.d.ts",
53
+ "import": "./dist/decision-proof/index.js",
54
+ "require": "./dist/decision-proof/index.js",
55
+ "default": "./dist/decision-proof/index.js"
50
56
  }
51
57
  },
52
58
  "files": [
@@ -56,7 +62,7 @@
56
62
  "scripts": {
57
63
  "build": "tsup",
58
64
  "type-check": "tsc --noEmit",
59
- "test": "echo 'No unit tests — add test files to run with bun test'",
65
+ "test": "bun test",
60
66
  "prepublishOnly": "bun run type-check && bun run test && bun run build",
61
67
  "release": "release-it",
62
68
  "release:dry": "release-it --dry-run",
@@ -74,8 +80,12 @@
74
80
  },
75
81
  "devDependencies": {
76
82
  "@types/bun": "latest",
83
+ "@wagmi/core": "^3.4.12",
84
+ "react": "^19.2.6",
77
85
  "release-it": "^20.0.1",
78
- "tsup": "^8.5.1"
86
+ "tsup": "^8.5.1",
87
+ "viem": "^2.49.3",
88
+ "wagmi": "^3.6.15"
79
89
  },
80
90
  "description": "PAY.ID policy engine — evaluate payment rules and generate EIP-712 Decision Proofs",
81
91
  "repository": {
@@ -1,50 +0,0 @@
1
- interface RuleResult {
2
- decision: "ALLOW" | "REJECT";
3
- code: string;
4
- reason?: string;
5
- }
6
- interface RuleTraceEntry {
7
- ruleId: string;
8
- field: string;
9
- op: string;
10
- expected: any;
11
- actual: any;
12
- result: "PASS" | "FAIL";
13
- }
14
- interface RuleDecisionDebug {
15
- trace: RuleTraceEntry[];
16
- }
17
- interface RuleResultDebug extends RuleResult {
18
- debug?: RuleDecisionDebug;
19
- }
20
-
21
- interface DecisionPayload {
22
- version: string;
23
- payId: string;
24
- payer: string;
25
- receiver: string;
26
- asset: string;
27
- amount: bigint;
28
- contextHash: string;
29
- ruleSetHash: string;
30
- ruleAuthority: string;
31
- issuedAt: bigint;
32
- expiresAt: bigint;
33
- nonce: string;
34
- requiresAttestation: boolean;
35
- attestationUIDsHash: string;
36
- }
37
- interface DecisionProof {
38
- payload: DecisionPayload;
39
- signature: string;
40
- }
41
-
42
- interface RuleSource {
43
- uri: string;
44
- hash?: string;
45
- }
46
- interface ResolverOptions {
47
- zgIndexerUrl?: string;
48
- }
49
-
50
- export type { DecisionPayload as D, ResolverOptions as R, DecisionProof as a, RuleDecisionDebug as b, RuleResult as c, RuleResultDebug as d, RuleSource as e, RuleTraceEntry as f };