payid 1.1.0 → 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 +478 -15
- package/dist/{chunk-X43JAJPI.js → chunk-AXLZUAYL.js} +20 -5
- package/dist/{chunk-GQJAHRA7.js → chunk-F2KQGW76.js} +22 -5
- package/dist/{chunk-XLQGSYE6.js → chunk-K2B5UHYA.js} +2 -2
- package/dist/{chunk-J5C4O242.js → chunk-WQEZYX4X.js} +2 -2
- package/dist/core/client/index.js +3 -3
- package/dist/core/server/index.js +3 -3
- package/dist/decision-proof/index.js +1 -1
- package/dist/index.js +11 -11
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -1,15 +1,478 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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 |
|
|
@@ -10,17 +10,32 @@ import {
|
|
|
10
10
|
import { ethers, ZeroAddress } from "ethers";
|
|
11
11
|
var hash = (v) => ethers.keccak256(ethers.toUtf8Bytes(v));
|
|
12
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
|
+
}
|
|
13
16
|
if (!ethers.isAddress(params.payer) || params.payer === ethers.ZeroAddress) {
|
|
14
|
-
throw new Error("GENERATE_PROOF: payer address
|
|
17
|
+
throw new Error("GENERATE_PROOF: payer address is invalid or zero");
|
|
15
18
|
}
|
|
16
19
|
if (!ethers.isAddress(params.receiver) || params.receiver === ethers.ZeroAddress) {
|
|
17
|
-
throw new Error("GENERATE_PROOF: receiver address
|
|
20
|
+
throw new Error("GENERATE_PROOF: receiver address is invalid or zero");
|
|
18
21
|
}
|
|
19
22
|
if (!ethers.isAddress(params.verifyingContract) || params.verifyingContract === ethers.ZeroAddress) {
|
|
20
|
-
throw new Error("GENERATE_PROOF: verifyingContract
|
|
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");
|
|
21
30
|
}
|
|
22
31
|
if (params.amount <= 0n) {
|
|
23
|
-
throw new Error("
|
|
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");
|
|
24
39
|
}
|
|
25
40
|
const now = params.blockTimestamp ?? Math.floor(Date.now() / 1e3);
|
|
26
41
|
const issuedAt = now - 30;
|
|
@@ -31,7 +46,7 @@ async function generateDecisionProof(params) {
|
|
|
31
46
|
chainId = Number(network.chainId);
|
|
32
47
|
}
|
|
33
48
|
if (!chainId || chainId <= 0 || !Number.isInteger(chainId)) {
|
|
34
|
-
throw new Error(`GENERATE_PROOF: chainId
|
|
49
|
+
throw new Error(`GENERATE_PROOF: chainId is invalid: ${chainId}`);
|
|
35
50
|
}
|
|
36
51
|
const requiresAttestation = Array.isArray(params.ruleConfig?.requires) && params.ruleConfig.requires.length > 0;
|
|
37
52
|
const attestationUIDsHash = params.attestationUIDs ? ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(["bytes32[]"], [params.attestationUIDs])) : ethers.ZeroHash;
|
|
@@ -129,29 +129,33 @@ async function runWasmRule(context, config, wasmBinary) {
|
|
|
129
129
|
async function runWasmRule2(context, config, _wasmBinary) {
|
|
130
130
|
return evaluateRule(context, config);
|
|
131
131
|
}
|
|
132
|
+
var MAX_RULE_DEPTH = 10;
|
|
132
133
|
function evaluateRule(context, config) {
|
|
133
134
|
const rules = config?.rules;
|
|
134
135
|
if (!Array.isArray(rules) || rules.length === 0) {
|
|
135
136
|
return { decision: "ALLOW", code: "NO_RULES", reason: "no rules defined" };
|
|
136
137
|
}
|
|
137
138
|
const logic = config?.logic ?? "AND";
|
|
138
|
-
return evalRules(context, rules, logic);
|
|
139
|
+
return evalRules(context, rules, logic, 0);
|
|
139
140
|
}
|
|
140
|
-
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
|
+
}
|
|
141
145
|
for (const rule of rules) {
|
|
142
|
-
const res = evalOneRule(context, rule);
|
|
146
|
+
const res = evalOneRule(context, rule, depth);
|
|
143
147
|
if (res.decision === "REJECT" && logic === "AND") return res;
|
|
144
148
|
if (res.decision === "ALLOW" && logic === "OR") return res;
|
|
145
149
|
}
|
|
146
150
|
if (logic === "AND") return { decision: "ALLOW", code: "OK", reason: "all rules passed" };
|
|
147
151
|
return { decision: "REJECT", code: "NO_RULE_MATCH", reason: "no rule matched in OR group" };
|
|
148
152
|
}
|
|
149
|
-
function evalOneRule(context, rule) {
|
|
153
|
+
function evalOneRule(context, rule, depth) {
|
|
150
154
|
const ruleId = rule?.id ?? "UNKNOWN_RULE";
|
|
151
155
|
const message = rule?.message ?? "";
|
|
152
156
|
if (Array.isArray(rule?.rules)) {
|
|
153
157
|
const subLogic = rule?.logic ?? "AND";
|
|
154
|
-
const res = evalRules(context, rule.rules, subLogic);
|
|
158
|
+
const res = evalRules(context, rule.rules, subLogic, depth + 1);
|
|
155
159
|
if (res.decision === "REJECT" && message) {
|
|
156
160
|
return { decision: "REJECT", code: ruleId, reason: message };
|
|
157
161
|
}
|
|
@@ -564,9 +568,22 @@ async function evaluate(context, ruleConfig, options, wasmBinary) {
|
|
|
564
568
|
if (!context.tx) {
|
|
565
569
|
throw new Error("evaluate(): context.tx is required");
|
|
566
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
|
+
}
|
|
567
578
|
if (!ruleConfig || typeof ruleConfig !== "object") {
|
|
568
579
|
throw new Error("evaluate(): ruleConfig is required");
|
|
569
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
|
+
}
|
|
570
587
|
let result;
|
|
571
588
|
try {
|
|
572
589
|
const preparedContext = options?.trustedIssuers ? preprocessContextV2(context, ruleConfig, options.trustedIssuers) : context;
|
|
@@ -9,10 +9,10 @@ import {
|
|
|
9
9
|
evaluate,
|
|
10
10
|
loadWasm,
|
|
11
11
|
resolveRule
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-F2KQGW76.js";
|
|
13
13
|
import {
|
|
14
14
|
generateDecisionProof
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-AXLZUAYL.js";
|
|
16
16
|
import {
|
|
17
17
|
__export
|
|
18
18
|
} from "./chunk-MLKGABMK.js";
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
PayIDClient,
|
|
3
3
|
createPayIDClient
|
|
4
|
-
} from "../../chunk-
|
|
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-
|
|
9
|
-
import "../../chunk-
|
|
8
|
+
import "../../chunk-F2KQGW76.js";
|
|
9
|
+
import "../../chunk-AXLZUAYL.js";
|
|
10
10
|
import "../../chunk-X7NYQ47Y.js";
|
|
11
11
|
import "../../chunk-KDC67LIN.js";
|
|
12
12
|
import "../../chunk-MLKGABMK.js";
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
PayIDServer,
|
|
3
3
|
createPayIDServer
|
|
4
|
-
} from "../../chunk-
|
|
5
|
-
import "../../chunk-
|
|
6
|
-
import "../../chunk-
|
|
4
|
+
} from "../../chunk-WQEZYX4X.js";
|
|
5
|
+
import "../../chunk-F2KQGW76.js";
|
|
6
|
+
import "../../chunk-AXLZUAYL.js";
|
|
7
7
|
import "../../chunk-X7NYQ47Y.js";
|
|
8
8
|
import "../../chunk-KDC67LIN.js";
|
|
9
9
|
import "../../chunk-MLKGABMK.js";
|
package/dist/index.js
CHANGED
|
@@ -1,33 +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-
|
|
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-
|
|
24
|
+
} from "./chunk-WQEZYX4X.js";
|
|
25
25
|
import {
|
|
26
26
|
verifyAttestation
|
|
27
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-F2KQGW76.js";
|
|
28
28
|
import {
|
|
29
29
|
generateDecisionProof
|
|
30
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-AXLZUAYL.js";
|
|
31
31
|
import "./chunk-X7NYQ47Y.js";
|
|
32
32
|
import "./chunk-KDC67LIN.js";
|
|
33
33
|
import {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payid",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"scripts": {
|
|
63
63
|
"build": "tsup",
|
|
64
64
|
"type-check": "tsc --noEmit",
|
|
65
|
-
"test": "
|
|
65
|
+
"test": "bun test",
|
|
66
66
|
"prepublishOnly": "bun run type-check && bun run test && bun run build",
|
|
67
67
|
"release": "release-it",
|
|
68
68
|
"release:dry": "release-it --dry-run",
|
|
@@ -80,8 +80,12 @@
|
|
|
80
80
|
},
|
|
81
81
|
"devDependencies": {
|
|
82
82
|
"@types/bun": "latest",
|
|
83
|
+
"@wagmi/core": "^3.4.12",
|
|
84
|
+
"react": "^19.2.6",
|
|
83
85
|
"release-it": "^20.0.1",
|
|
84
|
-
"tsup": "^8.5.1"
|
|
86
|
+
"tsup": "^8.5.1",
|
|
87
|
+
"viem": "^2.49.3",
|
|
88
|
+
"wagmi": "^3.6.15"
|
|
85
89
|
},
|
|
86
90
|
"description": "PAY.ID policy engine — evaluate payment rules and generate EIP-712 Decision Proofs",
|
|
87
91
|
"repository": {
|