@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.
Files changed (98) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/LICENSE +203 -0
  3. package/README.md +7 -0
  4. package/dist/decision-resolver.d.ts +5 -0
  5. package/dist/decision-resolver.d.ts.map +1 -0
  6. package/dist/decision-resolver.js +20 -0
  7. package/dist/decision-resolver.js.map +1 -0
  8. package/dist/errors.d.ts +3 -0
  9. package/dist/errors.d.ts.map +1 -0
  10. package/dist/errors.js +2 -0
  11. package/dist/errors.js.map +1 -0
  12. package/dist/index.d.ts +10 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.js +8 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/policies/allowlist.d.ts +3 -0
  17. package/dist/policies/allowlist.d.ts.map +1 -0
  18. package/dist/policies/allowlist.js +21 -0
  19. package/dist/policies/allowlist.js.map +1 -0
  20. package/dist/policies/budget-check.d.ts +3 -0
  21. package/dist/policies/budget-check.d.ts.map +1 -0
  22. package/dist/policies/budget-check.js +22 -0
  23. package/dist/policies/budget-check.js.map +1 -0
  24. package/dist/policies/index.d.ts +5 -0
  25. package/dist/policies/index.d.ts.map +1 -0
  26. package/dist/policies/index.js +5 -0
  27. package/dist/policies/index.js.map +1 -0
  28. package/dist/policies/max-per-call.d.ts +3 -0
  29. package/dist/policies/max-per-call.d.ts.map +1 -0
  30. package/dist/policies/max-per-call.js +20 -0
  31. package/dist/policies/max-per-call.js.map +1 -0
  32. package/dist/policies/rate-limit.d.ts +3 -0
  33. package/dist/policies/rate-limit.d.ts.map +1 -0
  34. package/dist/policies/rate-limit.js +33 -0
  35. package/dist/policies/rate-limit.js.map +1 -0
  36. package/dist/policy-engine.d.ts +8 -0
  37. package/dist/policy-engine.d.ts.map +1 -0
  38. package/dist/policy-engine.js +39 -0
  39. package/dist/policy-engine.js.map +1 -0
  40. package/dist/types/errors.d.ts +80 -0
  41. package/dist/types/errors.d.ts.map +1 -0
  42. package/dist/types/errors.js +150 -0
  43. package/dist/types/errors.js.map +1 -0
  44. package/dist/types/payment.d.ts +55 -0
  45. package/dist/types/payment.d.ts.map +1 -0
  46. package/dist/types/payment.js +188 -0
  47. package/dist/types/payment.js.map +1 -0
  48. package/dist/types/policy.d.ts +58 -0
  49. package/dist/types/policy.d.ts.map +1 -0
  50. package/dist/types/policy.js +194 -0
  51. package/dist/types/policy.js.map +1 -0
  52. package/dist/types/wallet.d.ts +66 -0
  53. package/dist/types/wallet.d.ts.map +1 -0
  54. package/dist/types/wallet.js +13 -0
  55. package/dist/types/wallet.js.map +1 -0
  56. package/dist/types.d.ts +117 -0
  57. package/dist/types.d.ts.map +1 -0
  58. package/dist/types.js +4 -0
  59. package/dist/types.js.map +1 -0
  60. package/dist/utils/crypto.d.ts +25 -0
  61. package/dist/utils/crypto.d.ts.map +1 -0
  62. package/dist/utils/crypto.js +119 -0
  63. package/dist/utils/crypto.js.map +1 -0
  64. package/dist/validation/schemas.d.ts +598 -0
  65. package/dist/validation/schemas.d.ts.map +1 -0
  66. package/dist/validation/schemas.js +178 -0
  67. package/dist/validation/schemas.js.map +1 -0
  68. package/package.json +30 -0
  69. package/src/decision-resolver.ts +16 -0
  70. package/src/errors.ts +20 -0
  71. package/src/index.ts +139 -0
  72. package/src/policies/allowlist.ts +26 -0
  73. package/src/policies/budget-check.ts +26 -0
  74. package/src/policies/index.ts +4 -0
  75. package/src/policies/max-per-call.ts +25 -0
  76. package/src/policies/rate-limit.ts +40 -0
  77. package/src/policy-engine.ts +65 -0
  78. package/src/types/errors.ts +215 -0
  79. package/src/types/payment.ts +297 -0
  80. package/src/types/policy.ts +289 -0
  81. package/src/types/wallet.ts +84 -0
  82. package/src/types.ts +193 -0
  83. package/src/utils/crypto.ts +182 -0
  84. package/src/validation/schemas.ts +208 -0
  85. package/test/architecture/architecture-invariants.test.ts +95 -0
  86. package/test/authorization-pipeline.test.ts +117 -0
  87. package/test/crypto.test.ts +56 -0
  88. package/test/decision-resolver.test.ts +52 -0
  89. package/test/errors.test.ts +97 -0
  90. package/test/payment-types.test.ts +49 -0
  91. package/test/policies.test.ts +162 -0
  92. package/test/policy-engine.test.ts +96 -0
  93. package/test/policy-types.test.ts +93 -0
  94. package/test/property/auth-invariants.test.ts +110 -0
  95. package/test/property/policy-invariants.test.ts +241 -0
  96. package/test/schemas.test.ts +58 -0
  97. package/tsconfig.json +9 -0
  98. package/vitest.config.ts +8 -0
@@ -0,0 +1,289 @@
1
+ import { ValidationError } from './errors.js'
2
+ import type { Address } from './wallet.js'
3
+
4
+ export const POLICY_DECISIONS = ['allow', 'deny', 'review'] as const
5
+
6
+ export type PolicyDecision = (typeof POLICY_DECISIONS)[number]
7
+
8
+ export interface PolicyReason {
9
+ code: string
10
+ message: string
11
+ detail?: Record<string, unknown>
12
+ }
13
+
14
+ export interface BasePolicy {
15
+ id: string
16
+ name: string
17
+ enabled: boolean
18
+ }
19
+
20
+ export interface BudgetPolicy extends BasePolicy {
21
+ kind: 'budget'
22
+ currency: 'USDC'
23
+ limit: bigint
24
+ softLimit?: bigint
25
+ }
26
+
27
+ export interface AllowlistPolicy extends BasePolicy {
28
+ kind: 'allowlist'
29
+ addresses: readonly Address[]
30
+ chains?: readonly number[]
31
+ }
32
+
33
+ export interface RateLimitPolicy extends BasePolicy {
34
+ kind: 'rate_limit'
35
+ maxRequests: number
36
+ intervalMs: number
37
+ burst?: number
38
+ scope: 'actor' | 'workspace' | 'ip'
39
+ }
40
+
41
+ export type PolicyDefinition = BudgetPolicy | AllowlistPolicy | RateLimitPolicy
42
+
43
+ export interface PolicyEvaluationContext {
44
+ actorId: string
45
+ workspaceId: string
46
+ recipient: Address
47
+ chainId: number
48
+ amount: bigint
49
+ currency: 'USDC'
50
+ timestampMs: number
51
+ spentAmount: bigint
52
+ requestCount: number
53
+ sourceIp?: string
54
+ }
55
+
56
+ export interface PolicyEvaluationResult {
57
+ policyId: string
58
+ decision: PolicyDecision
59
+ reason: PolicyReason
60
+ }
61
+
62
+ export interface AggregatedPolicyDecision {
63
+ decision: PolicyDecision
64
+ reasons: PolicyReason[]
65
+ evaluations: PolicyEvaluationResult[]
66
+ }
67
+
68
+ function assertBasePolicy(policy: BasePolicy): void {
69
+ if (!policy.id.trim()) {
70
+ throw new ValidationError('Policy id is required', { field: 'id' })
71
+ }
72
+
73
+ if (!policy.name.trim()) {
74
+ throw new ValidationError('Policy name is required', { field: 'name' })
75
+ }
76
+ }
77
+
78
+ function normalizeAddress(address: Address): string {
79
+ return address.toLowerCase()
80
+ }
81
+
82
+ export function evaluatePolicy(
83
+ policy: PolicyDefinition,
84
+ context: PolicyEvaluationContext
85
+ ): PolicyEvaluationResult {
86
+ assertBasePolicy(policy)
87
+
88
+ if (!policy.enabled) {
89
+ return {
90
+ policyId: policy.id,
91
+ decision: 'allow',
92
+ reason: {
93
+ code: 'policy_disabled',
94
+ message: `Policy ${policy.name} is disabled`,
95
+ },
96
+ }
97
+ }
98
+
99
+ switch (policy.kind) {
100
+ case 'budget': {
101
+ if (policy.currency !== context.currency) {
102
+ return {
103
+ policyId: policy.id,
104
+ decision: 'deny',
105
+ reason: {
106
+ code: 'budget_currency_mismatch',
107
+ message: `Budget policy ${policy.name} only allows ${policy.currency}`,
108
+ },
109
+ }
110
+ }
111
+
112
+ const nextSpend = context.spentAmount + context.amount
113
+ if (nextSpend > policy.limit) {
114
+ return {
115
+ policyId: policy.id,
116
+ decision: 'deny',
117
+ reason: {
118
+ code: 'budget_limit_exceeded',
119
+ message: `Budget policy ${policy.name} would exceed its hard limit`,
120
+ detail: {
121
+ limit: policy.limit.toString(),
122
+ spentAmount: context.spentAmount.toString(),
123
+ requestedAmount: context.amount.toString(),
124
+ },
125
+ },
126
+ }
127
+ }
128
+
129
+ if (policy.softLimit !== undefined && nextSpend > policy.softLimit) {
130
+ return {
131
+ policyId: policy.id,
132
+ decision: 'review',
133
+ reason: {
134
+ code: 'budget_soft_limit_exceeded',
135
+ message: `Budget policy ${policy.name} exceeded its soft limit`,
136
+ detail: {
137
+ softLimit: policy.softLimit.toString(),
138
+ nextSpend: nextSpend.toString(),
139
+ },
140
+ },
141
+ }
142
+ }
143
+
144
+ return {
145
+ policyId: policy.id,
146
+ decision: 'allow',
147
+ reason: {
148
+ code: 'budget_ok',
149
+ message: `Budget policy ${policy.name} approved the request`,
150
+ },
151
+ }
152
+ }
153
+
154
+ case 'allowlist': {
155
+ const addressAllowed = policy.addresses
156
+ .map((address) => normalizeAddress(address))
157
+ .includes(normalizeAddress(context.recipient))
158
+
159
+ if (!addressAllowed) {
160
+ return {
161
+ policyId: policy.id,
162
+ decision: 'deny',
163
+ reason: {
164
+ code: 'recipient_not_allowlisted',
165
+ message: `Recipient ${context.recipient} is not allowlisted by ${policy.name}`,
166
+ },
167
+ }
168
+ }
169
+
170
+ if (policy.chains && !policy.chains.includes(context.chainId)) {
171
+ return {
172
+ policyId: policy.id,
173
+ decision: 'deny',
174
+ reason: {
175
+ code: 'chain_not_allowlisted',
176
+ message: `Chain ${context.chainId} is not allowlisted by ${policy.name}`,
177
+ },
178
+ }
179
+ }
180
+
181
+ return {
182
+ policyId: policy.id,
183
+ decision: 'allow',
184
+ reason: {
185
+ code: 'allowlist_ok',
186
+ message: `Allowlist policy ${policy.name} approved the request`,
187
+ },
188
+ }
189
+ }
190
+
191
+ case 'rate_limit': {
192
+ if (!Number.isInteger(policy.maxRequests) || policy.maxRequests <= 0) {
193
+ throw new ValidationError('Rate limit maxRequests must be a positive integer', {
194
+ field: 'maxRequests',
195
+ value: policy.maxRequests,
196
+ })
197
+ }
198
+
199
+ if (!Number.isInteger(policy.intervalMs) || policy.intervalMs <= 0) {
200
+ throw new ValidationError('Rate limit intervalMs must be a positive integer', {
201
+ field: 'intervalMs',
202
+ value: policy.intervalMs,
203
+ })
204
+ }
205
+
206
+ const nextCount = context.requestCount + 1
207
+ if (nextCount > policy.maxRequests) {
208
+ return {
209
+ policyId: policy.id,
210
+ decision: 'deny',
211
+ reason: {
212
+ code: 'rate_limit_exceeded',
213
+ message: `Rate limit policy ${policy.name} denied the request`,
214
+ detail: {
215
+ scope: policy.scope,
216
+ maxRequests: policy.maxRequests,
217
+ requestCount: context.requestCount,
218
+ },
219
+ },
220
+ }
221
+ }
222
+
223
+ if (policy.burst !== undefined && nextCount > policy.burst) {
224
+ return {
225
+ policyId: policy.id,
226
+ decision: 'review',
227
+ reason: {
228
+ code: 'rate_limit_burst_review',
229
+ message: `Rate limit policy ${policy.name} exceeded its preferred burst`,
230
+ detail: {
231
+ burst: policy.burst,
232
+ requestCount: nextCount,
233
+ },
234
+ },
235
+ }
236
+ }
237
+
238
+ return {
239
+ policyId: policy.id,
240
+ decision: 'allow',
241
+ reason: {
242
+ code: 'rate_limit_ok',
243
+ message: `Rate limit policy ${policy.name} approved the request`,
244
+ },
245
+ }
246
+ }
247
+ }
248
+ }
249
+
250
+ export function aggregatePolicyDecisions(
251
+ evaluations: readonly PolicyEvaluationResult[]
252
+ ): AggregatedPolicyDecision {
253
+ const denyResult = evaluations.find((evaluation) => evaluation.decision === 'deny')
254
+ if (denyResult) {
255
+ return {
256
+ decision: 'deny',
257
+ reasons: evaluations
258
+ .filter((evaluation) => evaluation.decision === 'deny')
259
+ .map((evaluation) => evaluation.reason),
260
+ evaluations: [...evaluations],
261
+ }
262
+ }
263
+
264
+ const reviewResult = evaluations.find((evaluation) => evaluation.decision === 'review')
265
+ if (reviewResult) {
266
+ return {
267
+ decision: 'review',
268
+ reasons: evaluations
269
+ .filter((evaluation) => evaluation.decision === 'review')
270
+ .map((evaluation) => evaluation.reason),
271
+ evaluations: [...evaluations],
272
+ }
273
+ }
274
+
275
+ return {
276
+ decision: 'allow',
277
+ reasons: evaluations.map((evaluation) => evaluation.reason),
278
+ evaluations: [...evaluations],
279
+ }
280
+ }
281
+
282
+ export function evaluatePolicies(
283
+ policies: readonly PolicyDefinition[],
284
+ context: PolicyEvaluationContext
285
+ ): AggregatedPolicyDecision {
286
+ return aggregatePolicyDecisions(
287
+ policies.map((policy) => evaluatePolicy(policy, context))
288
+ )
289
+ }
@@ -0,0 +1,84 @@
1
+ export type Hex = `0x${string}`
2
+ export type Address = `0x${string}`
3
+
4
+ export type SmartWalletProvider = 'safe' | 'kernel' | 'circle'
5
+ export type EntryPointVersion = 'v0.6' | 'v0.7'
6
+
7
+ export interface SmartWallet {
8
+ address: Address
9
+ ownerAddress: Address
10
+ chainId: number
11
+ provider: SmartWalletProvider
12
+ entryPointAddress: Address
13
+ entryPointVersion: EntryPointVersion
14
+ factoryAddress?: Address
15
+ deployed: boolean
16
+ }
17
+
18
+ export interface UserOperationCall {
19
+ to: Address
20
+ value: bigint
21
+ data: Hex
22
+ }
23
+
24
+ export interface GasEstimate {
25
+ callGasLimit: bigint
26
+ verificationGasLimit: bigint
27
+ preVerificationGas: bigint
28
+ maxFeePerGas: bigint
29
+ maxPriorityFeePerGas: bigint
30
+ paymasterVerificationGasLimit?: bigint
31
+ paymasterPostOpGasLimit?: bigint
32
+ }
33
+
34
+ export interface UserOperationV06 {
35
+ version: 'v0.6'
36
+ sender: Address
37
+ nonce: bigint
38
+ initCode: Hex
39
+ callData: Hex
40
+ callGasLimit: bigint
41
+ verificationGasLimit: bigint
42
+ preVerificationGas: bigint
43
+ maxFeePerGas: bigint
44
+ maxPriorityFeePerGas: bigint
45
+ paymasterAndData: Hex
46
+ signature: Hex
47
+ }
48
+
49
+ export interface UserOperationV07 {
50
+ version: 'v0.7'
51
+ sender: Address
52
+ nonce: bigint
53
+ factory: Address | Hex
54
+ factoryData: Hex
55
+ callData: Hex
56
+ callGasLimit: bigint
57
+ verificationGasLimit: bigint
58
+ preVerificationGas: bigint
59
+ maxFeePerGas: bigint
60
+ maxPriorityFeePerGas: bigint
61
+ paymaster: Address | Hex
62
+ paymasterVerificationGasLimit: bigint
63
+ paymasterPostOpGasLimit: bigint
64
+ paymasterData: Hex
65
+ signature: Hex
66
+ }
67
+
68
+ export type UserOperation = UserOperationV06 | UserOperationV07
69
+
70
+ export function isUserOperationV06(value: UserOperation): value is UserOperationV06 {
71
+ return value.version === 'v0.6'
72
+ }
73
+
74
+ export function isUserOperationV07(value: UserOperation): value is UserOperationV07 {
75
+ return value.version === 'v0.7'
76
+ }
77
+
78
+ export function getUserOperationSender(value: UserOperation): Address {
79
+ return value.sender
80
+ }
81
+
82
+ export function getUserOperationNonce(value: UserOperation): bigint {
83
+ return value.nonce
84
+ }
package/src/types.ts ADDED
@@ -0,0 +1,193 @@
1
+ export interface Actor {
2
+ id: string
3
+ name: string
4
+ status: 'active' | 'frozen' | 'depleted'
5
+ createdAt: string
6
+ }
7
+
8
+ export interface Budget {
9
+ id: string
10
+ actorId: string
11
+ amount: bigint
12
+ used: bigint
13
+ currency: 'USDC'
14
+ period: 'monthly' | 'total'
15
+ resetAt: string
16
+ }
17
+
18
+ export interface Policy {
19
+ id: string
20
+ actorId: string
21
+ type: string
22
+ version: string
23
+ config: Record<string, unknown>
24
+ priority: number
25
+ }
26
+
27
+ export interface SettlementDescriptor {
28
+ address: string
29
+ chain: string
30
+ asset: string
31
+ }
32
+
33
+ export interface Target {
34
+ id: string
35
+ actorId: string
36
+ name: string
37
+ settlement: SettlementDescriptor
38
+ }
39
+
40
+ export interface Purpose {
41
+ type: string
42
+ id: string
43
+ description?: string
44
+ }
45
+
46
+ export interface Intent {
47
+ target: string
48
+ amount: bigint
49
+ purpose: Purpose
50
+ idempotencyKey?: string
51
+ }
52
+
53
+ export interface BudgetState {
54
+ total: bigint
55
+ used: bigint
56
+ remaining: bigint
57
+ period: string
58
+ currency: string
59
+ }
60
+
61
+ export interface PolicyResult {
62
+ policyId: string
63
+ policyType: string
64
+ version: string
65
+ decision: 'pass' | 'deny' | 'review'
66
+ reason: { code: string; message: string }
67
+ evaluatedAt: string
68
+ }
69
+
70
+ export type AuthorizationDecision = 'approved' | 'denied' | 'pending-review'
71
+
72
+ export interface Authorization {
73
+ intent: Intent
74
+ policyResults: PolicyResult[]
75
+ decision: AuthorizationDecision
76
+ evaluatedAt: string
77
+ actorVersion: string
78
+ }
79
+
80
+ export interface SettlementHint {
81
+ preferredChain?: string
82
+ deadline?: string
83
+ gasStrategy?: string
84
+ }
85
+
86
+ export interface ExecutionPlan {
87
+ authorization: Authorization
88
+ settlementHints: SettlementHint
89
+ }
90
+
91
+ export interface PolicyContext {
92
+ actor: Actor
93
+ intent: Intent
94
+ budget: BudgetState
95
+ evaluatedPolicies: PolicyResult[]
96
+ }
97
+
98
+ export type PolicyMiddleware = (
99
+ ctx: PolicyContext,
100
+ next: () => Promise<PolicyResult>
101
+ ) => Promise<PolicyResult>
102
+
103
+ export interface PolicyEvaluator {
104
+ id: string
105
+ type: string
106
+ version: string
107
+ middleware: PolicyMiddleware
108
+ }
109
+
110
+ export interface SimulationResult {
111
+ decision: AuthorizationDecision
112
+ policyResults: PolicyResult[]
113
+ estimatedGas?: bigint
114
+ estimatedCost?: bigint
115
+ }
116
+
117
+ export interface SignedTransaction {
118
+ serialized: `0x${string}`
119
+ hash: `0x${string}`
120
+ }
121
+
122
+ export interface Receipt {
123
+ txHash: `0x${string}`
124
+ status: 'confirmed' | 'failed'
125
+ blockNumber: bigint
126
+ gasUsed: bigint
127
+ effectiveGasPrice: bigint
128
+ }
129
+
130
+ export type {
131
+ PaymentIntentStatus,
132
+ PaymentAmount,
133
+ PaymentRecipient,
134
+ PaymentIntentFailure,
135
+ PaymentMetadataValue,
136
+ PaymentIntent,
137
+ SerializedBigInt,
138
+ } from './types/payment.js'
139
+
140
+ export {
141
+ PAYMENT_INTENT_STATUSES,
142
+ PAYMENT_INTENT_TERMINAL_STATUSES,
143
+ PAYMENT_INTENT_TRANSITIONS,
144
+ validatePaymentAmount,
145
+ isPaymentIntentTerminalStatus,
146
+ canTransitionPaymentIntentStatus,
147
+ assertValidPaymentIntentTransition,
148
+ assertPaymentIntent,
149
+ serializeBigInts,
150
+ deserializeBigInts,
151
+ serializePaymentIntent,
152
+ deserializePaymentIntent,
153
+ } from './types/payment.js'
154
+
155
+ export type {
156
+ PolicyDecision,
157
+ PolicyReason,
158
+ BasePolicy,
159
+ BudgetPolicy,
160
+ AllowlistPolicy,
161
+ RateLimitPolicy,
162
+ PolicyDefinition,
163
+ PolicyEvaluationContext,
164
+ PolicyEvaluationResult,
165
+ AggregatedPolicyDecision,
166
+ } from './types/policy.js'
167
+
168
+ export {
169
+ POLICY_DECISIONS,
170
+ evaluatePolicy,
171
+ aggregatePolicyDecisions,
172
+ evaluatePolicies,
173
+ } from './types/policy.js'
174
+
175
+ export type {
176
+ Hex,
177
+ Address,
178
+ SmartWalletProvider,
179
+ EntryPointVersion,
180
+ SmartWallet,
181
+ UserOperationCall,
182
+ GasEstimate,
183
+ UserOperationV06,
184
+ UserOperationV07,
185
+ UserOperation,
186
+ } from './types/wallet.js'
187
+
188
+ export {
189
+ isUserOperationV06,
190
+ isUserOperationV07,
191
+ getUserOperationSender,
192
+ getUserOperationNonce,
193
+ } from './types/wallet.js'