@brninpay/core 0.1.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.
- package/.turbo/turbo-build.log +4 -0
- package/LICENSE +203 -0
- package/README.md +7 -0
- package/dist/decision-resolver.d.ts +5 -0
- package/dist/decision-resolver.d.ts.map +1 -0
- package/dist/decision-resolver.js +20 -0
- package/dist/decision-resolver.js.map +1 -0
- package/dist/errors.d.ts +3 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +2 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/policies/allowlist.d.ts +3 -0
- package/dist/policies/allowlist.d.ts.map +1 -0
- package/dist/policies/allowlist.js +21 -0
- package/dist/policies/allowlist.js.map +1 -0
- package/dist/policies/budget-check.d.ts +3 -0
- package/dist/policies/budget-check.d.ts.map +1 -0
- package/dist/policies/budget-check.js +22 -0
- package/dist/policies/budget-check.js.map +1 -0
- package/dist/policies/index.d.ts +5 -0
- package/dist/policies/index.d.ts.map +1 -0
- package/dist/policies/index.js +5 -0
- package/dist/policies/index.js.map +1 -0
- package/dist/policies/max-per-call.d.ts +3 -0
- package/dist/policies/max-per-call.d.ts.map +1 -0
- package/dist/policies/max-per-call.js +20 -0
- package/dist/policies/max-per-call.js.map +1 -0
- package/dist/policies/rate-limit.d.ts +3 -0
- package/dist/policies/rate-limit.d.ts.map +1 -0
- package/dist/policies/rate-limit.js +33 -0
- package/dist/policies/rate-limit.js.map +1 -0
- package/dist/policy-engine.d.ts +8 -0
- package/dist/policy-engine.d.ts.map +1 -0
- package/dist/policy-engine.js +39 -0
- package/dist/policy-engine.js.map +1 -0
- package/dist/types/errors.d.ts +80 -0
- package/dist/types/errors.d.ts.map +1 -0
- package/dist/types/errors.js +150 -0
- package/dist/types/errors.js.map +1 -0
- package/dist/types/payment.d.ts +55 -0
- package/dist/types/payment.d.ts.map +1 -0
- package/dist/types/payment.js +188 -0
- package/dist/types/payment.js.map +1 -0
- package/dist/types/policy.d.ts +58 -0
- package/dist/types/policy.d.ts.map +1 -0
- package/dist/types/policy.js +194 -0
- package/dist/types/policy.js.map +1 -0
- package/dist/types/wallet.d.ts +66 -0
- package/dist/types/wallet.d.ts.map +1 -0
- package/dist/types/wallet.js +13 -0
- package/dist/types/wallet.js.map +1 -0
- package/dist/types.d.ts +117 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/crypto.d.ts +25 -0
- package/dist/utils/crypto.d.ts.map +1 -0
- package/dist/utils/crypto.js +119 -0
- package/dist/utils/crypto.js.map +1 -0
- package/dist/validation/schemas.d.ts +598 -0
- package/dist/validation/schemas.d.ts.map +1 -0
- package/dist/validation/schemas.js +178 -0
- package/dist/validation/schemas.js.map +1 -0
- package/package.json +30 -0
- package/src/decision-resolver.ts +16 -0
- package/src/errors.ts +20 -0
- package/src/index.ts +139 -0
- package/src/policies/allowlist.ts +26 -0
- package/src/policies/budget-check.ts +26 -0
- package/src/policies/index.ts +4 -0
- package/src/policies/max-per-call.ts +25 -0
- package/src/policies/rate-limit.ts +40 -0
- package/src/policy-engine.ts +65 -0
- package/src/types/errors.ts +215 -0
- package/src/types/payment.ts +297 -0
- package/src/types/policy.ts +289 -0
- package/src/types/wallet.ts +84 -0
- package/src/types.ts +193 -0
- package/src/utils/crypto.ts +182 -0
- package/src/validation/schemas.ts +208 -0
- package/test/architecture/architecture-invariants.test.ts +95 -0
- package/test/authorization-pipeline.test.ts +117 -0
- package/test/crypto.test.ts +56 -0
- package/test/decision-resolver.test.ts +52 -0
- package/test/errors.test.ts +97 -0
- package/test/payment-types.test.ts +49 -0
- package/test/policies.test.ts +162 -0
- package/test/policy-engine.test.ts +96 -0
- package/test/policy-types.test.ts +93 -0
- package/test/property/auth-invariants.test.ts +110 -0
- package/test/property/policy-invariants.test.ts +241 -0
- package/test/schemas.test.ts +58 -0
- package/tsconfig.json +9 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import fc from 'fast-check'
|
|
3
|
+
import { PolicyEngine } from '../../src/policy-engine.js'
|
|
4
|
+
import { createBudgetCheckPolicy } from '../../src/policies/budget-check.js'
|
|
5
|
+
import { createAllowlistPolicy } from '../../src/policies/allowlist.js'
|
|
6
|
+
import { createMaxPerCallPolicy } from '../../src/policies/max-per-call.js'
|
|
7
|
+
import { createRateLimitPolicy } from '../../src/policies/rate-limit.js'
|
|
8
|
+
import type { Actor, Intent, BudgetState } from '../../src/types.js'
|
|
9
|
+
|
|
10
|
+
const actor: Actor = {
|
|
11
|
+
id: 'actor_prop',
|
|
12
|
+
name: 'prop-test',
|
|
13
|
+
status: 'active',
|
|
14
|
+
createdAt: new Date().toISOString(),
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('Policy invariants (property-based)', () => {
|
|
18
|
+
it('adding more policies never changes deny to pass', async () => {
|
|
19
|
+
await fc.assert(
|
|
20
|
+
fc.asyncProperty(
|
|
21
|
+
fc.bigInt({ min: 0n, max: 1_000_000n }),
|
|
22
|
+
fc.bigInt({ min: 0n, max: 1_000_000n }),
|
|
23
|
+
fc.string({ minLength: 1 }),
|
|
24
|
+
async (budgetTotal, spendAmount, target) => {
|
|
25
|
+
fc.pre(budgetTotal > 0n)
|
|
26
|
+
const budget: BudgetState = {
|
|
27
|
+
total: budgetTotal,
|
|
28
|
+
used: 0n,
|
|
29
|
+
remaining: budgetTotal,
|
|
30
|
+
period: 'total',
|
|
31
|
+
currency: 'USDC',
|
|
32
|
+
}
|
|
33
|
+
const intent: Intent = {
|
|
34
|
+
target,
|
|
35
|
+
amount: spendAmount,
|
|
36
|
+
purpose: { type: 'test', id: 'prop' },
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const engineFewer = new PolicyEngine([
|
|
40
|
+
createAllowlistPolicy('pol_allow', '1', [target]),
|
|
41
|
+
])
|
|
42
|
+
const engineMore = new PolicyEngine([
|
|
43
|
+
createAllowlistPolicy('pol_allow', '1', [target]),
|
|
44
|
+
createMaxPerCallPolicy('pol_max', '1', spendAmount),
|
|
45
|
+
])
|
|
46
|
+
|
|
47
|
+
const resultFewer = await engineFewer.evaluate(actor, intent, budget)
|
|
48
|
+
const resultMore = await engineMore.evaluate(actor, intent, budget)
|
|
49
|
+
|
|
50
|
+
if (resultFewer.decision === 'approved') {
|
|
51
|
+
expect(resultMore.decision).toBe('approved')
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
),
|
|
55
|
+
{ numRuns: 100 }
|
|
56
|
+
)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('allowlist denies for all non-matching targets', async () => {
|
|
60
|
+
await fc.assert(
|
|
61
|
+
fc.asyncProperty(
|
|
62
|
+
fc.string({ minLength: 2 }),
|
|
63
|
+
fc.string({ minLength: 2 }),
|
|
64
|
+
async (allowed, target) => {
|
|
65
|
+
fc.pre(allowed !== target)
|
|
66
|
+
const budget: BudgetState = {
|
|
67
|
+
total: 1_000_000n,
|
|
68
|
+
used: 0n,
|
|
69
|
+
remaining: 1_000_000n,
|
|
70
|
+
period: 'total',
|
|
71
|
+
currency: 'USDC',
|
|
72
|
+
}
|
|
73
|
+
const engine = new PolicyEngine([
|
|
74
|
+
createAllowlistPolicy('pol_allow', '1', [allowed]),
|
|
75
|
+
])
|
|
76
|
+
const intent: Intent = {
|
|
77
|
+
target,
|
|
78
|
+
amount: 100n,
|
|
79
|
+
purpose: { type: 'test', id: 'prop' },
|
|
80
|
+
}
|
|
81
|
+
const auth = await engine.evaluate(actor, intent, budget)
|
|
82
|
+
expect(auth.decision).toBe('denied')
|
|
83
|
+
}
|
|
84
|
+
),
|
|
85
|
+
{ numRuns: 100 }
|
|
86
|
+
)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('max-per-call denies for any amount exceeding limit', async () => {
|
|
90
|
+
await fc.assert(
|
|
91
|
+
fc.asyncProperty(
|
|
92
|
+
fc.bigInt({ min: 1n, max: 1_000_000n }),
|
|
93
|
+
fc.bigInt({ min: 1n, max: 1_000_000n }),
|
|
94
|
+
async (limit, amount) => {
|
|
95
|
+
const engine = new PolicyEngine([
|
|
96
|
+
createMaxPerCallPolicy('pol_max', '1', limit),
|
|
97
|
+
])
|
|
98
|
+
const budget: BudgetState = {
|
|
99
|
+
total: 1_000_000_000n,
|
|
100
|
+
used: 0n,
|
|
101
|
+
remaining: 1_000_000_000n,
|
|
102
|
+
period: 'total',
|
|
103
|
+
currency: 'USDC',
|
|
104
|
+
}
|
|
105
|
+
const intent: Intent = {
|
|
106
|
+
target: 'test:svc',
|
|
107
|
+
amount,
|
|
108
|
+
purpose: { type: 'test', id: 'prop' },
|
|
109
|
+
}
|
|
110
|
+
const auth = await engine.evaluate(actor, intent, budget)
|
|
111
|
+
if (amount <= limit) {
|
|
112
|
+
expect(auth.decision).toBe('approved')
|
|
113
|
+
} else {
|
|
114
|
+
expect(auth.decision).toBe('denied')
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
),
|
|
118
|
+
{ numRuns: 100 }
|
|
119
|
+
)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('budget with used amount detects exhaustion correctly', async () => {
|
|
123
|
+
await fc.assert(
|
|
124
|
+
fc.asyncProperty(
|
|
125
|
+
fc.bigInt({ min: 1n, max: 1_000_000n }),
|
|
126
|
+
fc.bigInt({ min: 0n, max: 1_000_000n }),
|
|
127
|
+
fc.bigInt({ min: 0n, max: 1_000_000n }),
|
|
128
|
+
async (total, used, spendAmount) => {
|
|
129
|
+
fc.pre(used <= total)
|
|
130
|
+
const remaining = total - used
|
|
131
|
+
const budget: BudgetState = {
|
|
132
|
+
total,
|
|
133
|
+
used,
|
|
134
|
+
remaining,
|
|
135
|
+
period: 'total',
|
|
136
|
+
currency: 'USDC',
|
|
137
|
+
}
|
|
138
|
+
const engine = new PolicyEngine([
|
|
139
|
+
createBudgetCheckPolicy('pol_budget', '1'),
|
|
140
|
+
])
|
|
141
|
+
const intent: Intent = {
|
|
142
|
+
target: 'test:svc',
|
|
143
|
+
amount: spendAmount,
|
|
144
|
+
purpose: { type: 'test', id: 'prop' },
|
|
145
|
+
}
|
|
146
|
+
const auth = await engine.evaluate(actor, intent, budget)
|
|
147
|
+
const projected = used + spendAmount
|
|
148
|
+
if (projected > total) {
|
|
149
|
+
expect(auth.decision).toBe('denied')
|
|
150
|
+
} else {
|
|
151
|
+
expect(auth.decision).toBe('approved')
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
),
|
|
155
|
+
{ numRuns: 100 }
|
|
156
|
+
)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('rate limit passes for calls within limit', async () => {
|
|
160
|
+
await fc.assert(
|
|
161
|
+
fc.asyncProperty(
|
|
162
|
+
fc.integer({ min: 1, max: 20 }),
|
|
163
|
+
fc.integer({ min: 0, max: 49 }),
|
|
164
|
+
async (maxCalls, callIndex) => {
|
|
165
|
+
const store = new Map<string, number[]>()
|
|
166
|
+
const policy = createRateLimitPolicy('pol_rate', '1', maxCalls, 60_000, store)
|
|
167
|
+
const engine = new PolicyEngine([policy])
|
|
168
|
+
const budget: BudgetState = {
|
|
169
|
+
total: 1_000_000n,
|
|
170
|
+
used: 0n,
|
|
171
|
+
remaining: 1_000_000n,
|
|
172
|
+
period: 'total',
|
|
173
|
+
currency: 'USDC',
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for (let i = 0; i <= callIndex; i++) {
|
|
177
|
+
const intent: Intent = {
|
|
178
|
+
target: 'test:svc',
|
|
179
|
+
amount: 100n,
|
|
180
|
+
purpose: { type: 'test', id: `call-${i}` },
|
|
181
|
+
}
|
|
182
|
+
const auth = await engine.evaluate(actor, intent, budget)
|
|
183
|
+
if (i < maxCalls) {
|
|
184
|
+
expect(auth.decision).toBe('approved')
|
|
185
|
+
} else {
|
|
186
|
+
expect(auth.decision).toBe('denied')
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
),
|
|
191
|
+
{ numRuns: 50 }
|
|
192
|
+
)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('zero amount payment never affects budget', async () => {
|
|
196
|
+
await fc.assert(
|
|
197
|
+
fc.asyncProperty(
|
|
198
|
+
fc.bigInt({ min: 1n, max: 1_000_000n }),
|
|
199
|
+
async (total) => {
|
|
200
|
+
const budget: BudgetState = {
|
|
201
|
+
total,
|
|
202
|
+
used: 0n,
|
|
203
|
+
remaining: total,
|
|
204
|
+
period: 'total',
|
|
205
|
+
currency: 'USDC',
|
|
206
|
+
}
|
|
207
|
+
const engine = new PolicyEngine([
|
|
208
|
+
createBudgetCheckPolicy('pol_budget', '1'),
|
|
209
|
+
])
|
|
210
|
+
const intent0: Intent = {
|
|
211
|
+
target: 'test:svc',
|
|
212
|
+
amount: 0n,
|
|
213
|
+
purpose: { type: 'test', id: 'zero' },
|
|
214
|
+
}
|
|
215
|
+
const intent1: Intent = {
|
|
216
|
+
target: 'test:svc',
|
|
217
|
+
amount: 1n,
|
|
218
|
+
purpose: { type: 'test', id: 'one' },
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const auth0 = await engine.evaluate(actor, intent0, budget)
|
|
222
|
+
expect(auth0.decision).toBe('approved')
|
|
223
|
+
|
|
224
|
+
const budgetAfter: BudgetState = {
|
|
225
|
+
...budget,
|
|
226
|
+
used: total,
|
|
227
|
+
remaining: 0n,
|
|
228
|
+
}
|
|
229
|
+
const intentTooMuch: Intent = {
|
|
230
|
+
target: 'test:svc',
|
|
231
|
+
amount: 0n,
|
|
232
|
+
purpose: { type: 'test', id: 'zero-after' },
|
|
233
|
+
}
|
|
234
|
+
const authZeroAfter = await engine.evaluate(actor, intentTooMuch, budgetAfter)
|
|
235
|
+
expect(authZeroAfter.decision).toBe('approved')
|
|
236
|
+
}
|
|
237
|
+
),
|
|
238
|
+
{ numRuns: 50 }
|
|
239
|
+
)
|
|
240
|
+
})
|
|
241
|
+
})
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
parseAddress,
|
|
4
|
+
parsePaymentIntent,
|
|
5
|
+
parseUserOperation,
|
|
6
|
+
} from '../src/validation/schemas.js'
|
|
7
|
+
|
|
8
|
+
describe('validation schemas', () => {
|
|
9
|
+
it('parses payment intent payloads and normalizes bigint values', () => {
|
|
10
|
+
const result = parsePaymentIntent({
|
|
11
|
+
id: 'pi_123',
|
|
12
|
+
workspaceId: 'ws_123',
|
|
13
|
+
actorId: 'actor_123',
|
|
14
|
+
status: 'submitted',
|
|
15
|
+
amount: {
|
|
16
|
+
currency: 'USDC',
|
|
17
|
+
value: '1500000',
|
|
18
|
+
decimals: 6,
|
|
19
|
+
},
|
|
20
|
+
recipient: {
|
|
21
|
+
address: '0x1111111111111111111111111111111111111111',
|
|
22
|
+
chainId: 8453,
|
|
23
|
+
},
|
|
24
|
+
createdAt: '2026-07-06T10:00:00.000Z',
|
|
25
|
+
updatedAt: '2026-07-06T10:00:00.000Z',
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
expect(result.amount.value).toBe(1500000n)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('rejects invalid addresses', () => {
|
|
32
|
+
expect(() => parseAddress('not-an-address')).toThrow()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('parses ERC-4337 v0.7 user operations', () => {
|
|
36
|
+
const result = parseUserOperation({
|
|
37
|
+
version: 'v0.7',
|
|
38
|
+
sender: '0x1111111111111111111111111111111111111111',
|
|
39
|
+
nonce: '1',
|
|
40
|
+
factory: '0x2222222222222222222222222222222222222222',
|
|
41
|
+
factoryData: '0x',
|
|
42
|
+
callData: '0x1234',
|
|
43
|
+
callGasLimit: '21000',
|
|
44
|
+
verificationGasLimit: '50000',
|
|
45
|
+
preVerificationGas: '25000',
|
|
46
|
+
maxFeePerGas: '1000000000',
|
|
47
|
+
maxPriorityFeePerGas: '100000000',
|
|
48
|
+
paymaster: '0x3333333333333333333333333333333333333333',
|
|
49
|
+
paymasterVerificationGasLimit: '40000',
|
|
50
|
+
paymasterPostOpGasLimit: '15000',
|
|
51
|
+
paymasterData: '0x',
|
|
52
|
+
signature: '0x1234',
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
expect(result.version).toBe('v0.7')
|
|
56
|
+
expect(result.nonce).toBe(1n)
|
|
57
|
+
})
|
|
58
|
+
})
|
package/tsconfig.json
ADDED