@opencard-dev/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/dist/db.d.ts +145 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +373 -0
- package/dist/db.js.map +1 -0
- package/dist/errors.d.ts +72 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +124 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +67 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +53 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +109 -0
- package/dist/logger.js.map +1 -0
- package/dist/rules-engine.d.ts +159 -0
- package/dist/rules-engine.d.ts.map +1 -0
- package/dist/rules-engine.js +375 -0
- package/dist/rules-engine.js.map +1 -0
- package/dist/rules-store.d.ts +187 -0
- package/dist/rules-store.d.ts.map +1 -0
- package/dist/rules-store.js +291 -0
- package/dist/rules-store.js.map +1 -0
- package/dist/rules-validation.d.ts +54 -0
- package/dist/rules-validation.d.ts.map +1 -0
- package/dist/rules-validation.js +110 -0
- package/dist/rules-validation.js.map +1 -0
- package/dist/stripe-client.d.ts +154 -0
- package/dist/stripe-client.d.ts.map +1 -0
- package/dist/stripe-client.js +444 -0
- package/dist/stripe-client.js.map +1 -0
- package/dist/test-utils.d.ts +55 -0
- package/dist/test-utils.d.ts.map +1 -0
- package/dist/test-utils.js +91 -0
- package/dist/test-utils.js.map +1 -0
- package/dist/tracker.d.ts +130 -0
- package/dist/tracker.d.ts.map +1 -0
- package/dist/tracker.js +196 -0
- package/dist/tracker.js.map +1 -0
- package/dist/transaction-reconciler.d.ts +30 -0
- package/dist/transaction-reconciler.d.ts.map +1 -0
- package/dist/transaction-reconciler.js +131 -0
- package/dist/transaction-reconciler.js.map +1 -0
- package/dist/types.d.ts +194 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/dist/webhooks.d.ts +121 -0
- package/dist/webhooks.d.ts.map +1 -0
- package/dist/webhooks.js +307 -0
- package/dist/webhooks.js.map +1 -0
- package/package.json +46 -0
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Rules Engine
|
|
4
|
+
* ============
|
|
5
|
+
* Two responsibilities:
|
|
6
|
+
*
|
|
7
|
+
* 1. SpendRules — a fluent builder that makes it easy to construct SpendRule
|
|
8
|
+
* objects without remembering field names. Also ships pre-built templates
|
|
9
|
+
* for common agent configurations.
|
|
10
|
+
*
|
|
11
|
+
* 2. evaluateAuthorization — takes a live Stripe authorization request plus
|
|
12
|
+
* a card's rules and returns an approve/decline decision. This is called
|
|
13
|
+
* from the webhook handler in real time (within the ~2s Stripe window).
|
|
14
|
+
*
|
|
15
|
+
* All amounts are in cents throughout. Stripe uses cents; we match that to
|
|
16
|
+
* avoid any float-to-dollar conversion bugs.
|
|
17
|
+
*
|
|
18
|
+
* ─── Design philosophy ───────────────────────────────────────────────────────
|
|
19
|
+
* The rules engine enforces two core hard controls:
|
|
20
|
+
* 1. Spend limits (daily, monthly, per-transaction)
|
|
21
|
+
* 2. Category locks (allowed/blocked merchant categories)
|
|
22
|
+
*
|
|
23
|
+
* Confidence scoring — where agents self-report how sure they are about a
|
|
24
|
+
* purchase — was deliberately removed. Self-reported confidence is unreliable:
|
|
25
|
+
* an agent with no safety awareness will report 1.0; a cautious agent might
|
|
26
|
+
* report 0.7 for a perfectly reasonable purchase. It adds noise without
|
|
27
|
+
* adding real safety. Hard limits are the guardrail. Category locks add
|
|
28
|
+
* specificity. That's the model.
|
|
29
|
+
*/
|
|
30
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
|
+
exports.SpendRules = void 0;
|
|
32
|
+
exports.evaluateRule = evaluateRule;
|
|
33
|
+
exports.evaluateAuthorization = evaluateAuthorization;
|
|
34
|
+
const logger_1 = require("./logger");
|
|
35
|
+
const logger = (0, logger_1.getLogger)('rules-engine');
|
|
36
|
+
class SpendRules {
|
|
37
|
+
rule = {};
|
|
38
|
+
/**
|
|
39
|
+
* Create a new SpendRules builder
|
|
40
|
+
*/
|
|
41
|
+
constructor() {
|
|
42
|
+
this.rule = {};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Load a pre-built template
|
|
46
|
+
*/
|
|
47
|
+
static template(template) {
|
|
48
|
+
const builder = new SpendRules();
|
|
49
|
+
switch (template) {
|
|
50
|
+
case 'balanced':
|
|
51
|
+
// Reasonable defaults for a general-purpose agent card.
|
|
52
|
+
// Hard daily limit + per-transaction cap, decline on violation.
|
|
53
|
+
return builder
|
|
54
|
+
.dailyLimit(50000) // $500/day
|
|
55
|
+
.maxPerTx(10000) // $100/tx
|
|
56
|
+
.onFailure('decline')
|
|
57
|
+
.name('Balanced')
|
|
58
|
+
.description('General-purpose card with a $500/day cap and $100 per-transaction limit');
|
|
59
|
+
case 'category_locked':
|
|
60
|
+
// Restricted categories with low per-transaction limit
|
|
61
|
+
return builder
|
|
62
|
+
.category(['grocery', 'transit', 'fuel'])
|
|
63
|
+
.maxPerTx(2000) // $20/tx max
|
|
64
|
+
.dailyLimit(10000) // $100/day
|
|
65
|
+
.onFailure('decline')
|
|
66
|
+
.name('Category Locked')
|
|
67
|
+
.description('Restricted to specific categories, low transaction limit');
|
|
68
|
+
case 'daily_limit':
|
|
69
|
+
// Simple daily budget
|
|
70
|
+
return builder
|
|
71
|
+
.dailyLimit(10000) // $100/day
|
|
72
|
+
.onFailure('decline')
|
|
73
|
+
.name('Daily Limit')
|
|
74
|
+
.description('Hard daily spending cap of $100');
|
|
75
|
+
case 'approval_gated':
|
|
76
|
+
// High transactions require human approval.
|
|
77
|
+
// Note: 'alert' onFailure behaves as 'decline' today (see onFailure docs).
|
|
78
|
+
// Use the opencard_request_approval MCP tool for actual human-in-the-loop approval.
|
|
79
|
+
return builder
|
|
80
|
+
.approvalThreshold(5000) // $50+
|
|
81
|
+
.requiresApproval(true)
|
|
82
|
+
.onFailure('decline')
|
|
83
|
+
.name('Approval Gated')
|
|
84
|
+
.description('Transactions over $50 require human approval');
|
|
85
|
+
default:
|
|
86
|
+
const _exhaustive = template;
|
|
87
|
+
return _exhaustive;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Set allowed merchant categories
|
|
92
|
+
* Only transactions in these categories will be approved
|
|
93
|
+
*/
|
|
94
|
+
category(categories) {
|
|
95
|
+
this.rule.allowedCategories = categories;
|
|
96
|
+
return this;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Set blocked merchant categories
|
|
100
|
+
* Transactions in these categories will be declined
|
|
101
|
+
*/
|
|
102
|
+
blockCategory(categories) {
|
|
103
|
+
this.rule.blockedCategories = categories;
|
|
104
|
+
return this;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Set maximum amount per transaction (in cents)
|
|
108
|
+
*/
|
|
109
|
+
maxPerTx(cents) {
|
|
110
|
+
if (cents < 0) {
|
|
111
|
+
throw new Error('Max per transaction must be >= 0');
|
|
112
|
+
}
|
|
113
|
+
this.rule.maxPerTransaction = cents;
|
|
114
|
+
return this;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Set daily spending limit (in cents)
|
|
118
|
+
*/
|
|
119
|
+
dailyLimit(cents) {
|
|
120
|
+
if (cents < 0) {
|
|
121
|
+
throw new Error('Daily limit must be >= 0');
|
|
122
|
+
}
|
|
123
|
+
this.rule.dailyLimit = cents;
|
|
124
|
+
this.rule.spendingInterval = 'daily';
|
|
125
|
+
return this;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Set monthly spending limit (in cents)
|
|
129
|
+
*/
|
|
130
|
+
monthlyLimit(cents) {
|
|
131
|
+
if (cents < 0) {
|
|
132
|
+
throw new Error('Monthly limit must be >= 0');
|
|
133
|
+
}
|
|
134
|
+
this.rule.monthlyLimit = cents;
|
|
135
|
+
this.rule.spendingInterval = 'monthly';
|
|
136
|
+
return this;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Set per-authorization limit (in cents)
|
|
140
|
+
*/
|
|
141
|
+
perAuthLimit(cents) {
|
|
142
|
+
if (cents < 0) {
|
|
143
|
+
throw new Error('Per-auth limit must be >= 0');
|
|
144
|
+
}
|
|
145
|
+
this.rule.maxPerTransaction = cents;
|
|
146
|
+
this.rule.spendingInterval = 'per_authorization';
|
|
147
|
+
return this;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Set whether transactions above the approval threshold require human sign-off.
|
|
151
|
+
* Usually you don't call this directly — `approvalThreshold()` sets it automatically.
|
|
152
|
+
* But you can call it explicitly if you need to toggle approval requirements
|
|
153
|
+
* independently of the threshold value.
|
|
154
|
+
*/
|
|
155
|
+
requiresApproval(required) {
|
|
156
|
+
this.rule.requiresApproval = required;
|
|
157
|
+
return this;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Set approval threshold (in cents)
|
|
161
|
+
* Transactions above this amount require human approval.
|
|
162
|
+
* Also sets requiresApproval = true automatically.
|
|
163
|
+
*/
|
|
164
|
+
approvalThreshold(cents) {
|
|
165
|
+
if (cents < 0) {
|
|
166
|
+
throw new Error('Approval threshold must be >= 0');
|
|
167
|
+
}
|
|
168
|
+
this.rule.approvalThreshold = cents;
|
|
169
|
+
this.rule.requiresApproval = true;
|
|
170
|
+
return this;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Set failure handling behavior.
|
|
174
|
+
*
|
|
175
|
+
* Currently only 'decline' is fully implemented in the authorization flow.
|
|
176
|
+
* 'alert' and 'pause' are accepted for forward compatibility but behave
|
|
177
|
+
* the same as 'decline' today — the charge is declined. These modes will
|
|
178
|
+
* be wired to notification and card-pausing logic in a future release.
|
|
179
|
+
*/
|
|
180
|
+
onFailure(action) {
|
|
181
|
+
this.rule.onFailure = action;
|
|
182
|
+
return this;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Set human-readable name for this rule set
|
|
186
|
+
*/
|
|
187
|
+
name(name) {
|
|
188
|
+
this.rule.name = name;
|
|
189
|
+
return this;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Set human-readable description for this rule set
|
|
193
|
+
*/
|
|
194
|
+
description(desc) {
|
|
195
|
+
this.rule.description = desc;
|
|
196
|
+
return this;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Build and return the final SpendRule object
|
|
200
|
+
*/
|
|
201
|
+
build() {
|
|
202
|
+
if (this.rule.onFailure === 'alert' || this.rule.onFailure === 'pause') {
|
|
203
|
+
logger.warn(`Alert/pause onFailure modes are not yet implemented. Rule will default to decline behavior: ${JSON.stringify(this.rule)}`);
|
|
204
|
+
}
|
|
205
|
+
return { ...this.rule };
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Get a string representation of the rules
|
|
209
|
+
*/
|
|
210
|
+
toString() {
|
|
211
|
+
const parts = [];
|
|
212
|
+
if (this.rule.allowedCategories?.length) {
|
|
213
|
+
parts.push(`categories: [${this.rule.allowedCategories.join(', ')}]`);
|
|
214
|
+
}
|
|
215
|
+
if (this.rule.maxPerTransaction !== undefined) {
|
|
216
|
+
const dollars = (this.rule.maxPerTransaction / 100).toFixed(2);
|
|
217
|
+
parts.push(`max per tx: $${dollars}`);
|
|
218
|
+
}
|
|
219
|
+
if (this.rule.dailyLimit !== undefined) {
|
|
220
|
+
const dollars = (this.rule.dailyLimit / 100).toFixed(2);
|
|
221
|
+
parts.push(`daily limit: $${dollars}`);
|
|
222
|
+
}
|
|
223
|
+
if (this.rule.monthlyLimit !== undefined) {
|
|
224
|
+
const dollars = (this.rule.monthlyLimit / 100).toFixed(2);
|
|
225
|
+
parts.push(`monthly limit: $${dollars}`);
|
|
226
|
+
}
|
|
227
|
+
if (this.rule.requiresApproval && this.rule.approvalThreshold) {
|
|
228
|
+
const dollars = (this.rule.approvalThreshold / 100).toFixed(2);
|
|
229
|
+
parts.push(`approval required >$${dollars}`);
|
|
230
|
+
}
|
|
231
|
+
if (this.rule.onFailure) {
|
|
232
|
+
parts.push(`on violation: ${this.rule.onFailure}`);
|
|
233
|
+
}
|
|
234
|
+
return `SpendRules(${parts.join(', ')})`;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
exports.SpendRules = SpendRules;
|
|
238
|
+
/**
|
|
239
|
+
* Evaluate a transaction against a rule set
|
|
240
|
+
* Returns { allowed: boolean, reason?: string }
|
|
241
|
+
*/
|
|
242
|
+
function evaluateRule(rule, transaction) {
|
|
243
|
+
// Check allowed categories
|
|
244
|
+
if (rule.allowedCategories?.length &&
|
|
245
|
+
transaction.category &&
|
|
246
|
+
!rule.allowedCategories.includes(transaction.category)) {
|
|
247
|
+
return {
|
|
248
|
+
allowed: false,
|
|
249
|
+
reason: `Category '${transaction.category}' not in allowed list`,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
// Check blocked categories
|
|
253
|
+
if (rule.blockedCategories?.length &&
|
|
254
|
+
transaction.category &&
|
|
255
|
+
rule.blockedCategories.includes(transaction.category)) {
|
|
256
|
+
return {
|
|
257
|
+
allowed: false,
|
|
258
|
+
reason: `Category '${transaction.category}' is blocked`,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
// Check max per transaction
|
|
262
|
+
if (rule.maxPerTransaction !== undefined &&
|
|
263
|
+
transaction.amount > rule.maxPerTransaction) {
|
|
264
|
+
const txDollars = (transaction.amount / 100).toFixed(2);
|
|
265
|
+
const maxDollars = (rule.maxPerTransaction / 100).toFixed(2);
|
|
266
|
+
return {
|
|
267
|
+
allowed: false,
|
|
268
|
+
reason: `Transaction amount $${txDollars} exceeds per-tx limit $${maxDollars}`,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
// If all checks pass
|
|
272
|
+
return { allowed: true };
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* evaluateAuthorization
|
|
276
|
+
* ──────────────────────
|
|
277
|
+
* The core decision function. Given a live Stripe authorization request and
|
|
278
|
+
* the card's spending rules, determines whether to approve or decline.
|
|
279
|
+
*
|
|
280
|
+
* Rules are checked in order of cheapest/fastest first:
|
|
281
|
+
* 1. Merchant category (allowed list, then blocked list)
|
|
282
|
+
* 2. Per-transaction amount limit
|
|
283
|
+
* 3. Daily budget (current spend + this tx > limit?)
|
|
284
|
+
* 4. Monthly budget
|
|
285
|
+
*
|
|
286
|
+
* The first failing check short-circuits and returns a decline with a reason
|
|
287
|
+
* string that explains exactly which rule tripped.
|
|
288
|
+
*
|
|
289
|
+
* @param auth - The Stripe Issuing.Authorization from the webhook event
|
|
290
|
+
* @param rules - The SpendRule configured for this card
|
|
291
|
+
* @param context - Current spend totals from the tracker
|
|
292
|
+
* @returns AuthorizationDecision with approved flag and human-readable reason
|
|
293
|
+
*/
|
|
294
|
+
function evaluateAuthorization(auth, rules, context) {
|
|
295
|
+
// ── Extract key fields from the Stripe authorization object ─────────────
|
|
296
|
+
//
|
|
297
|
+
// auth.amount — the amount in cents the merchant is requesting
|
|
298
|
+
// auth.merchant_data.category — MCC (merchant category code) as a string
|
|
299
|
+
// e.g. "grocery_stores", "airlines", "restaurants"
|
|
300
|
+
const amount = auth.amount; // cents
|
|
301
|
+
const category = auth.merchant_data?.category || '';
|
|
302
|
+
// ── Check 1: Allowed categories ─────────────────────────────────────────
|
|
303
|
+
// If the rule specifies an allowlist, the merchant's category MUST be on it.
|
|
304
|
+
// This is useful for cards meant for a specific purpose (e.g. a card that
|
|
305
|
+
// should only be used for SaaS subscriptions).
|
|
306
|
+
if (rules.allowedCategories && rules.allowedCategories.length > 0) {
|
|
307
|
+
if (category && !rules.allowedCategories.includes(category)) {
|
|
308
|
+
return {
|
|
309
|
+
approved: false,
|
|
310
|
+
reason: `Merchant category '${category}' is not in the allowed list: [${rules.allowedCategories.join(', ')}]`,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
// ── Check 2: Blocked categories ─────────────────────────────────────────
|
|
315
|
+
// If the rule specifies a blocklist, ANY match immediately declines.
|
|
316
|
+
// Blocked list takes precedence even if the category is also in the allowed
|
|
317
|
+
// list (though that would be a misconfigured rule).
|
|
318
|
+
if (rules.blockedCategories && rules.blockedCategories.length > 0) {
|
|
319
|
+
if (category && rules.blockedCategories.includes(category)) {
|
|
320
|
+
return {
|
|
321
|
+
approved: false,
|
|
322
|
+
reason: `Merchant category '${category}' is explicitly blocked`,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// ── Check 3: Per-transaction limit ──────────────────────────────────────
|
|
327
|
+
// The simplest budget check: if this single transaction is too large,
|
|
328
|
+
// decline it regardless of running totals.
|
|
329
|
+
if (rules.maxPerTransaction !== undefined && amount > rules.maxPerTransaction) {
|
|
330
|
+
const txDollars = (amount / 100).toFixed(2);
|
|
331
|
+
const limitDollars = (rules.maxPerTransaction / 100).toFixed(2);
|
|
332
|
+
return {
|
|
333
|
+
approved: false,
|
|
334
|
+
reason: `Transaction $${txDollars} exceeds per-transaction limit of $${limitDollars}`,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
// ── Check 4: Daily budget ────────────────────────────────────────────────
|
|
338
|
+
// Would adding this transaction push today's total over the daily limit?
|
|
339
|
+
// We use the PROSPECTIVE total (spent + this amount) not the current total,
|
|
340
|
+
// so a card at $95/day with a $100 limit can't approve a $10 transaction.
|
|
341
|
+
if (rules.dailyLimit !== undefined) {
|
|
342
|
+
const prospectiveDaily = context.dailySpend + amount;
|
|
343
|
+
if (prospectiveDaily > rules.dailyLimit) {
|
|
344
|
+
const spentDollars = (context.dailySpend / 100).toFixed(2);
|
|
345
|
+
const txDollars = (amount / 100).toFixed(2);
|
|
346
|
+
const limitDollars = (rules.dailyLimit / 100).toFixed(2);
|
|
347
|
+
return {
|
|
348
|
+
approved: false,
|
|
349
|
+
reason: `Daily limit exceeded: already spent $${spentDollars} + $${txDollars} > daily limit $${limitDollars}`,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
// ── Check 5: Monthly budget ─────────────────────────────────────────────
|
|
354
|
+
// Same as daily, but for the calendar month.
|
|
355
|
+
if (rules.monthlyLimit !== undefined) {
|
|
356
|
+
const prospectiveMonthly = context.monthlySpend + amount;
|
|
357
|
+
if (prospectiveMonthly > rules.monthlyLimit) {
|
|
358
|
+
const spentDollars = (context.monthlySpend / 100).toFixed(2);
|
|
359
|
+
const txDollars = (amount / 100).toFixed(2);
|
|
360
|
+
const limitDollars = (rules.monthlyLimit / 100).toFixed(2);
|
|
361
|
+
return {
|
|
362
|
+
approved: false,
|
|
363
|
+
reason: `Monthly limit exceeded: already spent $${spentDollars} + $${txDollars} > monthly limit $${limitDollars}`,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
// ── All checks passed ────────────────────────────────────────────────────
|
|
368
|
+
// If we got here, the transaction satisfies every rule. Approve it.
|
|
369
|
+
const dollars = (amount / 100).toFixed(2);
|
|
370
|
+
return {
|
|
371
|
+
approved: true,
|
|
372
|
+
reason: `All rules passed — approving $${dollars} at ${auth.merchant_data?.name || 'unknown merchant'}`,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
//# sourceMappingURL=rules-engine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rules-engine.js","sourceRoot":"","sources":["../src/rules-engine.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;;;AAqPH,oCA8CC;AA+CD,sDA2FC;AA1aD,qCAAqC;AAGrC,MAAM,MAAM,GAAG,IAAA,kBAAS,EAAC,cAAc,CAAC,CAAC;AAEzC,MAAa,UAAU;IACb,IAAI,GAAc,EAAE,CAAC;IAE7B;;OAEG;IACH;QACE,IAAI,CAAC,IAAI,GAAG,EAAE,CAAC;IACjB,CAAC;IAED;;OAEG;IACH,MAAM,CAAC,QAAQ,CAAC,QAA2B;QACzC,MAAM,OAAO,GAAG,IAAI,UAAU,EAAE,CAAC;QAEjC,QAAQ,QAAQ,EAAE,CAAC;YACjB,KAAK,UAAU;gBACb,wDAAwD;gBACxD,gEAAgE;gBAChE,OAAO,OAAO;qBACX,UAAU,CAAC,KAAK,CAAC,CAAM,WAAW;qBAClC,QAAQ,CAAC,KAAK,CAAC,CAAQ,UAAU;qBACjC,SAAS,CAAC,SAAS,CAAC;qBACpB,IAAI,CAAC,UAAU,CAAC;qBAChB,WAAW,CACV,yEAAyE,CAC1E,CAAC;YAEN,KAAK,iBAAiB;gBACpB,uDAAuD;gBACvD,OAAO,OAAO;qBACX,QAAQ,CAAC,CAAC,SAAS,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;qBACxC,QAAQ,CAAC,IAAI,CAAC,CAAC,aAAa;qBAC5B,UAAU,CAAC,KAAK,CAAC,CAAC,WAAW;qBAC7B,SAAS,CAAC,SAAS,CAAC;qBACpB,IAAI,CAAC,iBAAiB,CAAC;qBACvB,WAAW,CAAC,0DAA0D,CAAC,CAAC;YAE7E,KAAK,aAAa;gBAChB,sBAAsB;gBACtB,OAAO,OAAO;qBACX,UAAU,CAAC,KAAK,CAAC,CAAC,WAAW;qBAC7B,SAAS,CAAC,SAAS,CAAC;qBACpB,IAAI,CAAC,aAAa,CAAC;qBACnB,WAAW,CAAC,iCAAiC,CAAC,CAAC;YAEpD,KAAK,gBAAgB;gBACnB,4CAA4C;gBAC5C,2EAA2E;gBAC3E,oFAAoF;gBACpF,OAAO,OAAO;qBACX,iBAAiB,CAAC,IAAI,CAAC,CAAC,OAAO;qBAC/B,gBAAgB,CAAC,IAAI,CAAC;qBACtB,SAAS,CAAC,SAAS,CAAC;qBACpB,IAAI,CAAC,gBAAgB,CAAC;qBACtB,WAAW,CAAC,8CAA8C,CAAC,CAAC;YAEjE;gBACE,MAAM,WAAW,GAAU,QAAQ,CAAC;gBACpC,OAAO,WAAW,CAAC;QACvB,CAAC;IACH,CAAC;IAED;;;OAGG;IACH,QAAQ,CAAC,UAAoB;QAC3B,IAAI,CAAC,IAAI,CAAC,iBAAiB,GAAG,UAAU,CAAC;QACzC,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;;OAGG;IACH,aAAa,CAAC,UAAoB;QAChC,IAAI,CAAC,IAAI,CAAC,iBAAiB,GAAG,UAAU,CAAC;QACzC,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;OAEG;IACH,QAAQ,CAAC,KAAa;QACpB,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;YACd,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAC;QACtD,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,iBAAiB,GAAG,KAAK,CAAC;QACpC,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;OAEG;IACH,UAAU,CAAC,KAAa;QACtB,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;YACd,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;QAC9C,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC;QAC7B,IAAI,CAAC,IAAI,CAAC,gBAAgB,GAAG,OAAO,CAAC;QACrC,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;OAEG;IACH,YAAY,CAAC,KAAa;QACxB,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;YACd,MAAM,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAC;QAChD,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC;QAC/B,IAAI,CAAC,IAAI,CAAC,gBAAgB,GAAG,SAAS,CAAC;QACvC,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;OAEG;IACH,YAAY,CAAC,KAAa;QACxB,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;YACd,MAAM,IAAI,KAAK,CAAC,6BAA6B,CAAC,CAAC;QACjD,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,iBAAiB,GAAG,KAAK,CAAC;QACpC,IAAI,CAAC,IAAI,CAAC,gBAAgB,GAAG,mBAAmB,CAAC;QACjD,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;;;;OAKG;IACH,gBAAgB,CAAC,QAAiB;QAChC,IAAI,CAAC,IAAI,CAAC,gBAAgB,GAAG,QAAQ,CAAC;QACtC,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;;;OAIG;IACH,iBAAiB,CAAC,KAAa;QAC7B,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;YACd,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAC;QACrD,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,iBAAiB,GAAG,KAAK,CAAC;QACpC,IAAI,CAAC,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC;QAClC,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;;;;;;OAOG;IACH,SAAS,CAAC,MAAqC;QAC7C,IAAI,CAAC,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC;QAC7B,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;OAEG;IACH,IAAI,CAAC,IAAY;QACf,IAAI,CAAC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACtB,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;OAEG;IACH,WAAW,CAAC,IAAY;QACtB,IAAI,CAAC,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;QAC7B,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;OAEG;IACH,KAAK;QACH,IAAI,IAAI,CAAC,IAAI,CAAC,SAAS,KAAK,OAAO,IAAI,IAAI,CAAC,IAAI,CAAC,SAAS,KAAK,OAAO,EAAE,CAAC;YACvE,MAAM,CAAC,IAAI,CACT,+FAA+F,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAC3H,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IAC1B,CAAC;IAED;;OAEG;IACH,QAAQ;QACN,MAAM,KAAK,GAAa,EAAE,CAAC;QAE3B,IAAI,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,MAAM,EAAE,CAAC;YACxC,KAAK,CAAC,IAAI,CAAC,gBAAgB,IAAI,CAAC,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACxE,CAAC;QAED,IAAI,IAAI,CAAC,IAAI,CAAC,iBAAiB,KAAK,SAAS,EAAE,CAAC;YAC9C,MAAM,OAAO,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,iBAAiB,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;YAC/D,KAAK,CAAC,IAAI,CAAC,gBAAgB,OAAO,EAAE,CAAC,CAAC;QACxC,CAAC;QAED,IAAI,IAAI,CAAC,IAAI,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;YACvC,MAAM,OAAO,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;YACxD,KAAK,CAAC,IAAI,CAAC,iBAAiB,OAAO,EAAE,CAAC,CAAC;QACzC,CAAC;QAED,IAAI,IAAI,CAAC,IAAI,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;YACzC,MAAM,OAAO,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;YAC1D,KAAK,CAAC,IAAI,CAAC,mBAAmB,OAAO,EAAE,CAAC,CAAC;QAC3C,CAAC;QAED,IAAI,IAAI,CAAC,IAAI,CAAC,gBAAgB,IAAI,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,CAAC;YAC9D,MAAM,OAAO,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,iBAAiB,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;YAC/D,KAAK,CAAC,IAAI,CAAC,uBAAuB,OAAO,EAAE,CAAC,CAAC;QAC/C,CAAC;QAED,IAAI,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;YACxB,KAAK,CAAC,IAAI,CAAC,iBAAiB,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;QACrD,CAAC;QAED,OAAO,cAAc,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;IAC3C,CAAC;CACF;AAvOD,gCAuOC;AAED;;;GAGG;AACH,SAAgB,YAAY,CAC1B,IAAe,EACf,WAGC;IAED,2BAA2B;IAC3B,IACE,IAAI,CAAC,iBAAiB,EAAE,MAAM;QAC9B,WAAW,CAAC,QAAQ;QACpB,CAAC,IAAI,CAAC,iBAAiB,CAAC,QAAQ,CAAC,WAAW,CAAC,QAAQ,CAAC,EACtD,CAAC;QACD,OAAO;YACL,OAAO,EAAE,KAAK;YACd,MAAM,EAAE,aAAa,WAAW,CAAC,QAAQ,uBAAuB;SACjE,CAAC;IACJ,CAAC;IAED,2BAA2B;IAC3B,IACE,IAAI,CAAC,iBAAiB,EAAE,MAAM;QAC9B,WAAW,CAAC,QAAQ;QACpB,IAAI,CAAC,iBAAiB,CAAC,QAAQ,CAAC,WAAW,CAAC,QAAQ,CAAC,EACrD,CAAC;QACD,OAAO;YACL,OAAO,EAAE,KAAK;YACd,MAAM,EAAE,aAAa,WAAW,CAAC,QAAQ,cAAc;SACxD,CAAC;IACJ,CAAC;IAED,4BAA4B;IAC5B,IACE,IAAI,CAAC,iBAAiB,KAAK,SAAS;QACpC,WAAW,CAAC,MAAM,GAAG,IAAI,CAAC,iBAAiB,EAC3C,CAAC;QACD,MAAM,SAAS,GAAG,CAAC,WAAW,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;QACxD,MAAM,UAAU,GAAG,CAAC,IAAI,CAAC,iBAAiB,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;QAC7D,OAAO;YACL,OAAO,EAAE,KAAK;YACd,MAAM,EAAE,uBAAuB,SAAS,0BAA0B,UAAU,EAAE;SAC/E,CAAC;IACJ,CAAC;IAED,qBAAqB;IACrB,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AAC3B,CAAC;AA2BD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,SAAgB,qBAAqB,CACnC,IAAkC,EAClC,KAAgB,EAChB,OAAqB;IAErB,2EAA2E;IAC3E,EAAE;IACF,+DAA+D;IAC/D,yEAAyE;IACzE,qDAAqD;IAErD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,QAAQ;IACpC,MAAM,QAAQ,GAAG,IAAI,CAAC,aAAa,EAAE,QAAQ,IAAI,EAAE,CAAC;IAEpD,2EAA2E;IAC3E,6EAA6E;IAC7E,0EAA0E;IAC1E,+CAA+C;IAC/C,IAAI,KAAK,CAAC,iBAAiB,IAAI,KAAK,CAAC,iBAAiB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAClE,IAAI,QAAQ,IAAI,CAAC,KAAK,CAAC,iBAAiB,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC5D,OAAO;gBACL,QAAQ,EAAE,KAAK;gBACf,MAAM,EAAE,sBAAsB,QAAQ,kCAAkC,KAAK,CAAC,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;aAC9G,CAAC;QACJ,CAAC;IACH,CAAC;IAED,2EAA2E;IAC3E,qEAAqE;IACrE,4EAA4E;IAC5E,oDAAoD;IACpD,IAAI,KAAK,CAAC,iBAAiB,IAAI,KAAK,CAAC,iBAAiB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAClE,IAAI,QAAQ,IAAI,KAAK,CAAC,iBAAiB,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC3D,OAAO;gBACL,QAAQ,EAAE,KAAK;gBACf,MAAM,EAAE,sBAAsB,QAAQ,yBAAyB;aAChE,CAAC;QACJ,CAAC;IACH,CAAC;IAED,2EAA2E;IAC3E,sEAAsE;IACtE,2CAA2C;IAC3C,IAAI,KAAK,CAAC,iBAAiB,KAAK,SAAS,IAAI,MAAM,GAAG,KAAK,CAAC,iBAAiB,EAAE,CAAC;QAC9E,MAAM,SAAS,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;QAC5C,MAAM,YAAY,GAAG,CAAC,KAAK,CAAC,iBAAiB,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;QAChE,OAAO;YACL,QAAQ,EAAE,KAAK;YACf,MAAM,EAAE,gBAAgB,SAAS,sCAAsC,YAAY,EAAE;SACtF,CAAC;IACJ,CAAC;IAED,4EAA4E;IAC5E,yEAAyE;IACzE,4EAA4E;IAC5E,0EAA0E;IAC1E,IAAI,KAAK,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;QACnC,MAAM,gBAAgB,GAAG,OAAO,CAAC,UAAU,GAAG,MAAM,CAAC;QACrD,IAAI,gBAAgB,GAAG,KAAK,CAAC,UAAU,EAAE,CAAC;YACxC,MAAM,YAAY,GAAG,CAAC,OAAO,CAAC,UAAU,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;YAC3D,MAAM,SAAS,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;YAC5C,MAAM,YAAY,GAAG,CAAC,KAAK,CAAC,UAAU,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;YACzD,OAAO;gBACL,QAAQ,EAAE,KAAK;gBACf,MAAM,EAAE,wCAAwC,YAAY,OAAO,SAAS,mBAAmB,YAAY,EAAE;aAC9G,CAAC;QACJ,CAAC;IACH,CAAC;IAED,2EAA2E;IAC3E,6CAA6C;IAC7C,IAAI,KAAK,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;QACrC,MAAM,kBAAkB,GAAG,OAAO,CAAC,YAAY,GAAG,MAAM,CAAC;QACzD,IAAI,kBAAkB,GAAG,KAAK,CAAC,YAAY,EAAE,CAAC;YAC5C,MAAM,YAAY,GAAG,CAAC,OAAO,CAAC,YAAY,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;YAC7D,MAAM,SAAS,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;YAC5C,MAAM,YAAY,GAAG,CAAC,KAAK,CAAC,YAAY,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;YAC3D,OAAO;gBACL,QAAQ,EAAE,KAAK;gBACf,MAAM,EAAE,0CAA0C,YAAY,OAAO,SAAS,qBAAqB,YAAY,EAAE;aAClH,CAAC;QACJ,CAAC;IACH,CAAC;IAED,4EAA4E;IAC5E,oEAAoE;IACpE,MAAM,OAAO,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IAC1C,OAAO;QACL,QAAQ,EAAE,IAAI;QACd,MAAM,EAAE,iCAAiC,OAAO,OAAO,IAAI,CAAC,aAAa,EAAE,IAAI,IAAI,kBAAkB,EAAE;KACxG,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rules Store
|
|
3
|
+
* ===========
|
|
4
|
+
* Persistent storage for SpendRule objects, backed by a local JSON file.
|
|
5
|
+
*
|
|
6
|
+
* ─── Why not keep rules in Stripe metadata? ─────────────────────────────────
|
|
7
|
+
* Storing rules in Stripe card metadata (as we did in Phase 1) is a security
|
|
8
|
+
* problem: anyone with Stripe dashboard access can edit the rules directly,
|
|
9
|
+
* bypassing any access controls we might add. By moving rules to our own store,
|
|
10
|
+
* we control who can read or modify them — and Stripe metadata only holds a
|
|
11
|
+
* reference ID (opencard_rule_id), not the rules themselves.
|
|
12
|
+
*
|
|
13
|
+
* ─── Why a JSON file? ────────────────────────────────────────────────────────
|
|
14
|
+
* Phase 1 is all about getting something working and safe without adding
|
|
15
|
+
* operational complexity. A JSON file:
|
|
16
|
+
* - Survives process restarts (unlike in-memory)
|
|
17
|
+
* - Requires no database setup or migration
|
|
18
|
+
* - Is easy to inspect, back up, and version-control
|
|
19
|
+
* - Is fast enough for Phase 1 workloads (hundreds of rules, not millions)
|
|
20
|
+
*
|
|
21
|
+
* Phase 2 will replace this with a proper database when we need multi-instance
|
|
22
|
+
* support or higher write throughput.
|
|
23
|
+
*
|
|
24
|
+
* ─── File format ─────────────────────────────────────────────────────────────
|
|
25
|
+
* The file is a JSON object: { [ruleId]: SpendRule }
|
|
26
|
+
* Example:
|
|
27
|
+
* {
|
|
28
|
+
* "rule_01HX9K2Z3A4B5C6D7E8F9G0H1J": { "dailyLimit": 10000, "onFailure": "decline" },
|
|
29
|
+
* "rule_01HX9K2Z3A4B5C6D7E8F9G0H1K": { "monthlyLimit": 50000 }
|
|
30
|
+
* }
|
|
31
|
+
*
|
|
32
|
+
* ─── ID format ───────────────────────────────────────────────────────────────
|
|
33
|
+
* IDs use the format `rule_<timestamp_hex><random_hex>` — lexicographically
|
|
34
|
+
* sortable (newer rules sort after older ones) and globally unique without
|
|
35
|
+
* a central counter. We implement this without external dependencies using
|
|
36
|
+
* Node.js's built-in crypto module.
|
|
37
|
+
*
|
|
38
|
+
* ─── Thread safety ───────────────────────────────────────────────────────────
|
|
39
|
+
* We serialize all writes through a simple async queue (each write awaits the
|
|
40
|
+
* previous one). This isn't needed for single-threaded Node.js, but it makes
|
|
41
|
+
* the behavior explicit and protects against future async-write bugs.
|
|
42
|
+
*/
|
|
43
|
+
import { SpendRule } from './types';
|
|
44
|
+
export declare class RulesStore {
|
|
45
|
+
/**
|
|
46
|
+
* Path to the JSON file where rules are stored.
|
|
47
|
+
* Configurable via OPENCARD_RULES_PATH env var.
|
|
48
|
+
* Defaults to ./opencard-rules.json relative to the process working directory.
|
|
49
|
+
*/
|
|
50
|
+
private readonly filePath;
|
|
51
|
+
/**
|
|
52
|
+
* Internal write queue: we chain writes so concurrent callers don't
|
|
53
|
+
* corrupt the file by reading stale contents and overwriting each other.
|
|
54
|
+
*/
|
|
55
|
+
private writeQueue;
|
|
56
|
+
constructor(filePath?: string);
|
|
57
|
+
/**
|
|
58
|
+
* Generates a unique, sortable rule ID.
|
|
59
|
+
*
|
|
60
|
+
* Format: `rule_<timestamp_ms_hex><random_8_hex_chars>`
|
|
61
|
+
* Example: `rule_018f1a2b3c4d5e6f7a8b9c0d`
|
|
62
|
+
*
|
|
63
|
+
* - The timestamp prefix makes IDs sort chronologically (newest last in
|
|
64
|
+
* a lexicographic sort), which is useful when listing rules.
|
|
65
|
+
* - The random suffix prevents collisions even if two rules are created
|
|
66
|
+
* in the same millisecond.
|
|
67
|
+
* - No external library needed — uses Node.js built-in crypto.
|
|
68
|
+
*/
|
|
69
|
+
private generateId;
|
|
70
|
+
/**
|
|
71
|
+
* Reads the rules file from disk and parses it.
|
|
72
|
+
*
|
|
73
|
+
* If the file doesn't exist yet, returns an empty object (not an error) —
|
|
74
|
+
* the file will be created on the first write.
|
|
75
|
+
*
|
|
76
|
+
* If the file exists but contains invalid JSON, throws an error. This
|
|
77
|
+
* shouldn't happen in normal operation (we always write valid JSON), but
|
|
78
|
+
* could happen if the file was manually edited.
|
|
79
|
+
*/
|
|
80
|
+
private readFile;
|
|
81
|
+
/**
|
|
82
|
+
* Writes the entire rules object to disk, atomically.
|
|
83
|
+
*
|
|
84
|
+
* We write to a temp file first, then rename it into place. This avoids
|
|
85
|
+
* leaving a half-written file if the process crashes mid-write.
|
|
86
|
+
*
|
|
87
|
+
* All writes are serialized through the writeQueue to prevent concurrent
|
|
88
|
+
* writes from racing each other.
|
|
89
|
+
*/
|
|
90
|
+
private writeFile;
|
|
91
|
+
/**
|
|
92
|
+
* Creates a new rule in the store.
|
|
93
|
+
*
|
|
94
|
+
* Validates the rule before writing — throws if validation fails.
|
|
95
|
+
* This is the right place to reject bad rules so they never make it
|
|
96
|
+
* into the store and can never affect a live transaction.
|
|
97
|
+
*
|
|
98
|
+
* @param rule - The spend rule to store
|
|
99
|
+
* @returns The generated rule ID (e.g. "rule_018f1a2b3c4d5e6f7a8b9c0d")
|
|
100
|
+
*
|
|
101
|
+
* @throws Error if the rule fails validation
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* const ruleId = await rulesStore.createRule({
|
|
105
|
+
* dailyLimit: 10000, // $100/day
|
|
106
|
+
* onFailure: 'decline',
|
|
107
|
+
* });
|
|
108
|
+
* // ruleId → "rule_018f1a2b3c4d5e6f7a8b9c0d"
|
|
109
|
+
*/
|
|
110
|
+
createRule(rule: SpendRule): Promise<string>;
|
|
111
|
+
/**
|
|
112
|
+
* Looks up a rule by its ID.
|
|
113
|
+
*
|
|
114
|
+
* Returns null (not an error) if the rule doesn't exist. This is important
|
|
115
|
+
* in the webhook path: if a card references a rule ID that's been deleted,
|
|
116
|
+
* the webhook handler should fall back to default-deny, not crash.
|
|
117
|
+
*
|
|
118
|
+
* @param ruleId - The rule ID to look up
|
|
119
|
+
* @returns The SpendRule, or null if not found
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* const rule = await rulesStore.getRule('rule_018f1a2b...');
|
|
123
|
+
* if (rule === null) {
|
|
124
|
+
* // rule was deleted or ID is wrong — use default-deny
|
|
125
|
+
* }
|
|
126
|
+
*/
|
|
127
|
+
getRule(ruleId: string): Promise<SpendRule | null>;
|
|
128
|
+
/**
|
|
129
|
+
* Updates an existing rule.
|
|
130
|
+
*
|
|
131
|
+
* Validates the new rule before writing. The update replaces the entire
|
|
132
|
+
* rule — it's not a partial merge. Pass the full updated rule object.
|
|
133
|
+
*
|
|
134
|
+
* @throws Error if the rule doesn't exist (can't update what isn't there)
|
|
135
|
+
* @throws Error if the new rule fails validation
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* await rulesStore.updateRule('rule_018f1a2b...', {
|
|
139
|
+
* dailyLimit: 20000, // raised from $100 to $200
|
|
140
|
+
* onFailure: 'decline',
|
|
141
|
+
* });
|
|
142
|
+
*/
|
|
143
|
+
updateRule(ruleId: string, rule: SpendRule): Promise<void>;
|
|
144
|
+
/**
|
|
145
|
+
* Deletes a rule from the store.
|
|
146
|
+
*
|
|
147
|
+
* Note: this does NOT update any cards that reference this rule.
|
|
148
|
+
* Cards referencing a deleted rule ID will fall back to default-deny
|
|
149
|
+
* in the webhook handler (by design — it's better to be safe).
|
|
150
|
+
*
|
|
151
|
+
* If the rule doesn't exist, this is a no-op (not an error).
|
|
152
|
+
*
|
|
153
|
+
* @param ruleId - The rule ID to delete
|
|
154
|
+
*/
|
|
155
|
+
deleteRule(ruleId: string): Promise<void>;
|
|
156
|
+
/**
|
|
157
|
+
* Lists all rules in the store.
|
|
158
|
+
*
|
|
159
|
+
* Returns them sorted by ID (which is chronological since IDs are
|
|
160
|
+
* timestamp-prefixed). Newest rules appear last.
|
|
161
|
+
*
|
|
162
|
+
* @returns Array of { id, rule } objects, sorted by creation order
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* const rules = await rulesStore.listRules();
|
|
166
|
+
* for (const { id, rule } of rules) {
|
|
167
|
+
* console.log(`${id}: ${rule.name ?? 'unnamed'}`);
|
|
168
|
+
* }
|
|
169
|
+
*/
|
|
170
|
+
listRules(): Promise<{
|
|
171
|
+
id: string;
|
|
172
|
+
rule: SpendRule;
|
|
173
|
+
}[]>;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Module-level singleton instance of RulesStore.
|
|
177
|
+
*
|
|
178
|
+
* Export and use this everywhere, just like the `tracker` singleton.
|
|
179
|
+
* This ensures all parts of the system share the same file path and
|
|
180
|
+
* write queue — critical for avoiding concurrent write corruption.
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* import { rulesStore } from '@opencard-dev/core';
|
|
184
|
+
* const ruleId = await rulesStore.createRule({ dailyLimit: 10000 });
|
|
185
|
+
*/
|
|
186
|
+
export declare const rulesStore: RulesStore;
|
|
187
|
+
//# sourceMappingURL=rules-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rules-store.d.ts","sourceRoot":"","sources":["../src/rules-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAKH,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAcpC,qBAAa,UAAU;IACrB;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAElC;;;OAGG;IACH,OAAO,CAAC,UAAU,CAAoC;gBAE1C,QAAQ,CAAC,EAAE,MAAM;IAU7B;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,UAAU;IAQlB;;;;;;;;;OASG;YACW,QAAQ;IAgBtB;;;;;;;;OAQG;YACW,SAAS;IA+BvB;;;;;;;;;;;;;;;;;;OAkBG;IACG,UAAU,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC;IA0BlD;;;;;;;;;;;;;;;OAeG;IACG,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IAKxD;;;;;;;;;;;;;;OAcG;IACG,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAuBhE;;;;;;;;;;OAUG;IACG,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAe/C;;;;;;;;;;;;;OAaG;IACG,SAAS,IAAI,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,SAAS,CAAA;KAAE,EAAE,CAAC;CAO9D;AAID;;;;;;;;;;GAUG;AACH,eAAO,MAAM,UAAU,YAAmB,CAAC"}
|