@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.d.ts +356 -0
- package/dist/index.js +1109 -0
- package/dist/index.js.map +1 -0
- package/package.json +39 -0
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
|