@terminator-network/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/index.js ADDED
@@ -0,0 +1,1109 @@
1
+ // src/config.ts
2
+ import { z } from "zod";
3
+ import { homedir } from "os";
4
+ import { join } from "path";
5
+ var ConfigSchema = z.object({
6
+ // Cloudflare Email
7
+ cfApiToken: z.string().optional(),
8
+ cfAccountId: z.string().optional(),
9
+ cfZoneId: z.string().optional(),
10
+ emailDomain: z.string().optional(),
11
+ // Twilio
12
+ twilioAccountSid: z.string().optional(),
13
+ twilioAuthToken: z.string().optional(),
14
+ // Lithic
15
+ lithicApiKey: z.string().optional(),
16
+ lithicEnvironment: z.enum(["sandbox", "production"]).default("sandbox"),
17
+ // General
18
+ dbPath: z.string().default(join(homedir(), ".terminator", "terminator.db")),
19
+ logLevel: z.enum(["debug", "info", "warn", "error"]).default("info"),
20
+ maxIdentities: z.coerce.number().int().positive().default(50),
21
+ defaultTtlMinutes: z.coerce.number().int().positive().default(60),
22
+ defaultSpendLimitCents: z.coerce.number().int().positive().default(5e3)
23
+ });
24
+ function loadConfig(overrides) {
25
+ const raw = {
26
+ cfApiToken: process.env.TERMINATOR_CF_API_TOKEN,
27
+ cfAccountId: process.env.TERMINATOR_CF_ACCOUNT_ID,
28
+ cfZoneId: process.env.TERMINATOR_CF_ZONE_ID,
29
+ emailDomain: process.env.TERMINATOR_EMAIL_DOMAIN,
30
+ twilioAccountSid: process.env.TERMINATOR_TWILIO_ACCOUNT_SID,
31
+ twilioAuthToken: process.env.TERMINATOR_TWILIO_AUTH_TOKEN,
32
+ lithicApiKey: process.env.TERMINATOR_LITHIC_API_KEY,
33
+ lithicEnvironment: process.env.TERMINATOR_LITHIC_ENVIRONMENT,
34
+ dbPath: process.env.TERMINATOR_DB_PATH,
35
+ logLevel: process.env.TERMINATOR_LOG_LEVEL,
36
+ maxIdentities: process.env.TERMINATOR_MAX_IDENTITIES,
37
+ defaultTtlMinutes: process.env.TERMINATOR_DEFAULT_TTL_MINUTES,
38
+ defaultSpendLimitCents: process.env.TERMINATOR_DEFAULT_SPEND_LIMIT_CENTS,
39
+ ...overrides
40
+ };
41
+ const filtered = Object.fromEntries(
42
+ Object.entries(raw).filter(([_, v]) => v !== void 0)
43
+ );
44
+ const result = ConfigSchema.safeParse(filtered);
45
+ if (!result.success) {
46
+ const issues = result.error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`);
47
+ throw new Error(`Terminator configuration error:
48
+ ${issues.join("\n")}`);
49
+ }
50
+ return result.data;
51
+ }
52
+ function getConfiguredProviders(config) {
53
+ return {
54
+ email: !!(config.cfApiToken && config.cfAccountId && config.emailDomain),
55
+ phone: !!(config.twilioAccountSid && config.twilioAuthToken),
56
+ card: !!config.lithicApiKey
57
+ };
58
+ }
59
+
60
+ // src/identity/persona.ts
61
+ var FIRST_NAMES = [
62
+ "Alex",
63
+ "Jordan",
64
+ "Taylor",
65
+ "Morgan",
66
+ "Casey",
67
+ "Riley",
68
+ "Quinn",
69
+ "Avery",
70
+ "Blake",
71
+ "Drew",
72
+ "Ellis",
73
+ "Finley",
74
+ "Harper",
75
+ "Jamie",
76
+ "Kai",
77
+ "Lane",
78
+ "Marley",
79
+ "Nico",
80
+ "Parker",
81
+ "Reese",
82
+ "Sage",
83
+ "Skyler",
84
+ "Tatum",
85
+ "Val",
86
+ "Wren"
87
+ ];
88
+ var LAST_NAMES = [
89
+ "Anderson",
90
+ "Brooks",
91
+ "Chen",
92
+ "Davis",
93
+ "Evans",
94
+ "Foster",
95
+ "Garcia",
96
+ "Hayes",
97
+ "Ito",
98
+ "Jensen",
99
+ "Kim",
100
+ "Lambert",
101
+ "Mitchell",
102
+ "Nakamura",
103
+ "Ortiz",
104
+ "Palmer",
105
+ "Reed",
106
+ "Shaw",
107
+ "Torres",
108
+ "Vance",
109
+ "Walsh",
110
+ "Young",
111
+ "Zhang",
112
+ "Barrett",
113
+ "Cole"
114
+ ];
115
+ var STREETS = [
116
+ "123 Oak Ave",
117
+ "456 Maple St",
118
+ "789 Pine Rd",
119
+ "321 Elm Dr",
120
+ "654 Cedar Ln",
121
+ "987 Birch Way",
122
+ "147 Willow Ct",
123
+ "258 Spruce Blvd",
124
+ "369 Aspen Pl",
125
+ "741 Walnut Ter"
126
+ ];
127
+ var CITIES_STATES = [
128
+ { city: "Portland", state: "OR", zip: "97201" },
129
+ { city: "Austin", state: "TX", zip: "78701" },
130
+ { city: "Denver", state: "CO", zip: "80202" },
131
+ { city: "Seattle", state: "WA", zip: "98101" },
132
+ { city: "Chicago", state: "IL", zip: "60601" },
133
+ { city: "Boston", state: "MA", zip: "02101" },
134
+ { city: "Atlanta", state: "GA", zip: "30301" },
135
+ { city: "Miami", state: "FL", zip: "33101" },
136
+ { city: "Phoenix", state: "AZ", zip: "85001" },
137
+ { city: "Minneapolis", state: "MN", zip: "55401" }
138
+ ];
139
+ function pick(arr) {
140
+ return arr[Math.floor(Math.random() * arr.length)];
141
+ }
142
+ function createPersona() {
143
+ const firstName = pick(FIRST_NAMES);
144
+ const lastName = pick(LAST_NAMES);
145
+ const location = pick(CITIES_STATES);
146
+ const street = pick(STREETS);
147
+ return {
148
+ firstName,
149
+ lastName,
150
+ fullName: `${firstName} ${lastName}`,
151
+ address: {
152
+ line1: street,
153
+ city: location.city,
154
+ state: location.state,
155
+ postalCode: location.zip,
156
+ country: "US"
157
+ }
158
+ };
159
+ }
160
+
161
+ // src/kill-switch.ts
162
+ var KillSwitch = class {
163
+ constructor(identityStore, activityLog, emailProvider, phoneProvider, cardProvider) {
164
+ this.identityStore = identityStore;
165
+ this.activityLog = activityLog;
166
+ this.emailProvider = emailProvider;
167
+ this.phoneProvider = phoneProvider;
168
+ this.cardProvider = cardProvider;
169
+ }
170
+ async killIdentity(id) {
171
+ const identity = this.identityStore.get(id);
172
+ if (!identity) throw new Error(`Identity not found: ${id}`);
173
+ this.identityStore.updateStatus(id, "killing");
174
+ this.activityLog.append({
175
+ identityId: id,
176
+ eventType: "identity.killing",
177
+ provider: null,
178
+ resourceType: "identity",
179
+ resourceId: id,
180
+ details: null,
181
+ costEstimateCents: null
182
+ });
183
+ const result = {
184
+ identityId: id,
185
+ emailRevoked: false,
186
+ phoneRevoked: false,
187
+ cardRevoked: false,
188
+ errors: []
189
+ };
190
+ const tasks = [];
191
+ if (identity.email && identity.emailProviderId && this.emailProvider) {
192
+ tasks.push(
193
+ this.revokeEmail(identity, result)
194
+ );
195
+ }
196
+ if (identity.phone && identity.phoneProviderId && this.phoneProvider) {
197
+ tasks.push(
198
+ this.revokePhone(identity, result)
199
+ );
200
+ }
201
+ if (identity.cardProviderId && this.cardProvider) {
202
+ tasks.push(
203
+ this.revokeCard(identity, result)
204
+ );
205
+ }
206
+ await Promise.allSettled(tasks);
207
+ this.identityStore.updateStatus(id, "killed");
208
+ this.activityLog.append({
209
+ identityId: id,
210
+ eventType: "identity.killed",
211
+ provider: null,
212
+ resourceType: "identity",
213
+ resourceId: id,
214
+ details: {
215
+ emailRevoked: result.emailRevoked,
216
+ phoneRevoked: result.phoneRevoked,
217
+ cardRevoked: result.cardRevoked,
218
+ errors: result.errors
219
+ },
220
+ costEstimateCents: null
221
+ });
222
+ return result;
223
+ }
224
+ async killAll() {
225
+ const active = this.identityStore.list("active");
226
+ const provisioning = this.identityStore.list("provisioning");
227
+ const all = [...active, ...provisioning];
228
+ const results = [];
229
+ for (const identity of all) {
230
+ try {
231
+ const result = await this.killIdentity(identity.id);
232
+ results.push(result);
233
+ } catch (error) {
234
+ results.push({
235
+ identityId: identity.id,
236
+ emailRevoked: false,
237
+ phoneRevoked: false,
238
+ cardRevoked: false,
239
+ errors: [error instanceof Error ? error.message : String(error)]
240
+ });
241
+ }
242
+ }
243
+ return results;
244
+ }
245
+ async revokeEmail(identity, result) {
246
+ try {
247
+ await this.emailProvider.deleteInbox({
248
+ address: identity.email,
249
+ providerId: identity.emailProviderId,
250
+ provider: this.emailProvider.name
251
+ });
252
+ result.emailRevoked = true;
253
+ this.activityLog.append({
254
+ identityId: identity.id,
255
+ eventType: "email.inbox_deleted",
256
+ provider: this.emailProvider.name,
257
+ resourceType: "email",
258
+ resourceId: identity.emailProviderId,
259
+ details: { address: identity.email },
260
+ costEstimateCents: null
261
+ });
262
+ } catch (error) {
263
+ result.errors.push(`Email: ${error instanceof Error ? error.message : String(error)}`);
264
+ }
265
+ }
266
+ async revokePhone(identity, result) {
267
+ try {
268
+ await this.phoneProvider.releaseNumber({
269
+ number: identity.phone,
270
+ providerId: identity.phoneProviderId,
271
+ provider: this.phoneProvider.name
272
+ });
273
+ result.phoneRevoked = true;
274
+ this.activityLog.append({
275
+ identityId: identity.id,
276
+ eventType: "phone.released",
277
+ provider: this.phoneProvider.name,
278
+ resourceType: "phone",
279
+ resourceId: identity.phoneProviderId,
280
+ details: { number: identity.phone },
281
+ costEstimateCents: null
282
+ });
283
+ } catch (error) {
284
+ result.errors.push(`Phone: ${error instanceof Error ? error.message : String(error)}`);
285
+ }
286
+ }
287
+ async revokeCard(identity, result) {
288
+ try {
289
+ await this.cardProvider.deactivateCard({
290
+ lastFour: identity.cardLastFour,
291
+ providerId: identity.cardProviderId,
292
+ provider: this.cardProvider.name
293
+ });
294
+ result.cardRevoked = true;
295
+ this.activityLog.append({
296
+ identityId: identity.id,
297
+ eventType: "card.deactivated",
298
+ provider: this.cardProvider.name,
299
+ resourceType: "card",
300
+ resourceId: identity.cardProviderId,
301
+ details: { lastFour: identity.cardLastFour },
302
+ costEstimateCents: null
303
+ });
304
+ } catch (error) {
305
+ result.errors.push(`Card: ${error instanceof Error ? error.message : String(error)}`);
306
+ }
307
+ }
308
+ };
309
+
310
+ // src/parsers/verification.ts
311
+ var CODE_PATTERNS = [
312
+ // "Your code is 123456" or "verification code: 123456"
313
+ /(?:code|otp|pin|token)\s*(?:is|:)\s*(\d{4,8})/i,
314
+ // "123456 is your verification code"
315
+ /(\d{4,8})\s+is\s+your\s+(?:verification|confirmation|security)\s+code/i,
316
+ // "Enter 123456 to verify"
317
+ /enter\s+(\d{4,8})\s+to/i,
318
+ // Standalone 6-digit code on its own line
319
+ /^(\d{6})$/m,
320
+ // "G-123456" (Google-style)
321
+ /[A-Z]-(\d{4,8})/
322
+ ];
323
+ function parseVerificationCode(text) {
324
+ for (const pattern of CODE_PATTERNS) {
325
+ const match = text.match(pattern);
326
+ if (match?.[1]) {
327
+ return {
328
+ code: match[1],
329
+ pattern: pattern.source
330
+ };
331
+ }
332
+ }
333
+ return null;
334
+ }
335
+
336
+ // src/policies/engine.ts
337
+ var PolicyEngine = class {
338
+ constructor(policyStore, identityStore, config) {
339
+ this.policyStore = policyStore;
340
+ this.identityStore = identityStore;
341
+ this.config = config;
342
+ }
343
+ checkCreateIdentity(resources, spendLimitCents) {
344
+ const violations = [];
345
+ const policies = this.policyStore.getEnabled();
346
+ for (const policy of policies) {
347
+ const violation = this.evaluateForCreate(policy, resources, spendLimitCents);
348
+ if (violation) violations.push(violation);
349
+ }
350
+ const activeCount = this.identityStore.countActive();
351
+ if (activeCount >= this.config.maxIdentities) {
352
+ violations.push({
353
+ policy: "built-in:max_identities",
354
+ ruleType: "max_identities",
355
+ message: `Maximum active identities reached (${activeCount}/${this.config.maxIdentities})`
356
+ });
357
+ }
358
+ return violations;
359
+ }
360
+ evaluateForCreate(policy, resources, spendLimitCents) {
361
+ switch (policy.ruleType) {
362
+ case "max_identities": {
363
+ const max = policy.ruleValue;
364
+ const activeCount = this.identityStore.countActive();
365
+ if (activeCount >= max) {
366
+ return {
367
+ policy: policy.name,
368
+ ruleType: policy.ruleType,
369
+ message: `Policy "${policy.name}": max identities (${max}) reached`
370
+ };
371
+ }
372
+ return null;
373
+ }
374
+ case "max_spend_per_identity": {
375
+ const maxSpend = policy.ruleValue;
376
+ const requestedSpend = spendLimitCents ?? this.config.defaultSpendLimitCents;
377
+ if (resources.includes("card") && requestedSpend > maxSpend) {
378
+ return {
379
+ policy: policy.name,
380
+ ruleType: policy.ruleType,
381
+ message: `Policy "${policy.name}": spend limit $${(requestedSpend / 100).toFixed(2)} exceeds max $${(maxSpend / 100).toFixed(2)}`
382
+ };
383
+ }
384
+ return null;
385
+ }
386
+ case "require_card_approval": {
387
+ if (resources.includes("card") && policy.ruleValue === true) {
388
+ return {
389
+ policy: policy.name,
390
+ ruleType: policy.ruleType,
391
+ message: `Policy "${policy.name}": card creation requires explicit approval`
392
+ };
393
+ }
394
+ return null;
395
+ }
396
+ default:
397
+ return null;
398
+ }
399
+ }
400
+ };
401
+
402
+ // src/providers/card/mock.ts
403
+ import { ulid } from "ulid";
404
+ var MockCardProvider = class {
405
+ name = "mock";
406
+ cards = /* @__PURE__ */ new Map();
407
+ async createCard(_identity, options) {
408
+ const id = ulid();
409
+ const lastFour = String(Math.floor(1e3 + Math.random() * 9e3));
410
+ const details = {
411
+ pan: `4242424242${String(Math.floor(1e5 + Math.random() * 9e5))}${lastFour}`,
412
+ cvv: String(Math.floor(100 + Math.random() * 900)),
413
+ expMonth: (/* @__PURE__ */ new Date()).getMonth() + 1,
414
+ expYear: (/* @__PURE__ */ new Date()).getFullYear() + 2,
415
+ lastFour
416
+ };
417
+ this.cards.set(id, { details, active: true });
418
+ return { lastFour, providerId: id, provider: this.name };
419
+ }
420
+ async getCardDetails(card) {
421
+ const entry = this.cards.get(card.providerId);
422
+ if (!entry) throw new Error(`Card not found: ${card.providerId}`);
423
+ if (!entry.active) throw new Error(`Card is deactivated: ${card.providerId}`);
424
+ return entry.details;
425
+ }
426
+ async deactivateCard(card) {
427
+ const entry = this.cards.get(card.providerId);
428
+ if (entry) entry.active = false;
429
+ }
430
+ async healthCheck() {
431
+ return { provider: this.name, healthy: true, message: "Mock provider always healthy" };
432
+ }
433
+ };
434
+
435
+ // src/providers/email/mock.ts
436
+ import { ulid as ulid2 } from "ulid";
437
+ var MockEmailProvider = class {
438
+ name = "mock";
439
+ inboxes = /* @__PURE__ */ new Map();
440
+ async createInbox(identity) {
441
+ const id = ulid2();
442
+ const address = `${identity.persona.firstName.toLowerCase()}-${id.slice(-6).toLowerCase()}@mock.terminator.test`;
443
+ this.inboxes.set(address, []);
444
+ return { address, providerId: id, provider: this.name };
445
+ }
446
+ async getMessages(inbox, since) {
447
+ const messages = this.inboxes.get(inbox.address) ?? [];
448
+ if (!since) return messages;
449
+ return messages.filter((m) => new Date(m.receivedAt) >= since);
450
+ }
451
+ async deleteInbox(inbox) {
452
+ this.inboxes.delete(inbox.address);
453
+ }
454
+ async healthCheck() {
455
+ return { provider: this.name, healthy: true, message: "Mock provider always healthy" };
456
+ }
457
+ // Test helper: inject a message into an inbox
458
+ injectMessage(address, message) {
459
+ const messages = this.inboxes.get(address);
460
+ if (!messages) throw new Error(`No inbox found for ${address}`);
461
+ messages.push({
462
+ ...message,
463
+ id: ulid2(),
464
+ receivedAt: (/* @__PURE__ */ new Date()).toISOString()
465
+ });
466
+ }
467
+ };
468
+
469
+ // src/providers/phone/mock.ts
470
+ import { ulid as ulid3 } from "ulid";
471
+ var MockPhoneProvider = class {
472
+ name = "mock";
473
+ numbers = /* @__PURE__ */ new Map();
474
+ async provisionNumber(_identity, _options) {
475
+ const id = ulid3();
476
+ const areaCode = Math.floor(200 + Math.random() * 800);
477
+ const lineNumber = Math.floor(1e6 + Math.random() * 9e6);
478
+ const number = `+1${areaCode}${lineNumber}`;
479
+ this.numbers.set(number, []);
480
+ return { number, providerId: id, provider: this.name };
481
+ }
482
+ async getMessages(phone, since) {
483
+ const messages = this.numbers.get(phone.number) ?? [];
484
+ if (!since) return messages;
485
+ return messages.filter((m) => new Date(m.receivedAt) >= since);
486
+ }
487
+ async releaseNumber(phone) {
488
+ this.numbers.delete(phone.number);
489
+ }
490
+ async healthCheck() {
491
+ return { provider: this.name, healthy: true, message: "Mock provider always healthy" };
492
+ }
493
+ // Test helper: inject an SMS into a number
494
+ injectSms(number, message) {
495
+ const messages = this.numbers.get(number);
496
+ if (!messages) throw new Error(`No phone number found for ${number}`);
497
+ messages.push({
498
+ ...message,
499
+ id: ulid3(),
500
+ receivedAt: (/* @__PURE__ */ new Date()).toISOString()
501
+ });
502
+ }
503
+ };
504
+
505
+ // src/store/activity-log.ts
506
+ import { ulid as ulid4 } from "ulid";
507
+ function rowToEvent(row) {
508
+ return {
509
+ id: row.id,
510
+ identityId: row.identity_id,
511
+ timestamp: row.timestamp,
512
+ eventType: row.event_type,
513
+ provider: row.provider,
514
+ resourceType: row.resource_type,
515
+ resourceId: row.resource_id,
516
+ details: row.details ? JSON.parse(row.details) : null,
517
+ costEstimateCents: row.cost_estimate_cents
518
+ };
519
+ }
520
+ var ActivityLog = class {
521
+ constructor(db) {
522
+ this.db = db;
523
+ }
524
+ append(event) {
525
+ const id = ulid4();
526
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
527
+ this.db.prepare(`
528
+ INSERT INTO activity_log (
529
+ id, identity_id, timestamp, event_type, provider,
530
+ resource_type, resource_id, details, cost_estimate_cents
531
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
532
+ `).run(
533
+ id,
534
+ event.identityId,
535
+ timestamp,
536
+ event.eventType,
537
+ event.provider,
538
+ event.resourceType,
539
+ event.resourceId,
540
+ event.details ? JSON.stringify(event.details) : null,
541
+ event.costEstimateCents
542
+ );
543
+ return { id, timestamp, ...event };
544
+ }
545
+ query(options) {
546
+ const conditions = [];
547
+ const values = [];
548
+ if (options?.identityId) {
549
+ conditions.push("identity_id = ?");
550
+ values.push(options.identityId);
551
+ }
552
+ if (options?.eventType) {
553
+ conditions.push("event_type = ?");
554
+ values.push(options.eventType);
555
+ }
556
+ if (options?.since) {
557
+ conditions.push("timestamp >= ?");
558
+ values.push(options.since);
559
+ }
560
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
561
+ const limit = options?.limit ? `LIMIT ${options.limit}` : "LIMIT 100";
562
+ const rows = this.db.prepare(
563
+ `SELECT * FROM activity_log ${where} ORDER BY timestamp DESC ${limit}`
564
+ ).all(...values);
565
+ return rows.map(rowToEvent);
566
+ }
567
+ };
568
+
569
+ // src/store/database.ts
570
+ import Database from "better-sqlite3";
571
+ import { mkdirSync } from "fs";
572
+ import { dirname } from "path";
573
+ var SCHEMA = `
574
+ CREATE TABLE IF NOT EXISTS identities (
575
+ id TEXT PRIMARY KEY,
576
+ persona_first_name TEXT NOT NULL,
577
+ persona_last_name TEXT NOT NULL,
578
+ persona_full_name TEXT NOT NULL,
579
+ persona_address_json TEXT NOT NULL,
580
+ email TEXT,
581
+ email_provider_id TEXT,
582
+ phone TEXT,
583
+ phone_provider_id TEXT,
584
+ card_last_four TEXT,
585
+ card_provider_id TEXT,
586
+ status TEXT NOT NULL DEFAULT 'created',
587
+ spend_limit_cents INTEGER,
588
+ ttl_expires_at TEXT,
589
+ resources_json TEXT NOT NULL DEFAULT '[]',
590
+ created_at TEXT NOT NULL,
591
+ updated_at TEXT NOT NULL
592
+ );
593
+
594
+ CREATE TABLE IF NOT EXISTS activity_log (
595
+ id TEXT PRIMARY KEY,
596
+ identity_id TEXT,
597
+ timestamp TEXT NOT NULL,
598
+ event_type TEXT NOT NULL,
599
+ provider TEXT,
600
+ resource_type TEXT,
601
+ resource_id TEXT,
602
+ details TEXT,
603
+ cost_estimate_cents INTEGER,
604
+ FOREIGN KEY (identity_id) REFERENCES identities(id)
605
+ );
606
+
607
+ CREATE TABLE IF NOT EXISTS policies (
608
+ id TEXT PRIMARY KEY,
609
+ name TEXT NOT NULL,
610
+ rule_type TEXT NOT NULL,
611
+ rule_value TEXT NOT NULL,
612
+ enabled INTEGER NOT NULL DEFAULT 1,
613
+ created_at TEXT NOT NULL
614
+ );
615
+
616
+ CREATE INDEX IF NOT EXISTS idx_activity_identity ON activity_log(identity_id);
617
+ CREATE INDEX IF NOT EXISTS idx_activity_timestamp ON activity_log(timestamp);
618
+ CREATE INDEX IF NOT EXISTS idx_activity_event_type ON activity_log(event_type);
619
+ CREATE INDEX IF NOT EXISTS idx_identities_status ON identities(status);
620
+ `;
621
+ function createDatabase(dbPath) {
622
+ mkdirSync(dirname(dbPath), { recursive: true });
623
+ const db = new Database(dbPath);
624
+ db.pragma("journal_mode = WAL");
625
+ db.pragma("foreign_keys = ON");
626
+ db.exec(SCHEMA);
627
+ return db;
628
+ }
629
+ function createInMemoryDatabase() {
630
+ const db = new Database(":memory:");
631
+ db.pragma("foreign_keys = ON");
632
+ db.exec(SCHEMA);
633
+ return db;
634
+ }
635
+
636
+ // src/store/identities.ts
637
+ import { ulid as ulid5 } from "ulid";
638
+
639
+ // src/identity/state-machine.ts
640
+ var VALID_TRANSITIONS = {
641
+ created: ["provisioning", "failed"],
642
+ provisioning: ["active", "failed", "killing"],
643
+ active: ["killing"],
644
+ killing: ["killed", "failed"],
645
+ killed: [],
646
+ failed: ["provisioning", "killing"]
647
+ };
648
+ function canTransition(from, to) {
649
+ return VALID_TRANSITIONS[from]?.includes(to) ?? false;
650
+ }
651
+ function assertTransition(from, to) {
652
+ if (!canTransition(from, to)) {
653
+ throw new Error(`Invalid identity transition: ${from} -> ${to}`);
654
+ }
655
+ }
656
+
657
+ // src/store/identities.ts
658
+ function rowToIdentity(row) {
659
+ return {
660
+ id: row.id,
661
+ persona: {
662
+ firstName: row.persona_first_name,
663
+ lastName: row.persona_last_name,
664
+ fullName: row.persona_full_name,
665
+ address: JSON.parse(row.persona_address_json)
666
+ },
667
+ status: row.status,
668
+ email: row.email ?? void 0,
669
+ emailProviderId: row.email_provider_id ?? void 0,
670
+ phone: row.phone ?? void 0,
671
+ phoneProviderId: row.phone_provider_id ?? void 0,
672
+ cardLastFour: row.card_last_four ?? void 0,
673
+ cardProviderId: row.card_provider_id ?? void 0,
674
+ spendLimitCents: row.spend_limit_cents ?? void 0,
675
+ ttlExpiresAt: row.ttl_expires_at ?? void 0,
676
+ resources: JSON.parse(row.resources_json),
677
+ createdAt: row.created_at,
678
+ updatedAt: row.updated_at
679
+ };
680
+ }
681
+ var IdentityStore = class {
682
+ constructor(db) {
683
+ this.db = db;
684
+ }
685
+ create(persona, resources, options) {
686
+ const id = ulid5();
687
+ const now = (/* @__PURE__ */ new Date()).toISOString();
688
+ const ttlExpiresAt = options?.ttlMinutes ? new Date(Date.now() + options.ttlMinutes * 6e4).toISOString() : null;
689
+ this.db.prepare(`
690
+ INSERT INTO identities (
691
+ id, persona_first_name, persona_last_name, persona_full_name,
692
+ persona_address_json, status, spend_limit_cents, ttl_expires_at,
693
+ resources_json, created_at, updated_at
694
+ ) VALUES (?, ?, ?, ?, ?, 'created', ?, ?, ?, ?, ?)
695
+ `).run(
696
+ id,
697
+ persona.firstName,
698
+ persona.lastName,
699
+ persona.fullName,
700
+ JSON.stringify(persona.address),
701
+ options?.spendLimitCents ?? null,
702
+ ttlExpiresAt,
703
+ JSON.stringify(resources),
704
+ now,
705
+ now
706
+ );
707
+ return this.get(id);
708
+ }
709
+ get(id) {
710
+ const row = this.db.prepare("SELECT * FROM identities WHERE id = ?").get(id);
711
+ return row ? rowToIdentity(row) : null;
712
+ }
713
+ list(status) {
714
+ const rows = status ? this.db.prepare("SELECT * FROM identities WHERE status = ? ORDER BY created_at DESC").all(status) : this.db.prepare("SELECT * FROM identities ORDER BY created_at DESC").all();
715
+ return rows.map(rowToIdentity);
716
+ }
717
+ updateStatus(id, newStatus) {
718
+ const current = this.get(id);
719
+ if (!current) throw new Error(`Identity not found: ${id}`);
720
+ assertTransition(current.status, newStatus);
721
+ this.db.prepare("UPDATE identities SET status = ?, updated_at = ? WHERE id = ?").run(
722
+ newStatus,
723
+ (/* @__PURE__ */ new Date()).toISOString(),
724
+ id
725
+ );
726
+ return this.get(id);
727
+ }
728
+ updateResources(id, updates) {
729
+ const sets = [];
730
+ const values = [];
731
+ if (updates.email !== void 0) {
732
+ sets.push("email = ?");
733
+ values.push(updates.email ?? null);
734
+ }
735
+ if (updates.emailProviderId !== void 0) {
736
+ sets.push("email_provider_id = ?");
737
+ values.push(updates.emailProviderId ?? null);
738
+ }
739
+ if (updates.phone !== void 0) {
740
+ sets.push("phone = ?");
741
+ values.push(updates.phone ?? null);
742
+ }
743
+ if (updates.phoneProviderId !== void 0) {
744
+ sets.push("phone_provider_id = ?");
745
+ values.push(updates.phoneProviderId ?? null);
746
+ }
747
+ if (updates.cardLastFour !== void 0) {
748
+ sets.push("card_last_four = ?");
749
+ values.push(updates.cardLastFour ?? null);
750
+ }
751
+ if (updates.cardProviderId !== void 0) {
752
+ sets.push("card_provider_id = ?");
753
+ values.push(updates.cardProviderId ?? null);
754
+ }
755
+ if (sets.length === 0) return this.get(id);
756
+ sets.push("updated_at = ?");
757
+ values.push((/* @__PURE__ */ new Date()).toISOString());
758
+ values.push(id);
759
+ this.db.prepare(`UPDATE identities SET ${sets.join(", ")} WHERE id = ?`).run(...values);
760
+ return this.get(id);
761
+ }
762
+ getExpired() {
763
+ const now = (/* @__PURE__ */ new Date()).toISOString();
764
+ const rows = this.db.prepare(
765
+ "SELECT * FROM identities WHERE ttl_expires_at IS NOT NULL AND ttl_expires_at <= ? AND status = 'active'"
766
+ ).all(now);
767
+ return rows.map(rowToIdentity);
768
+ }
769
+ countActive() {
770
+ const row = this.db.prepare(
771
+ "SELECT COUNT(*) as count FROM identities WHERE status IN ('created', 'provisioning', 'active')"
772
+ ).get();
773
+ return row.count;
774
+ }
775
+ };
776
+
777
+ // src/store/policies.ts
778
+ import { ulid as ulid6 } from "ulid";
779
+ function rowToPolicy(row) {
780
+ return {
781
+ id: row.id,
782
+ name: row.name,
783
+ ruleType: row.rule_type,
784
+ ruleValue: JSON.parse(row.rule_value),
785
+ enabled: row.enabled === 1,
786
+ createdAt: row.created_at
787
+ };
788
+ }
789
+ var PolicyStore = class {
790
+ constructor(db) {
791
+ this.db = db;
792
+ }
793
+ create(name, ruleType, ruleValue) {
794
+ const id = ulid6();
795
+ const now = (/* @__PURE__ */ new Date()).toISOString();
796
+ this.db.prepare(`
797
+ INSERT INTO policies (id, name, rule_type, rule_value, enabled, created_at)
798
+ VALUES (?, ?, ?, ?, 1, ?)
799
+ `).run(id, name, ruleType, JSON.stringify(ruleValue), now);
800
+ return this.get(id);
801
+ }
802
+ get(id) {
803
+ const row = this.db.prepare("SELECT * FROM policies WHERE id = ?").get(id);
804
+ return row ? rowToPolicy(row) : null;
805
+ }
806
+ list() {
807
+ const rows = this.db.prepare("SELECT * FROM policies ORDER BY created_at DESC").all();
808
+ return rows.map(rowToPolicy);
809
+ }
810
+ getEnabled() {
811
+ const rows = this.db.prepare(
812
+ "SELECT * FROM policies WHERE enabled = 1 ORDER BY created_at DESC"
813
+ ).all();
814
+ return rows.map(rowToPolicy);
815
+ }
816
+ setEnabled(id, enabled) {
817
+ this.db.prepare("UPDATE policies SET enabled = ? WHERE id = ?").run(enabled ? 1 : 0, id);
818
+ }
819
+ delete(id) {
820
+ this.db.prepare("DELETE FROM policies WHERE id = ?").run(id);
821
+ }
822
+ };
823
+
824
+ // src/terminator.ts
825
+ var Terminator = class {
826
+ config;
827
+ identityStore;
828
+ activityLog;
829
+ policyStore;
830
+ policyEngine;
831
+ killSwitch;
832
+ emailProvider;
833
+ phoneProvider;
834
+ cardProvider;
835
+ db;
836
+ constructor(options = {}) {
837
+ this.config = loadConfig(options.config);
838
+ this.db = options.inMemory ? createInMemoryDatabase() : createDatabase(this.config.dbPath);
839
+ this.identityStore = new IdentityStore(this.db);
840
+ this.activityLog = new ActivityLog(this.db);
841
+ this.policyStore = new PolicyStore(this.db);
842
+ this.emailProvider = options.emailProvider ?? new MockEmailProvider();
843
+ this.phoneProvider = options.phoneProvider ?? new MockPhoneProvider();
844
+ this.cardProvider = options.cardProvider ?? new MockCardProvider();
845
+ this.policyEngine = new PolicyEngine(
846
+ this.policyStore,
847
+ this.identityStore,
848
+ this.config
849
+ );
850
+ this.killSwitch = new KillSwitch(
851
+ this.identityStore,
852
+ this.activityLog,
853
+ this.emailProvider,
854
+ this.phoneProvider,
855
+ this.cardProvider
856
+ );
857
+ }
858
+ async createIdentity(options = {}) {
859
+ const resources = options.resources ?? ["email", "phone", "card"];
860
+ const spendLimitCents = options.spendLimitCents ?? this.config.defaultSpendLimitCents;
861
+ const ttlMinutes = options.ttlMinutes ?? this.config.defaultTtlMinutes;
862
+ const violations = this.policyEngine.checkCreateIdentity(resources, spendLimitCents);
863
+ const cardApprovalViolation = violations.find(
864
+ (v) => v.ruleType === "require_card_approval"
865
+ );
866
+ const otherViolations = violations.filter((v) => v.ruleType !== "require_card_approval");
867
+ if (otherViolations.length > 0) {
868
+ throw new PolicyViolationError(otherViolations);
869
+ }
870
+ if (cardApprovalViolation && resources.includes("card") && !options.confirm) {
871
+ throw new PolicyViolationError([cardApprovalViolation]);
872
+ }
873
+ const persona = createPersona();
874
+ let identity = this.identityStore.create(persona, resources, {
875
+ spendLimitCents,
876
+ ttlMinutes
877
+ });
878
+ this.activityLog.append({
879
+ identityId: identity.id,
880
+ eventType: "identity.created",
881
+ provider: null,
882
+ resourceType: "identity",
883
+ resourceId: identity.id,
884
+ details: { resources, persona: { name: persona.fullName } },
885
+ costEstimateCents: null
886
+ });
887
+ identity = this.identityStore.updateStatus(identity.id, "provisioning");
888
+ this.activityLog.append({
889
+ identityId: identity.id,
890
+ eventType: "identity.provisioning",
891
+ provider: null,
892
+ resourceType: "identity",
893
+ resourceId: identity.id,
894
+ details: { resources },
895
+ costEstimateCents: null
896
+ });
897
+ const errors = [];
898
+ if (resources.includes("email") && this.emailProvider) {
899
+ try {
900
+ const inbox = await this.emailProvider.createInbox(identity);
901
+ identity = this.identityStore.updateResources(identity.id, {
902
+ email: inbox.address,
903
+ emailProviderId: inbox.providerId
904
+ });
905
+ persona.email = inbox.address;
906
+ this.activityLog.append({
907
+ identityId: identity.id,
908
+ eventType: "email.inbox_created",
909
+ provider: this.emailProvider.name,
910
+ resourceType: "email",
911
+ resourceId: inbox.providerId,
912
+ details: { address: inbox.address },
913
+ costEstimateCents: 0
914
+ });
915
+ } catch (error) {
916
+ errors.push(`email: ${error instanceof Error ? error.message : String(error)}`);
917
+ }
918
+ }
919
+ if (resources.includes("phone") && this.phoneProvider) {
920
+ try {
921
+ const phone = await this.phoneProvider.provisionNumber(identity);
922
+ identity = this.identityStore.updateResources(identity.id, {
923
+ phone: phone.number,
924
+ phoneProviderId: phone.providerId
925
+ });
926
+ persona.phone = phone.number;
927
+ this.activityLog.append({
928
+ identityId: identity.id,
929
+ eventType: "phone.provisioned",
930
+ provider: this.phoneProvider.name,
931
+ resourceType: "phone",
932
+ resourceId: phone.providerId,
933
+ details: { number: phone.number },
934
+ costEstimateCents: 115
935
+ // ~$1.15/month for Twilio number
936
+ });
937
+ } catch (error) {
938
+ errors.push(`phone: ${error instanceof Error ? error.message : String(error)}`);
939
+ }
940
+ }
941
+ if (resources.includes("card") && this.cardProvider) {
942
+ try {
943
+ const card = await this.cardProvider.createCard(identity, {
944
+ spendLimitCents,
945
+ type: "single_use"
946
+ });
947
+ identity = this.identityStore.updateResources(identity.id, {
948
+ cardLastFour: card.lastFour,
949
+ cardProviderId: card.providerId
950
+ });
951
+ this.activityLog.append({
952
+ identityId: identity.id,
953
+ eventType: "card.created",
954
+ provider: this.cardProvider.name,
955
+ resourceType: "card",
956
+ resourceId: card.providerId,
957
+ details: { lastFour: card.lastFour, spendLimitCents },
958
+ costEstimateCents: 10
959
+ // ~$0.10 per virtual card
960
+ });
961
+ } catch (error) {
962
+ errors.push(`card: ${error instanceof Error ? error.message : String(error)}`);
963
+ }
964
+ }
965
+ const hasAnyResource = identity.email || identity.phone || identity.cardProviderId;
966
+ if (hasAnyResource) {
967
+ identity = this.identityStore.updateStatus(identity.id, "active");
968
+ this.activityLog.append({
969
+ identityId: identity.id,
970
+ eventType: "identity.active",
971
+ provider: null,
972
+ resourceType: "identity",
973
+ resourceId: identity.id,
974
+ details: errors.length > 0 ? { partialErrors: errors } : null,
975
+ costEstimateCents: null
976
+ });
977
+ } else {
978
+ identity = this.identityStore.updateStatus(identity.id, "failed");
979
+ this.activityLog.append({
980
+ identityId: identity.id,
981
+ eventType: "identity.failed",
982
+ provider: null,
983
+ resourceType: "identity",
984
+ resourceId: identity.id,
985
+ details: { errors },
986
+ costEstimateCents: null
987
+ });
988
+ }
989
+ return identity;
990
+ }
991
+ getIdentity(id) {
992
+ return this.identityStore.get(id);
993
+ }
994
+ listIdentities(status) {
995
+ if (status === "all" || status === void 0) return this.identityStore.list();
996
+ return this.identityStore.list(status);
997
+ }
998
+ async readMessages(identityId, since) {
999
+ const identity = this.identityStore.get(identityId);
1000
+ if (!identity) throw new Error(`Identity not found: ${identityId}`);
1001
+ const emails = [];
1002
+ const sms = [];
1003
+ if (identity.email && identity.emailProviderId && this.emailProvider) {
1004
+ const inbox = {
1005
+ address: identity.email,
1006
+ providerId: identity.emailProviderId,
1007
+ provider: this.emailProvider.name
1008
+ };
1009
+ const messages = await this.emailProvider.getMessages(inbox, since);
1010
+ emails.push(...messages);
1011
+ }
1012
+ if (identity.phone && identity.phoneProviderId && this.phoneProvider) {
1013
+ const phone = {
1014
+ number: identity.phone,
1015
+ providerId: identity.phoneProviderId,
1016
+ provider: this.phoneProvider.name
1017
+ };
1018
+ const messages = await this.phoneProvider.getMessages(phone, since);
1019
+ sms.push(...messages);
1020
+ }
1021
+ return { emails, sms };
1022
+ }
1023
+ async getCardDetails(identityId) {
1024
+ const identity = this.identityStore.get(identityId);
1025
+ if (!identity) throw new Error(`Identity not found: ${identityId}`);
1026
+ if (!identity.cardProviderId || !this.cardProvider) {
1027
+ throw new Error("No card associated with this identity");
1028
+ }
1029
+ return this.cardProvider.getCardDetails({
1030
+ lastFour: identity.cardLastFour,
1031
+ providerId: identity.cardProviderId,
1032
+ provider: this.cardProvider.name
1033
+ });
1034
+ }
1035
+ async extractCode(identityId) {
1036
+ const { emails, sms } = await this.readMessages(identityId);
1037
+ for (const msg of [...sms].reverse()) {
1038
+ const result = parseVerificationCode(msg.body);
1039
+ if (result) return result.code;
1040
+ }
1041
+ for (const msg of [...emails].reverse()) {
1042
+ const text = msg.text || msg.subject;
1043
+ const result = parseVerificationCode(text);
1044
+ if (result) return result.code;
1045
+ }
1046
+ return null;
1047
+ }
1048
+ async killIdentity(id) {
1049
+ return this.killSwitch.killIdentity(id);
1050
+ }
1051
+ async killAll() {
1052
+ return this.killSwitch.killAll();
1053
+ }
1054
+ getActivityLog(options) {
1055
+ return this.activityLog.query(options);
1056
+ }
1057
+ async checkStatus() {
1058
+ const providers = [];
1059
+ if (this.emailProvider) {
1060
+ providers.push(await this.emailProvider.healthCheck());
1061
+ }
1062
+ if (this.phoneProvider) {
1063
+ providers.push(await this.phoneProvider.healthCheck());
1064
+ }
1065
+ if (this.cardProvider) {
1066
+ providers.push(await this.cardProvider.healthCheck());
1067
+ }
1068
+ return {
1069
+ providers,
1070
+ activeIdentities: this.identityStore.countActive(),
1071
+ configuredProviders: {
1072
+ email: !!this.emailProvider,
1073
+ phone: !!this.phoneProvider,
1074
+ card: !!this.cardProvider
1075
+ }
1076
+ };
1077
+ }
1078
+ close() {
1079
+ this.db.close();
1080
+ }
1081
+ };
1082
+ var PolicyViolationError = class extends Error {
1083
+ constructor(violations) {
1084
+ super(violations.map((v) => v.message).join("; "));
1085
+ this.violations = violations;
1086
+ this.name = "PolicyViolationError";
1087
+ }
1088
+ };
1089
+ export {
1090
+ ActivityLog,
1091
+ IdentityStore,
1092
+ KillSwitch,
1093
+ MockCardProvider,
1094
+ MockEmailProvider,
1095
+ MockPhoneProvider,
1096
+ PolicyEngine,
1097
+ PolicyStore,
1098
+ PolicyViolationError,
1099
+ Terminator,
1100
+ assertTransition,
1101
+ canTransition,
1102
+ createDatabase,
1103
+ createInMemoryDatabase,
1104
+ createPersona,
1105
+ getConfiguredProviders,
1106
+ loadConfig,
1107
+ parseVerificationCode
1108
+ };
1109
+ //# sourceMappingURL=index.js.map