@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,291 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Rules Store
|
|
4
|
+
* ===========
|
|
5
|
+
* Persistent storage for SpendRule objects, backed by a local JSON file.
|
|
6
|
+
*
|
|
7
|
+
* ─── Why not keep rules in Stripe metadata? ─────────────────────────────────
|
|
8
|
+
* Storing rules in Stripe card metadata (as we did in Phase 1) is a security
|
|
9
|
+
* problem: anyone with Stripe dashboard access can edit the rules directly,
|
|
10
|
+
* bypassing any access controls we might add. By moving rules to our own store,
|
|
11
|
+
* we control who can read or modify them — and Stripe metadata only holds a
|
|
12
|
+
* reference ID (opencard_rule_id), not the rules themselves.
|
|
13
|
+
*
|
|
14
|
+
* ─── Why a JSON file? ────────────────────────────────────────────────────────
|
|
15
|
+
* Phase 1 is all about getting something working and safe without adding
|
|
16
|
+
* operational complexity. A JSON file:
|
|
17
|
+
* - Survives process restarts (unlike in-memory)
|
|
18
|
+
* - Requires no database setup or migration
|
|
19
|
+
* - Is easy to inspect, back up, and version-control
|
|
20
|
+
* - Is fast enough for Phase 1 workloads (hundreds of rules, not millions)
|
|
21
|
+
*
|
|
22
|
+
* Phase 2 will replace this with a proper database when we need multi-instance
|
|
23
|
+
* support or higher write throughput.
|
|
24
|
+
*
|
|
25
|
+
* ─── File format ─────────────────────────────────────────────────────────────
|
|
26
|
+
* The file is a JSON object: { [ruleId]: SpendRule }
|
|
27
|
+
* Example:
|
|
28
|
+
* {
|
|
29
|
+
* "rule_01HX9K2Z3A4B5C6D7E8F9G0H1J": { "dailyLimit": 10000, "onFailure": "decline" },
|
|
30
|
+
* "rule_01HX9K2Z3A4B5C6D7E8F9G0H1K": { "monthlyLimit": 50000 }
|
|
31
|
+
* }
|
|
32
|
+
*
|
|
33
|
+
* ─── ID format ───────────────────────────────────────────────────────────────
|
|
34
|
+
* IDs use the format `rule_<timestamp_hex><random_hex>` — lexicographically
|
|
35
|
+
* sortable (newer rules sort after older ones) and globally unique without
|
|
36
|
+
* a central counter. We implement this without external dependencies using
|
|
37
|
+
* Node.js's built-in crypto module.
|
|
38
|
+
*
|
|
39
|
+
* ─── Thread safety ───────────────────────────────────────────────────────────
|
|
40
|
+
* We serialize all writes through a simple async queue (each write awaits the
|
|
41
|
+
* previous one). This isn't needed for single-threaded Node.js, but it makes
|
|
42
|
+
* the behavior explicit and protects against future async-write bugs.
|
|
43
|
+
*/
|
|
44
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
45
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
46
|
+
};
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
exports.rulesStore = exports.RulesStore = void 0;
|
|
49
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
50
|
+
const path_1 = __importDefault(require("path"));
|
|
51
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
52
|
+
const rules_validation_1 = require("./rules-validation");
|
|
53
|
+
// ─── RulesStore class ─────────────────────────────────────────────────────────
|
|
54
|
+
class RulesStore {
|
|
55
|
+
/**
|
|
56
|
+
* Path to the JSON file where rules are stored.
|
|
57
|
+
* Configurable via OPENCARD_RULES_PATH env var.
|
|
58
|
+
* Defaults to ./opencard-rules.json relative to the process working directory.
|
|
59
|
+
*/
|
|
60
|
+
filePath;
|
|
61
|
+
/**
|
|
62
|
+
* Internal write queue: we chain writes so concurrent callers don't
|
|
63
|
+
* corrupt the file by reading stale contents and overwriting each other.
|
|
64
|
+
*/
|
|
65
|
+
writeQueue = Promise.resolve();
|
|
66
|
+
constructor(filePath) {
|
|
67
|
+
// Prefer explicit argument, then env var, then default.
|
|
68
|
+
// path.resolve() converts relative paths to absolute, using process.cwd().
|
|
69
|
+
this.filePath = filePath
|
|
70
|
+
|| process.env.OPENCARD_RULES_PATH
|
|
71
|
+
|| path_1.default.resolve(process.cwd(), 'opencard-rules.json');
|
|
72
|
+
}
|
|
73
|
+
// ─── ID generation ─────────────────────────────────────────────────────────
|
|
74
|
+
/**
|
|
75
|
+
* Generates a unique, sortable rule ID.
|
|
76
|
+
*
|
|
77
|
+
* Format: `rule_<timestamp_ms_hex><random_8_hex_chars>`
|
|
78
|
+
* Example: `rule_018f1a2b3c4d5e6f7a8b9c0d`
|
|
79
|
+
*
|
|
80
|
+
* - The timestamp prefix makes IDs sort chronologically (newest last in
|
|
81
|
+
* a lexicographic sort), which is useful when listing rules.
|
|
82
|
+
* - The random suffix prevents collisions even if two rules are created
|
|
83
|
+
* in the same millisecond.
|
|
84
|
+
* - No external library needed — uses Node.js built-in crypto.
|
|
85
|
+
*/
|
|
86
|
+
generateId() {
|
|
87
|
+
const timestampHex = Date.now().toString(16).padStart(12, '0');
|
|
88
|
+
const randomHex = crypto_1.default.randomBytes(6).toString('hex');
|
|
89
|
+
return `rule_${timestampHex}${randomHex}`;
|
|
90
|
+
}
|
|
91
|
+
// ─── File I/O ───────────────────────────────────────────────────────────────
|
|
92
|
+
/**
|
|
93
|
+
* Reads the rules file from disk and parses it.
|
|
94
|
+
*
|
|
95
|
+
* If the file doesn't exist yet, returns an empty object (not an error) —
|
|
96
|
+
* the file will be created on the first write.
|
|
97
|
+
*
|
|
98
|
+
* If the file exists but contains invalid JSON, throws an error. This
|
|
99
|
+
* shouldn't happen in normal operation (we always write valid JSON), but
|
|
100
|
+
* could happen if the file was manually edited.
|
|
101
|
+
*/
|
|
102
|
+
async readFile() {
|
|
103
|
+
try {
|
|
104
|
+
const raw = await promises_1.default.readFile(this.filePath, 'utf8');
|
|
105
|
+
return JSON.parse(raw);
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
// ENOENT = file not found — that's OK, just means no rules stored yet
|
|
109
|
+
if (err.code === 'ENOENT') {
|
|
110
|
+
return {};
|
|
111
|
+
}
|
|
112
|
+
// Any other error (permission denied, malformed JSON, etc.) is a real problem
|
|
113
|
+
throw new Error(`[RulesStore] Failed to read rules file at ${this.filePath}: ${String(err)}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Writes the entire rules object to disk, atomically.
|
|
118
|
+
*
|
|
119
|
+
* We write to a temp file first, then rename it into place. This avoids
|
|
120
|
+
* leaving a half-written file if the process crashes mid-write.
|
|
121
|
+
*
|
|
122
|
+
* All writes are serialized through the writeQueue to prevent concurrent
|
|
123
|
+
* writes from racing each other.
|
|
124
|
+
*/
|
|
125
|
+
async writeFile(contents) {
|
|
126
|
+
// Chain this write onto the end of the queue
|
|
127
|
+
this.writeQueue = this.writeQueue.then(async () => {
|
|
128
|
+
const tmpPath = `${this.filePath}.tmp`;
|
|
129
|
+
const json = JSON.stringify(contents, null, 2); // pretty-print for human readability
|
|
130
|
+
try {
|
|
131
|
+
// Ensure the parent directory exists (in case OPENCARD_RULES_PATH points
|
|
132
|
+
// to a dir that hasn't been created yet)
|
|
133
|
+
await promises_1.default.mkdir(path_1.default.dirname(this.filePath), { recursive: true });
|
|
134
|
+
// Write to temp file first
|
|
135
|
+
await promises_1.default.writeFile(tmpPath, json, 'utf8');
|
|
136
|
+
// Rename into place (atomic on POSIX systems when same filesystem)
|
|
137
|
+
await promises_1.default.rename(tmpPath, this.filePath);
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
// Clean up temp file if we can (ignore errors from cleanup itself)
|
|
141
|
+
await promises_1.default.unlink(tmpPath).catch(() => { });
|
|
142
|
+
throw new Error(`[RulesStore] Failed to write rules file at ${this.filePath}: ${String(err)}`);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
// Wait for this specific write to complete before returning
|
|
146
|
+
return this.writeQueue;
|
|
147
|
+
}
|
|
148
|
+
// ─── CRUD operations ────────────────────────────────────────────────────────
|
|
149
|
+
/**
|
|
150
|
+
* Creates a new rule in the store.
|
|
151
|
+
*
|
|
152
|
+
* Validates the rule before writing — throws if validation fails.
|
|
153
|
+
* This is the right place to reject bad rules so they never make it
|
|
154
|
+
* into the store and can never affect a live transaction.
|
|
155
|
+
*
|
|
156
|
+
* @param rule - The spend rule to store
|
|
157
|
+
* @returns The generated rule ID (e.g. "rule_018f1a2b3c4d5e6f7a8b9c0d")
|
|
158
|
+
*
|
|
159
|
+
* @throws Error if the rule fails validation
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* const ruleId = await rulesStore.createRule({
|
|
163
|
+
* dailyLimit: 10000, // $100/day
|
|
164
|
+
* onFailure: 'decline',
|
|
165
|
+
* });
|
|
166
|
+
* // ruleId → "rule_018f1a2b3c4d5e6f7a8b9c0d"
|
|
167
|
+
*/
|
|
168
|
+
async createRule(rule) {
|
|
169
|
+
// Validate before writing — reject invalid rules with clear error messages
|
|
170
|
+
const validation = (0, rules_validation_1.validateRule)(rule);
|
|
171
|
+
if (!validation.valid) {
|
|
172
|
+
throw new Error(`[RulesStore] Invalid rule: ${validation.errors.join('; ')}`);
|
|
173
|
+
}
|
|
174
|
+
const ruleId = this.generateId();
|
|
175
|
+
const contents = await this.readFile();
|
|
176
|
+
// Sanity check: generated ID should never collide, but just in case
|
|
177
|
+
if (contents[ruleId]) {
|
|
178
|
+
throw new Error(`[RulesStore] ID collision on ${ruleId} — this should never happen`);
|
|
179
|
+
}
|
|
180
|
+
contents[ruleId] = rule;
|
|
181
|
+
await this.writeFile(contents);
|
|
182
|
+
console.log(`[RulesStore] Created rule ${ruleId}`);
|
|
183
|
+
return ruleId;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Looks up a rule by its ID.
|
|
187
|
+
*
|
|
188
|
+
* Returns null (not an error) if the rule doesn't exist. This is important
|
|
189
|
+
* in the webhook path: if a card references a rule ID that's been deleted,
|
|
190
|
+
* the webhook handler should fall back to default-deny, not crash.
|
|
191
|
+
*
|
|
192
|
+
* @param ruleId - The rule ID to look up
|
|
193
|
+
* @returns The SpendRule, or null if not found
|
|
194
|
+
*
|
|
195
|
+
* @example
|
|
196
|
+
* const rule = await rulesStore.getRule('rule_018f1a2b...');
|
|
197
|
+
* if (rule === null) {
|
|
198
|
+
* // rule was deleted or ID is wrong — use default-deny
|
|
199
|
+
* }
|
|
200
|
+
*/
|
|
201
|
+
async getRule(ruleId) {
|
|
202
|
+
const contents = await this.readFile();
|
|
203
|
+
return contents[ruleId] ?? null;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Updates an existing rule.
|
|
207
|
+
*
|
|
208
|
+
* Validates the new rule before writing. The update replaces the entire
|
|
209
|
+
* rule — it's not a partial merge. Pass the full updated rule object.
|
|
210
|
+
*
|
|
211
|
+
* @throws Error if the rule doesn't exist (can't update what isn't there)
|
|
212
|
+
* @throws Error if the new rule fails validation
|
|
213
|
+
*
|
|
214
|
+
* @example
|
|
215
|
+
* await rulesStore.updateRule('rule_018f1a2b...', {
|
|
216
|
+
* dailyLimit: 20000, // raised from $100 to $200
|
|
217
|
+
* onFailure: 'decline',
|
|
218
|
+
* });
|
|
219
|
+
*/
|
|
220
|
+
async updateRule(ruleId, rule) {
|
|
221
|
+
// Validate before writing
|
|
222
|
+
const validation = (0, rules_validation_1.validateRule)(rule);
|
|
223
|
+
if (!validation.valid) {
|
|
224
|
+
throw new Error(`[RulesStore] Invalid rule: ${validation.errors.join('; ')}`);
|
|
225
|
+
}
|
|
226
|
+
const contents = await this.readFile();
|
|
227
|
+
if (!contents[ruleId]) {
|
|
228
|
+
throw new Error(`[RulesStore] Rule ${ruleId} not found — cannot update a rule that doesn't exist`);
|
|
229
|
+
}
|
|
230
|
+
contents[ruleId] = rule;
|
|
231
|
+
await this.writeFile(contents);
|
|
232
|
+
console.log(`[RulesStore] Updated rule ${ruleId}`);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Deletes a rule from the store.
|
|
236
|
+
*
|
|
237
|
+
* Note: this does NOT update any cards that reference this rule.
|
|
238
|
+
* Cards referencing a deleted rule ID will fall back to default-deny
|
|
239
|
+
* in the webhook handler (by design — it's better to be safe).
|
|
240
|
+
*
|
|
241
|
+
* If the rule doesn't exist, this is a no-op (not an error).
|
|
242
|
+
*
|
|
243
|
+
* @param ruleId - The rule ID to delete
|
|
244
|
+
*/
|
|
245
|
+
async deleteRule(ruleId) {
|
|
246
|
+
const contents = await this.readFile();
|
|
247
|
+
if (!contents[ruleId]) {
|
|
248
|
+
// No-op — deleting a non-existent rule is fine
|
|
249
|
+
console.warn(`[RulesStore] deleteRule called for non-existent rule ${ruleId} — no-op`);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
delete contents[ruleId];
|
|
253
|
+
await this.writeFile(contents);
|
|
254
|
+
console.log(`[RulesStore] Deleted rule ${ruleId}`);
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Lists all rules in the store.
|
|
258
|
+
*
|
|
259
|
+
* Returns them sorted by ID (which is chronological since IDs are
|
|
260
|
+
* timestamp-prefixed). Newest rules appear last.
|
|
261
|
+
*
|
|
262
|
+
* @returns Array of { id, rule } objects, sorted by creation order
|
|
263
|
+
*
|
|
264
|
+
* @example
|
|
265
|
+
* const rules = await rulesStore.listRules();
|
|
266
|
+
* for (const { id, rule } of rules) {
|
|
267
|
+
* console.log(`${id}: ${rule.name ?? 'unnamed'}`);
|
|
268
|
+
* }
|
|
269
|
+
*/
|
|
270
|
+
async listRules() {
|
|
271
|
+
const contents = await this.readFile();
|
|
272
|
+
return Object.entries(contents)
|
|
273
|
+
.sort(([a], [b]) => a.localeCompare(b)) // sort by ID = sort by creation time
|
|
274
|
+
.map(([id, rule]) => ({ id, rule }));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
exports.RulesStore = RulesStore;
|
|
278
|
+
// ─── Singleton ────────────────────────────────────────────────────────────────
|
|
279
|
+
/**
|
|
280
|
+
* Module-level singleton instance of RulesStore.
|
|
281
|
+
*
|
|
282
|
+
* Export and use this everywhere, just like the `tracker` singleton.
|
|
283
|
+
* This ensures all parts of the system share the same file path and
|
|
284
|
+
* write queue — critical for avoiding concurrent write corruption.
|
|
285
|
+
*
|
|
286
|
+
* @example
|
|
287
|
+
* import { rulesStore } from '@opencard-dev/core';
|
|
288
|
+
* const ruleId = await rulesStore.createRule({ dailyLimit: 10000 });
|
|
289
|
+
*/
|
|
290
|
+
exports.rulesStore = new RulesStore();
|
|
291
|
+
//# sourceMappingURL=rules-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rules-store.js","sourceRoot":"","sources":["../src/rules-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule Validation
|
|
3
|
+
* ===============
|
|
4
|
+
* Validates SpendRule objects before they're written to the rules store.
|
|
5
|
+
*
|
|
6
|
+
* ─── Why validate at write time? ────────────────────────────────────────────
|
|
7
|
+
* We check rules when they're created or updated — not at authorization time.
|
|
8
|
+
* This means:
|
|
9
|
+
* 1. If you write a bad rule, you find out immediately (not at 2am when a
|
|
10
|
+
* card transaction fails in a confusing way).
|
|
11
|
+
* 2. The authorization path (hot path, ~2 second window) stays fast and simple.
|
|
12
|
+
* 3. We can give detailed error messages to whoever is creating the rule,
|
|
13
|
+
* not bury them in webhook logs.
|
|
14
|
+
*
|
|
15
|
+
* ─── What we validate ────────────────────────────────────────────────────────
|
|
16
|
+
* - Numeric limits must be positive integers (cents, so negative = nonsense)
|
|
17
|
+
* - Category arrays must actually be arrays of strings
|
|
18
|
+
* - onFailure must be one of the known action strings
|
|
19
|
+
*
|
|
20
|
+
* ─── What we don't validate ──────────────────────────────────────────────────
|
|
21
|
+
* We don't validate category strings themselves (i.e., we don't check if
|
|
22
|
+
* "grocery" is a real Stripe MCC category). That would require a lookup table
|
|
23
|
+
* and would break if Stripe adds new categories. Better to allow unknown
|
|
24
|
+
* categories and just have them never match.
|
|
25
|
+
*/
|
|
26
|
+
import { SpendRule } from './types';
|
|
27
|
+
/**
|
|
28
|
+
* Result of a rule validation check.
|
|
29
|
+
* `valid: true` means the rule is safe to store.
|
|
30
|
+
* `valid: false` means there are errors — check the `errors` array.
|
|
31
|
+
*/
|
|
32
|
+
export interface ValidationResult {
|
|
33
|
+
valid: boolean;
|
|
34
|
+
errors: string[];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Validates a SpendRule before it's written to the rules store.
|
|
38
|
+
*
|
|
39
|
+
* Collects ALL errors at once (instead of failing on the first one),
|
|
40
|
+
* so callers get a complete picture of what's wrong in a single call.
|
|
41
|
+
*
|
|
42
|
+
* @param rule - The rule to validate
|
|
43
|
+
* @returns { valid, errors } — if valid is false, errors has at least one entry
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* const result = validateRule({ dailyLimit: -500, onFailure: 'explode' });
|
|
47
|
+
* // result.valid === false
|
|
48
|
+
* // result.errors === [
|
|
49
|
+
* // "dailyLimit must be a positive number (in cents), got -500",
|
|
50
|
+
* // "onFailure must be one of: decline, alert, pause — got 'explode'"
|
|
51
|
+
* // ]
|
|
52
|
+
*/
|
|
53
|
+
export declare function validateRule(rule: SpendRule): ValidationResult;
|
|
54
|
+
//# sourceMappingURL=rules-validation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rules-validation.d.ts","sourceRoot":"","sources":["../src/rules-validation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEpC;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AA+BD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,GAAG,gBAAgB,CAgD9D"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Rule Validation
|
|
4
|
+
* ===============
|
|
5
|
+
* Validates SpendRule objects before they're written to the rules store.
|
|
6
|
+
*
|
|
7
|
+
* ─── Why validate at write time? ────────────────────────────────────────────
|
|
8
|
+
* We check rules when they're created or updated — not at authorization time.
|
|
9
|
+
* This means:
|
|
10
|
+
* 1. If you write a bad rule, you find out immediately (not at 2am when a
|
|
11
|
+
* card transaction fails in a confusing way).
|
|
12
|
+
* 2. The authorization path (hot path, ~2 second window) stays fast and simple.
|
|
13
|
+
* 3. We can give detailed error messages to whoever is creating the rule,
|
|
14
|
+
* not bury them in webhook logs.
|
|
15
|
+
*
|
|
16
|
+
* ─── What we validate ────────────────────────────────────────────────────────
|
|
17
|
+
* - Numeric limits must be positive integers (cents, so negative = nonsense)
|
|
18
|
+
* - Category arrays must actually be arrays of strings
|
|
19
|
+
* - onFailure must be one of the known action strings
|
|
20
|
+
*
|
|
21
|
+
* ─── What we don't validate ──────────────────────────────────────────────────
|
|
22
|
+
* We don't validate category strings themselves (i.e., we don't check if
|
|
23
|
+
* "grocery" is a real Stripe MCC category). That would require a lookup table
|
|
24
|
+
* and would break if Stripe adds new categories. Better to allow unknown
|
|
25
|
+
* categories and just have them never match.
|
|
26
|
+
*/
|
|
27
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
+
exports.validateRule = validateRule;
|
|
29
|
+
/**
|
|
30
|
+
* The three valid values for onFailure.
|
|
31
|
+
* - 'decline': reject the transaction immediately
|
|
32
|
+
* - 'alert': approve but send an alert (Phase 2 feature)
|
|
33
|
+
* - 'pause': pause the card after violation (blocks future transactions)
|
|
34
|
+
*/
|
|
35
|
+
const VALID_ON_FAILURE_VALUES = ['decline', 'alert', 'pause'];
|
|
36
|
+
/**
|
|
37
|
+
* The numeric fields in SpendRule that must be positive numbers if present.
|
|
38
|
+
* All are in cents (USD). Zero doesn't make sense as a limit — it would mean
|
|
39
|
+
* "allow spending of $0" which is effectively a declined card.
|
|
40
|
+
*/
|
|
41
|
+
const NUMERIC_LIMIT_FIELDS = [
|
|
42
|
+
'dailyLimit',
|
|
43
|
+
'monthlyLimit',
|
|
44
|
+
'maxPerTransaction',
|
|
45
|
+
'approvalThreshold',
|
|
46
|
+
];
|
|
47
|
+
/**
|
|
48
|
+
* The array fields in SpendRule that must be string arrays if present.
|
|
49
|
+
* These hold merchant category codes (MCCs) like "grocery", "transit", etc.
|
|
50
|
+
*/
|
|
51
|
+
const CATEGORY_ARRAY_FIELDS = [
|
|
52
|
+
'allowedCategories',
|
|
53
|
+
'blockedCategories',
|
|
54
|
+
];
|
|
55
|
+
/**
|
|
56
|
+
* Validates a SpendRule before it's written to the rules store.
|
|
57
|
+
*
|
|
58
|
+
* Collects ALL errors at once (instead of failing on the first one),
|
|
59
|
+
* so callers get a complete picture of what's wrong in a single call.
|
|
60
|
+
*
|
|
61
|
+
* @param rule - The rule to validate
|
|
62
|
+
* @returns { valid, errors } — if valid is false, errors has at least one entry
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* const result = validateRule({ dailyLimit: -500, onFailure: 'explode' });
|
|
66
|
+
* // result.valid === false
|
|
67
|
+
* // result.errors === [
|
|
68
|
+
* // "dailyLimit must be a positive number (in cents), got -500",
|
|
69
|
+
* // "onFailure must be one of: decline, alert, pause — got 'explode'"
|
|
70
|
+
* // ]
|
|
71
|
+
*/
|
|
72
|
+
function validateRule(rule) {
|
|
73
|
+
const errors = [];
|
|
74
|
+
// ── Check numeric limit fields ──────────────────────────────────────────────
|
|
75
|
+
for (const field of NUMERIC_LIMIT_FIELDS) {
|
|
76
|
+
const value = rule[field];
|
|
77
|
+
if (value !== undefined) {
|
|
78
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
|
|
79
|
+
errors.push(`${field} must be a positive number (in cents), got ${JSON.stringify(value)}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// ── Check category array fields ─────────────────────────────────────────────
|
|
84
|
+
for (const field of CATEGORY_ARRAY_FIELDS) {
|
|
85
|
+
const value = rule[field];
|
|
86
|
+
if (value !== undefined) {
|
|
87
|
+
if (!Array.isArray(value)) {
|
|
88
|
+
errors.push(`${field} must be an array of strings, got ${typeof value}`);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
// All elements must be strings
|
|
92
|
+
const nonStrings = value.filter((v) => typeof v !== 'string');
|
|
93
|
+
if (nonStrings.length > 0) {
|
|
94
|
+
errors.push(`${field} must contain only strings — found ${nonStrings.length} non-string value(s): ${JSON.stringify(nonStrings)}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// ── Check onFailure ─────────────────────────────────────────────────────────
|
|
100
|
+
if (rule.onFailure !== undefined) {
|
|
101
|
+
if (!VALID_ON_FAILURE_VALUES.includes(rule.onFailure)) {
|
|
102
|
+
errors.push(`onFailure must be one of: ${VALID_ON_FAILURE_VALUES.join(', ')} — got '${rule.onFailure}'`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
valid: errors.length === 0,
|
|
107
|
+
errors,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=rules-validation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rules-validation.js","sourceRoot":"","sources":["../src/rules-validation.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;;AA4DH,oCAgDC;AA9FD;;;;;GAKG;AACH,MAAM,uBAAuB,GAAG,CAAC,SAAS,EAAE,OAAO,EAAE,OAAO,CAAU,CAAC;AAEvE;;;;GAIG;AACH,MAAM,oBAAoB,GAAG;IAC3B,YAAY;IACZ,cAAc;IACd,mBAAmB;IACnB,mBAAmB;CACX,CAAC;AAEX;;;GAGG;AACH,MAAM,qBAAqB,GAAG;IAC5B,mBAAmB;IACnB,mBAAmB;CACX,CAAC;AAEX;;;;;;;;;;;;;;;;GAgBG;AACH,SAAgB,YAAY,CAAC,IAAe;IAC1C,MAAM,MAAM,GAAa,EAAE,CAAC;IAE5B,+EAA+E;IAC/E,KAAK,MAAM,KAAK,IAAI,oBAAoB,EAAE,CAAC;QACzC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC;QAC1B,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,EAAE,CAAC;gBACvE,MAAM,CAAC,IAAI,CACT,GAAG,KAAK,8CAA8C,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAC9E,CAAC;YACJ,CAAC;QACH,CAAC;IACH,CAAC;IAED,+EAA+E;IAC/E,KAAK,MAAM,KAAK,IAAI,qBAAqB,EAAE,CAAC;QAC1C,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC;QAC1B,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC1B,MAAM,CAAC,IAAI,CACT,GAAG,KAAK,qCAAqC,OAAO,KAAK,EAAE,CAC5D,CAAC;YACJ,CAAC;iBAAM,CAAC;gBACN,+BAA+B;gBAC/B,MAAM,UAAU,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC;gBAC9D,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oBAC1B,MAAM,CAAC,IAAI,CACT,GAAG,KAAK,sCAAsC,UAAU,CAAC,MAAM,yBAAyB,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,EAAE,CACrH,CAAC;gBACJ,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAED,+EAA+E;IAC/E,IAAI,IAAI,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;QACjC,IAAI,CAAC,uBAAuB,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAmD,CAAC,EAAE,CAAC;YAChG,MAAM,CAAC,IAAI,CACT,6BAA6B,uBAAuB,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,IAAI,CAAC,SAAS,GAAG,CAC5F,CAAC;QACJ,CAAC;IACH,CAAC;IAED,OAAO;QACL,KAAK,EAAE,MAAM,CAAC,MAAM,KAAK,CAAC;QAC1B,MAAM;KACP,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StripeClient
|
|
3
|
+
* ============
|
|
4
|
+
* A clean, typed wrapper around Stripe's Issuing API.
|
|
5
|
+
*
|
|
6
|
+
* Stripe's Issuing product lets you create virtual (and physical) payment cards
|
|
7
|
+
* programmatically. OpenCard uses it to give AI agents their own cards with
|
|
8
|
+
* spending controls attached.
|
|
9
|
+
*
|
|
10
|
+
* ─── Why a wrapper? ─────────────────────────────────────────────────────────
|
|
11
|
+
* The raw Stripe SDK is powerful but returns Stripe's own types, which don't
|
|
12
|
+
* always match what OpenCard needs. This wrapper:
|
|
13
|
+
* - Converts Stripe's snake_case fields to camelCase (matches our TypeScript style)
|
|
14
|
+
* - Maps Stripe's types to our internal interfaces (Card, Cardholder, etc.)
|
|
15
|
+
* - Centralizes error handling in one place
|
|
16
|
+
* - Makes the API surface smaller and easier to understand
|
|
17
|
+
*
|
|
18
|
+
* ─── Stripe API version ─────────────────────────────────────────────────────
|
|
19
|
+
* We pin to "2023-10-16". If you upgrade the Stripe SDK, check if the API
|
|
20
|
+
* version needs updating and whether any field names/types changed.
|
|
21
|
+
*
|
|
22
|
+
* ─── Test vs. Live mode ─────────────────────────────────────────────────────
|
|
23
|
+
* Stripe uses the key prefix to determine mode:
|
|
24
|
+
* sk_test_... → test mode (no real money, safe to experiment)
|
|
25
|
+
* sk_live_... → live mode (REAL money — be careful!)
|
|
26
|
+
* Never commit a live key to git.
|
|
27
|
+
*/
|
|
28
|
+
import { OpenCardConfig, Card, Cardholder, BillingDetails, CardCreateOptions, Transaction, CardBalance, TransactionQueryOptions, Authorization, SpendingLimits } from './types';
|
|
29
|
+
export declare class StripeClient {
|
|
30
|
+
/** The underlying Stripe SDK instance. Use this for any Stripe calls not yet wrapped here. */
|
|
31
|
+
private stripe;
|
|
32
|
+
/** Stored config so we can reference it in later operations. */
|
|
33
|
+
private config;
|
|
34
|
+
/**
|
|
35
|
+
* Creates a new StripeClient.
|
|
36
|
+
*
|
|
37
|
+
* @param config - Optional configuration. If omitted, reads from environment variables.
|
|
38
|
+
*
|
|
39
|
+
* @throws Error if no API key is found (neither in config nor in STRIPE_SECRET_KEY env var)
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* // Use environment variable (recommended for production)
|
|
43
|
+
* const client = new StripeClient();
|
|
44
|
+
*
|
|
45
|
+
* // Pass key directly (good for testing)
|
|
46
|
+
* const client = new StripeClient({ secretKey: 'sk_test_...' });
|
|
47
|
+
*/
|
|
48
|
+
constructor(config?: OpenCardConfig);
|
|
49
|
+
/**
|
|
50
|
+
* Creates a new cardholder in Stripe Issuing.
|
|
51
|
+
*
|
|
52
|
+
* A cardholder is the entity that "owns" the cards. In a typical OpenCard
|
|
53
|
+
* setup, you'd create one cardholder per agent or per project, then create
|
|
54
|
+
* multiple cards under that cardholder.
|
|
55
|
+
*
|
|
56
|
+
* Stripe requires a billing address for cardholders in most configurations.
|
|
57
|
+
* In test mode this requirement may be relaxed.
|
|
58
|
+
*
|
|
59
|
+
* @param name - Display name for the cardholder (e.g. "Atlas Agent", "Research Bot")
|
|
60
|
+
* @param email - Contact email (used by Stripe for notifications)
|
|
61
|
+
* @param billing - Billing address (required for live mode)
|
|
62
|
+
* @returns The created Cardholder object
|
|
63
|
+
*/
|
|
64
|
+
createCardholder(name: string, email: string, billing?: BillingDetails): Promise<Cardholder>;
|
|
65
|
+
/**
|
|
66
|
+
* Creates a new virtual card for an agent.
|
|
67
|
+
*
|
|
68
|
+
* The card is issued under the specified cardholder. Spending limits can be
|
|
69
|
+
* applied either via Stripe's native spending_controls (enforced at the
|
|
70
|
+
* Stripe level, last-resort safeguard) or via OpenCard's rules engine
|
|
71
|
+
* (enforced via the webhook server, smarter but requires the server to be running).
|
|
72
|
+
*
|
|
73
|
+
* The `agentName` is stored in card metadata so you can identify which
|
|
74
|
+
* agent this card belongs to when reviewing Stripe dashboard or logs.
|
|
75
|
+
*
|
|
76
|
+
* @param cardholderId - The Stripe cardholder ID (e.g. "ich_abc123")
|
|
77
|
+
* @param options - Card creation options (name, rules, status, metadata)
|
|
78
|
+
* @returns The created Card object
|
|
79
|
+
*/
|
|
80
|
+
createCard(cardholderId: string, options: CardCreateOptions): Promise<Card>;
|
|
81
|
+
/**
|
|
82
|
+
* Retrieves a card by its Stripe card ID.
|
|
83
|
+
* Useful for checking current status, limits, and metadata.
|
|
84
|
+
*
|
|
85
|
+
* @param cardId - Stripe card ID (e.g. "ic_abc123")
|
|
86
|
+
*/
|
|
87
|
+
getCard(cardId: string): Promise<Card>;
|
|
88
|
+
/**
|
|
89
|
+
* Pauses a card by setting its status to 'inactive'.
|
|
90
|
+
*
|
|
91
|
+
* A paused card will decline all new charges but can be resumed later.
|
|
92
|
+
* This is different from canceling — canceled cards cannot be reactivated.
|
|
93
|
+
*
|
|
94
|
+
* Use this when an agent has violated rules and needs to be stopped temporarily,
|
|
95
|
+
* or when you want to manually review spending before re-enabling.
|
|
96
|
+
*
|
|
97
|
+
* @param cardId - Stripe card ID to pause
|
|
98
|
+
*/
|
|
99
|
+
pauseCard(cardId: string): Promise<Card>;
|
|
100
|
+
/**
|
|
101
|
+
* Resumes a paused card by setting its status back to 'active'.
|
|
102
|
+
* The card will accept new charges again immediately after this call.
|
|
103
|
+
*
|
|
104
|
+
* @param cardId - Stripe card ID to resume
|
|
105
|
+
*/
|
|
106
|
+
resumeCard(cardId: string): Promise<Card>;
|
|
107
|
+
/**
|
|
108
|
+
* Updates the spending limits on a card.
|
|
109
|
+
*
|
|
110
|
+
* These are Stripe-native limits (not OpenCard rules). They're enforced at
|
|
111
|
+
* the Stripe level regardless of whether our webhook server is running.
|
|
112
|
+
* Good for setting hard floors that can never be bypassed.
|
|
113
|
+
*
|
|
114
|
+
* @param cardId - Stripe card ID
|
|
115
|
+
* @param limits - Array of spending limit objects
|
|
116
|
+
*/
|
|
117
|
+
setSpendingLimits(cardId: string, limits: SpendingLimits[]): Promise<Card>;
|
|
118
|
+
/**
|
|
119
|
+
* Retrieves the transaction history for a card.
|
|
120
|
+
*
|
|
121
|
+
* Note: Stripe Issuing transactions are distinct from regular Stripe charges.
|
|
122
|
+
* They represent captured spending on issued cards.
|
|
123
|
+
*
|
|
124
|
+
* Note on Stripe v14 Transaction type: The `status` and `decline_reason` fields
|
|
125
|
+
* don't exist on Issuing.Transaction in the v14 type definitions (the transaction
|
|
126
|
+
* object represents *completed* captures, not pending authorizations). For
|
|
127
|
+
* pending/declined status, use the Issuing.Authorization type instead.
|
|
128
|
+
*
|
|
129
|
+
* @param cardId - Stripe card ID
|
|
130
|
+
* @param options - Optional filters (limit, date range, etc.)
|
|
131
|
+
*/
|
|
132
|
+
getTransactions(cardId: string, options?: TransactionQueryOptions): Promise<Transaction[]>;
|
|
133
|
+
/**
|
|
134
|
+
* Returns a balance snapshot for this account.
|
|
135
|
+
*
|
|
136
|
+
* Note: Stripe Issuing doesn't expose a per-card balance API. The "balance"
|
|
137
|
+
* for virtual cards is determined by your spending limits, not a pre-loaded
|
|
138
|
+
* amount. This is a stub that returns zeroes.
|
|
139
|
+
*
|
|
140
|
+
* Phase 2 will compute this by summing transactions against spending limits.
|
|
141
|
+
*/
|
|
142
|
+
getBalance(): Promise<CardBalance>;
|
|
143
|
+
/**
|
|
144
|
+
* Returns pending authorizations for a card.
|
|
145
|
+
*
|
|
146
|
+
* Stripe doesn't provide a REST API for pending authorizations — they're
|
|
147
|
+
* delivered exclusively via webhooks. This stub exists for API completeness.
|
|
148
|
+
* Phase 1 uses the webhook server for real-time authorization handling.
|
|
149
|
+
*
|
|
150
|
+
* @param _cardId - Card ID (unused in Phase 1)
|
|
151
|
+
*/
|
|
152
|
+
getPendingAuthorizations(_cardId: string): Promise<Authorization[]>;
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=stripe-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stripe-client.d.ts","sourceRoot":"","sources":["../src/stripe-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAGH,OAAO,EACL,cAAc,EACd,IAAI,EACJ,UAAU,EACV,cAAc,EACd,iBAAiB,EACjB,WAAW,EACX,WAAW,EACX,uBAAuB,EACvB,aAAa,EACb,cAAc,EACf,MAAM,SAAS,CAAC;AAIjB,qBAAa,YAAY;IACvB,8FAA8F;IAC9F,OAAO,CAAC,MAAM,CAAS;IACvB,gEAAgE;IAChE,OAAO,CAAC,MAAM,CAA2B;IAEzC;;;;;;;;;;;;;OAaG;gBACS,MAAM,GAAE,cAAmB;IAqCvC;;;;;;;;;;;;;;OAcG;IACG,gBAAgB,CACpB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,UAAU,CAAC;IAiDtB;;;;;;;;;;;;;;OAcG;IACG,UAAU,CACd,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,IAAI,CAAC;IA8DhB;;;;;OAKG;IACG,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAc5C;;;;;;;;;;OAUG;IACG,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgB9C;;;;;OAKG;IACG,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgB/C;;;;;;;;;OASG;IACG,iBAAiB,CACrB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,cAAc,EAAE,GACvB,OAAO,CAAC,IAAI,CAAC;IAwBhB;;;;;;;;;;;;;OAaG;IACG,eAAe,CACnB,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,uBAAuB,GAChC,OAAO,CAAC,WAAW,EAAE,CAAC;IAkCzB;;;;;;;;OAQG;IACG,UAAU,IAAI,OAAO,CAAC,WAAW,CAAC;IASxC;;;;;;;;OAQG;IACG,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC;CAM1E"}
|