@splitin/verification-cli 0.1.0-beta.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/cli.cjs ADDED
@@ -0,0 +1,949 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ var url = require('url');
5
+ var fs = require('fs');
6
+ var http = require('http');
7
+ var path = require('path');
8
+ var verificationAdapterSdk = require('@splitin/verification-adapter-sdk');
9
+
10
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
11
+ // src/argv.ts
12
+ var COMMANDS_WITH_SUBCOMMAND = /* @__PURE__ */ new Set(["config", "db", "provider", "registry", "release"]);
13
+ function parseArgv(argv) {
14
+ const tokens = argv.slice(2);
15
+ const flags = {};
16
+ const rest = [];
17
+ for (let index = 0; index < tokens.length; index += 1) {
18
+ const token = tokens[index];
19
+ if (token === "--") {
20
+ rest.push(...tokens.slice(index + 1));
21
+ break;
22
+ }
23
+ if (token.startsWith("--")) {
24
+ const eq = token.indexOf("=");
25
+ if (eq !== -1) {
26
+ flags[token.slice(2, eq)] = token.slice(eq + 1);
27
+ continue;
28
+ }
29
+ const key = token.slice(2);
30
+ const next = tokens[index + 1];
31
+ if (next && !next.startsWith("-")) {
32
+ flags[key] = next;
33
+ index += 1;
34
+ } else {
35
+ flags[key] = true;
36
+ }
37
+ continue;
38
+ }
39
+ if (token.startsWith("-") && token.length === 2) {
40
+ const key = token.slice(1);
41
+ const next = tokens[index + 1];
42
+ if (next && !next.startsWith("-")) {
43
+ flags[key] = next;
44
+ index += 1;
45
+ } else {
46
+ flags[key] = true;
47
+ }
48
+ continue;
49
+ }
50
+ rest.push(token);
51
+ }
52
+ const command = rest[0] ?? "help";
53
+ let subcommand = null;
54
+ let positionals = rest.slice(1);
55
+ if (COMMANDS_WITH_SUBCOMMAND.has(command) && positionals[0] && !positionals[0].startsWith("-")) {
56
+ subcommand = positionals[0];
57
+ positionals = positionals.slice(1);
58
+ }
59
+ if (flags.help === true || flags.h === true) {
60
+ return { command: command === "help" ? "help" : command, subcommand, positionals, flags: { ...flags, help: true } };
61
+ }
62
+ return { command, subcommand, positionals, flags };
63
+ }
64
+ function flagString(flags, name, fallback = "") {
65
+ const value = flags[name];
66
+ return typeof value === "string" ? value : fallback;
67
+ }
68
+ function flagBoolean(flags, name) {
69
+ return flags[name] === true || flags[name] === "true" || flags[name] === "1";
70
+ }
71
+
72
+ // src/redact.ts
73
+ var SECRET_PREFIX = /\b((?:sk_|rk_|whsec_)[A-Za-z0-9_-]+)/g;
74
+ var TOKEN_ASSIGNMENT = /(\b(?:token|access_token|refresh_token|id_token|api[_-]?key|client[_-]?secret|private[_-]?key|signing[_-]?secret)\b\s*[:=]\s*)(["']?)([^\s"',}]+)\2/gi;
75
+ var BEARER = /\bBearer\s+[A-Za-z0-9._~+/-]+=*/gi;
76
+ var JWT = /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9._~+/-]+=*/g;
77
+ function redactSecrets(value) {
78
+ return value.replace(BEARER, "Bearer ***").replace(JWT, "***").replace(SECRET_PREFIX, (_, match) => `${match.slice(0, match.indexOf("_") + 1)}***`).replace(TOKEN_ASSIGNMENT, (_, prefix, quote) => `${prefix}${quote}***${quote}`);
79
+ }
80
+ function redactValue(value) {
81
+ if (typeof value === "string") return redactSecrets(value);
82
+ if (Array.isArray(value)) return value.map(redactValue);
83
+ if (value && typeof value === "object") {
84
+ const entries = Object.entries(value).map(([key, nested]) => {
85
+ if (isSecretKey(key) && typeof nested === "string" && nested.length > 0) {
86
+ return [key, redactSecrets(nested) === nested ? "***" : redactSecrets(nested)];
87
+ }
88
+ return [key, redactValue(nested)];
89
+ });
90
+ return Object.fromEntries(entries);
91
+ }
92
+ return value;
93
+ }
94
+ function isSecretKey(key) {
95
+ return /secret|token|password|authorization|api[_-]?key|private[_-]?key|signing/i.test(key);
96
+ }
97
+
98
+ // src/config.ts
99
+ var CONFIG_FILE_NAME = "verification.config.json";
100
+ var CLI_VERSION = "0.1.0-beta.0";
101
+ function defaultConfig() {
102
+ return {
103
+ contractVersion: verificationAdapterSdk.VERIFICATION_ADAPTER_CONTRACT_VERSION,
104
+ engineCompatibility: verificationAdapterSdk.ENGINE_CONTRACT_VERSION,
105
+ environment: "sandbox",
106
+ productionEnabled: false,
107
+ productionRoutesEnabled: false,
108
+ database: {
109
+ url: "",
110
+ schema: "verification",
111
+ migrationsDirectory: "migrations/verification"
112
+ },
113
+ webhooks: {
114
+ publicBaseUrl: "",
115
+ toleranceSeconds: 300
116
+ },
117
+ browser: {
118
+ publishableKeys: {}
119
+ },
120
+ routing: {
121
+ defaultProvider: "test_fake",
122
+ rules: [{ packageCode: "human_idv", provider: "test_fake", countryCode: "US" }]
123
+ },
124
+ providers: {
125
+ test_fake: disabledProvider("sandbox", "fake-1"),
126
+ stripe_identity: disabledProvider("sandbox", "2024-06-20"),
127
+ persona: disabledProvider("sandbox", "2023-01-05"),
128
+ plaid_idv: disabledProvider("sandbox", "2020-09-14")
129
+ }
130
+ };
131
+ }
132
+ function disabledProvider(environment, apiVersion) {
133
+ return {
134
+ enabled: false,
135
+ environment,
136
+ secretKey: "",
137
+ webhookSecret: "",
138
+ publishableKey: "",
139
+ apiVersion
140
+ };
141
+ }
142
+ function configPath(cwd) {
143
+ return path.join(cwd, CONFIG_FILE_NAME);
144
+ }
145
+ function loadConfig(cwd) {
146
+ const path = configPath(cwd);
147
+ if (!fs.existsSync(path)) {
148
+ throw new Error(`Missing ${CONFIG_FILE_NAME}. Run \`splitin-verification init\` first.`);
149
+ }
150
+ const parsed = JSON.parse(fs.readFileSync(path, "utf8"));
151
+ return { ...defaultConfig(), ...parsed, providers: { ...defaultConfig().providers, ...parsed.providers } };
152
+ }
153
+ function writeConfig(cwd, config) {
154
+ const path$1 = configPath(cwd);
155
+ fs.mkdirSync(path.dirname(path$1), { recursive: true });
156
+ fs.writeFileSync(path$1, `${JSON.stringify(config, null, 2)}
157
+ `, "utf8");
158
+ return path$1;
159
+ }
160
+ function safeConfigView(config) {
161
+ return redactValue(config);
162
+ }
163
+ function validateCompatibility(config) {
164
+ const issues = [];
165
+ if (config.contractVersion !== verificationAdapterSdk.VERIFICATION_ADAPTER_CONTRACT_VERSION) {
166
+ issues.push({
167
+ code: "contract_version",
168
+ message: `Config contract ${config.contractVersion} does not match SDK ${verificationAdapterSdk.VERIFICATION_ADAPTER_CONTRACT_VERSION}.`
169
+ });
170
+ }
171
+ if (!verificationAdapterSdk.majorsCompatible(config.engineCompatibility, verificationAdapterSdk.ENGINE_CONTRACT_VERSION)) {
172
+ issues.push({
173
+ code: "engine_incompatible",
174
+ message: `Engine compatibility ${config.engineCompatibility} is not compatible with ${verificationAdapterSdk.ENGINE_CONTRACT_VERSION}.`
175
+ });
176
+ }
177
+ if (config.productionEnabled || config.productionRoutesEnabled || config.environment === "production") {
178
+ issues.push({
179
+ code: "production_disabled",
180
+ message: "Production routes and production environment are disabled until sandbox certification."
181
+ });
182
+ }
183
+ return issues;
184
+ }
185
+ function validateProviderCredentials(config) {
186
+ const issues = [];
187
+ for (const [provider, credentials] of Object.entries(config.providers)) {
188
+ if (!credentials.enabled) continue;
189
+ if (credentials.environment === "production" || looksLikeLiveSecret(credentials.secretKey)) {
190
+ issues.push({
191
+ provider,
192
+ code: "production_credential_blocked",
193
+ message: `Provider "${provider}" has a live or production credential. The CLI never creates billable production attempts.`
194
+ });
195
+ }
196
+ if (credentials.secretKey && !looksLikeSandboxSecret(credentials.secretKey) && !looksLikeLiveSecret(credentials.secretKey)) {
197
+ issues.push({
198
+ provider,
199
+ code: "unrecognized_secret_prefix",
200
+ message: `Provider "${provider}" secret does not use a recognized sandbox prefix. Values are never printed.`
201
+ });
202
+ }
203
+ if (credentials.webhookSecret && !credentials.webhookSecret.startsWith("whsec_") && provider !== "test_fake") {
204
+ issues.push({
205
+ provider,
206
+ code: "webhook_secret_shape",
207
+ message: `Provider "${provider}" webhook secret should use a sandbox webhook prefix. The value is not printed.`
208
+ });
209
+ }
210
+ if (!credentials.apiVersion.trim()) {
211
+ issues.push({
212
+ provider,
213
+ code: "api_version_missing",
214
+ message: `Provider "${provider}" is missing a pinned API version.`
215
+ });
216
+ }
217
+ }
218
+ return issues;
219
+ }
220
+ function looksLikeLiveSecret(value) {
221
+ return /^(?:sk_live_|rk_live_)/.test(value) || value.startsWith("sk_") && !value.includes("test");
222
+ }
223
+ function looksLikeSandboxSecret(value) {
224
+ return /^(?:sk_test_|rk_test_|test_|sandbox_)/.test(value) || value.startsWith("whsec_test");
225
+ }
226
+ function resolveCwd(flags) {
227
+ const cwd = flags.cwd ?? flags.C;
228
+ return path.resolve(typeof cwd === "string" && cwd.length > 0 ? cwd : process.cwd());
229
+ }
230
+ function writeText(path$1, contents) {
231
+ fs.mkdirSync(path.dirname(path$1), { recursive: true });
232
+ fs.writeFileSync(path$1, contents, "utf8");
233
+ }
234
+ function envExample() {
235
+ return `# SplitIn verification adapter SDK \u2014 development placeholders only.
236
+ # Production is disabled. Never commit real secrets.
237
+
238
+ VERIFICATION_ENVIRONMENT=sandbox
239
+ VERIFICATION_PRODUCTION_ENABLED=false
240
+ VERIFICATION_PRODUCTION_ROUTES_ENABLED=false
241
+
242
+ DATABASE_URL=
243
+ VERIFICATION_DATABASE_SCHEMA=verification
244
+
245
+ STRIPE_IDENTITY_SECRET_KEY=
246
+ STRIPE_IDENTITY_WEBHOOK_SECRET=
247
+ STRIPE_IDENTITY_PUBLISHABLE_KEY=
248
+
249
+ PERSONA_API_KEY=
250
+ PERSONA_WEBHOOK_SECRET=
251
+
252
+ PLAID_CLIENT_ID=
253
+ PLAID_SECRET=
254
+ PLAID_WEBHOOK_SECRET=
255
+
256
+ VERIFICATION_WEBHOOK_PUBLIC_BASE_URL=
257
+ VERIFICATION_BROWSER_PUBLISHABLE_KEY=
258
+ `;
259
+ }
260
+ var MIGRATION_FILES = [
261
+ { version: "001", name: "init" },
262
+ { version: "002", name: "seed" },
263
+ { version: "003", name: "retention" }
264
+ ];
265
+ function migrationsDirectory() {
266
+ const here = path.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href))));
267
+ const candidates = [
268
+ path.join(here, "..", "migrations"),
269
+ path.join(here, "..", "..", "verification-postgres", "migrations")
270
+ ];
271
+ for (const directory of candidates) {
272
+ if (fs.existsSync(path.join(directory, "001_init.sql"))) return directory;
273
+ }
274
+ throw new Error("Bundled verification SQL migrations were not found.");
275
+ }
276
+ function loadBundledMigrations() {
277
+ const directory = migrationsDirectory();
278
+ return MIGRATION_FILES.map((item) => ({
279
+ version: item.version,
280
+ name: item.name,
281
+ up: fs.readFileSync(path.join(directory, `${item.version}_${item.name}.sql`), "utf8"),
282
+ down: fs.readFileSync(path.join(directory, `${item.version}_${item.name}.down.sql`), "utf8")
283
+ }));
284
+ }
285
+ var bundledMigrations = loadBundledMigrations();
286
+ var STATE_FILE = ".verification/migrations-state.json";
287
+ function statePath(cwd) {
288
+ return path.join(cwd, STATE_FILE);
289
+ }
290
+ function readState(cwd) {
291
+ const path = statePath(cwd);
292
+ if (!fs.existsSync(path)) return { applied: [] };
293
+ return JSON.parse(fs.readFileSync(path, "utf8"));
294
+ }
295
+ function writeState(cwd, state) {
296
+ fs.mkdirSync(path.join(cwd, ".verification"), { recursive: true });
297
+ fs.writeFileSync(statePath(cwd), `${JSON.stringify(state, null, 2)}
298
+ `, "utf8");
299
+ }
300
+ function writeMigrationFiles(cwd, config) {
301
+ const directory = path.join(cwd, config.database.migrationsDirectory);
302
+ fs.mkdirSync(directory, { recursive: true });
303
+ for (const migration of bundledMigrations) {
304
+ fs.writeFileSync(path.join(directory, `${migration.version}_${migration.name}.up.sql`), migration.up, "utf8");
305
+ fs.writeFileSync(path.join(directory, `${migration.version}_${migration.name}.down.sql`), migration.down, "utf8");
306
+ }
307
+ return directory;
308
+ }
309
+ function migrateUp(cwd, config) {
310
+ writeMigrationFiles(cwd, config);
311
+ const state = readState(cwd);
312
+ const applied = [];
313
+ const sql = [];
314
+ for (const migration of bundledMigrations) {
315
+ if (state.applied.includes(migration.version)) continue;
316
+ sql.push(migration.up);
317
+ state.applied.push(migration.version);
318
+ applied.push(`${migration.version}_${migration.name}`);
319
+ }
320
+ writeState(cwd, state);
321
+ return { applied, sql };
322
+ }
323
+ function migrateDown(cwd, config) {
324
+ writeMigrationFiles(cwd, config);
325
+ const state = readState(cwd);
326
+ const version = state.applied.at(-1);
327
+ if (!version) return { rolledBack: null, sql: [] };
328
+ const migration = bundledMigrations.find((item) => item.version === version);
329
+ if (!migration) return { rolledBack: null, sql: [] };
330
+ state.applied = state.applied.filter((item) => item !== version);
331
+ writeState(cwd, state);
332
+ return { rolledBack: `${migration.version}_${migration.name}`, sql: [migration.down] };
333
+ }
334
+ function appliedMigrations(cwd) {
335
+ return readState(cwd).applied;
336
+ }
337
+ function listedMigrationFiles(cwd, config) {
338
+ const directory = path.join(cwd, config.database.migrationsDirectory);
339
+ if (!fs.existsSync(directory)) return [];
340
+ return fs.readdirSync(directory).filter((name) => name.endsWith(".sql")).sort();
341
+ }
342
+ var SCAFFOLD_PACKAGE = "com.example.employee_check";
343
+ var SCAFFOLD_PROVIDER = "example_employee_check";
344
+ function scaffoldProvider(cwd, directory = `adapters/${SCAFFOLD_PROVIDER}`) {
345
+ const root = path.join(cwd, directory);
346
+ writeText(path.join(root, "package.json"), `${JSON.stringify({
347
+ name: `@example/${SCAFFOLD_PROVIDER}`,
348
+ version: "0.1.0-beta.0",
349
+ private: true,
350
+ type: "module",
351
+ license: "MIT",
352
+ description: `Fourth-party verification adapter stub for ${SCAFFOLD_PACKAGE}.`,
353
+ main: "./src/index.ts",
354
+ dependencies: {
355
+ "@splitin/verification-adapter-sdk": "0.1.0-beta.0"
356
+ }
357
+ }, null, 2)}
358
+ `);
359
+ writeText(path.join(root, "README.md"), `# Example employee-check adapter
360
+
361
+ Fourth-party adapter stub. Package code: \`${SCAFFOLD_PACKAGE}\`.
362
+
363
+ This adapter is sandbox-only. Production routes stay disabled until you run
364
+ \`splitin-verification provider conformance\` against a non-billable fake or
365
+ sandbox fixture.
366
+
367
+ \`\`\`ts
368
+ import { ExampleEmployeeCheckAdapter } from './src/index.ts';
369
+ \`\`\`
370
+ `);
371
+ writeText(path.join(root, "src/manifest.ts"), `import {
372
+ defineProviderManifest,
373
+ emptyConfigurationSchema,
374
+ VERIFICATION_ADAPTER_CONTRACT_VERSION,
375
+ } from '@splitin/verification-adapter-sdk';
376
+
377
+ export const exampleEmployeeCheckManifest = defineProviderManifest({
378
+ contractVersion: VERIFICATION_ADAPTER_CONTRACT_VERSION,
379
+ adapterVersion: '1.0.0',
380
+ engineCompatibility: '1.0.0',
381
+ provider: '${SCAFFOLD_PROVIDER}',
382
+ displayName: 'Example Employee Check',
383
+ description: 'Fourth-party adapter stub. Replace the retrieve mapping before production.',
384
+ supportedPackages: ['${SCAFFOLD_PACKAGE}'],
385
+ supportedCountries: ['US'],
386
+ environments: ['sandbox'],
387
+ capabilities: {
388
+ presentations: ['hosted'],
389
+ canResume: true,
390
+ canRetry: true,
391
+ canCancel: true,
392
+ canRedact: true,
393
+ },
394
+ launcherKeys: ['hosted'],
395
+ launchPresentations: ['hosted'],
396
+ configurationSchemaVersion: 'urn:example:employee-check:config:v1',
397
+ configurationSchema: emptyConfigurationSchema,
398
+ webhook: { protocol: 'none', eventFamilies: ['employee_check'] },
399
+ dataPolicy: {
400
+ classifications: ['normalized_status'],
401
+ prohibitedPersistence: ['raw_webhook', 'launch_secret', 'document', 'selfie'],
402
+ rawPayloadPersistence: false,
403
+ browserSecretPersistence: false,
404
+ governmentIdentifierPersistence: false,
405
+ },
406
+ retry: { sameResourceWhenResumable: true, newAttemptAfterTerminal: true },
407
+ cancellation: { supported: true, terminal: true },
408
+ redaction: { supported: true, asynchronous: false },
409
+ apiHosts: ['127.0.0.1'],
410
+ testedApiVersions: ['example-1'],
411
+ });
412
+ `);
413
+ writeText(path.join(root, "src/index.ts"), `import type {
414
+ NormalizedProviderEvent,
415
+ NormalizedProviderSnapshot,
416
+ ProviderAttemptCommand,
417
+ ProviderAttemptResult,
418
+ ProviderLaunchEnvelope,
419
+ ProviderOperationResult,
420
+ ProviderRedactionCommand,
421
+ ProviderRedactionResult,
422
+ ProviderResourceCommand,
423
+ ProviderRetryCommand,
424
+ ProviderRuntimeContext,
425
+ VerificationAdapterV1,
426
+ VerifiedWebhookEnvelope,
427
+ } from '@splitin/verification-adapter-sdk';
428
+ import {
429
+ ProviderError,
430
+ VERIFICATION_ADAPTER_CONTRACT_VERSION,
431
+ createDefaultRuntime,
432
+ } from '@splitin/verification-adapter-sdk';
433
+ import { exampleEmployeeCheckManifest } from './manifest.ts';
434
+
435
+ export class ExampleEmployeeCheckAdapter implements VerificationAdapterV1<Record<string, never>> {
436
+ readonly contractVersion = VERIFICATION_ADAPTER_CONTRACT_VERSION;
437
+ readonly manifest = exampleEmployeeCheckManifest;
438
+ readonly provider = '${SCAFFOLD_PROVIDER}';
439
+ readonly environment = 'sandbox' as const;
440
+ readonly runtime: ProviderRuntimeContext<Record<string, never>>;
441
+
442
+ constructor(runtime?: ProviderRuntimeContext<Record<string, never>>) {
443
+ this.runtime = runtime ?? createDefaultRuntime('sandbox', {}, { allowedHosts: ['127.0.0.1'] });
444
+ }
445
+
446
+ validateConfiguration(): void {
447
+ if (this.runtime.environment !== 'sandbox') {
448
+ throw new ProviderError('INVALID_CONFIGURATION', 'The example adapter is sandbox-only.');
449
+ }
450
+ }
451
+
452
+ async createAttempt(command: ProviderAttemptCommand): Promise<ProviderAttemptResult> {
453
+ if (command.packageCode !== '${SCAFFOLD_PACKAGE}') {
454
+ throw new ProviderError('UNSUPPORTED_CAPABILITY', 'This adapter only serves ${SCAFFOLD_PACKAGE}.');
455
+ }
456
+ const providerResourceId = \`eec_\${command.attemptId.replace(/[^a-zA-Z0-9]/g, '')}\`;
457
+ return {
458
+ attemptId: command.attemptId,
459
+ providerResourceId,
460
+ providerStatus: 'pending_user_input',
461
+ canonicalStatus: 'pending_user_input',
462
+ launch: this.launch(command.attemptId),
463
+ };
464
+ }
465
+
466
+ async resumeAttempt(command: ProviderResourceCommand): Promise<ProviderLaunchEnvelope> {
467
+ return this.launch(command.attemptId);
468
+ }
469
+
470
+ async retrieveAttempt(command: ProviderResourceCommand): Promise<NormalizedProviderSnapshot> {
471
+ return {
472
+ providerResourceId: command.providerResourceId,
473
+ providerStatus: 'pending_user_input',
474
+ canonicalStatus: 'pending_user_input',
475
+ occurredAt: this.runtime.now().toISOString(),
476
+ normalizedReasonCodes: [],
477
+ safeMetadata: { package: '${SCAFFOLD_PACKAGE}' },
478
+ };
479
+ }
480
+
481
+ async retryAttempt(command: ProviderRetryCommand): Promise<ProviderAttemptResult> {
482
+ return this.createAttempt({ ...command, attemptId: \`\${command.attemptId}_retry\` });
483
+ }
484
+
485
+ async cancelAttempt(): Promise<ProviderOperationResult> {
486
+ return { accepted: true, providerStatus: 'canceled', canonicalStatus: 'canceled' };
487
+ }
488
+
489
+ async redactSubject(_command: ProviderRedactionCommand): Promise<ProviderRedactionResult> {
490
+ return { completed: true, retryable: false, disposition: 'redacted' };
491
+ }
492
+
493
+ async verifyWebhook(): Promise<VerifiedWebhookEnvelope> {
494
+ throw new ProviderError('UNSUPPORTED_CAPABILITY', 'This stub does not receive webhooks.', {
495
+ safeCode: 'webhooks_not_supported',
496
+ });
497
+ }
498
+
499
+ async normalizeWebhook(_input: VerifiedWebhookEnvelope): Promise<NormalizedProviderEvent> {
500
+ throw new ProviderError('UNSUPPORTED_CAPABILITY', 'This stub does not receive webhooks.', {
501
+ safeCode: 'webhooks_not_supported',
502
+ });
503
+ }
504
+
505
+ private launch(attemptId: string): ProviderLaunchEnvelope {
506
+ return {
507
+ attemptId,
508
+ canonicalStatus: 'pending_user_input',
509
+ launcherKey: 'hosted',
510
+ presentation: 'hosted',
511
+ hostedUrl: 'https://127.0.0.1/example-employee-check',
512
+ continuationReference: \`cont_\${attemptId}\`,
513
+ };
514
+ }
515
+ }
516
+ `);
517
+ return root;
518
+ }
519
+
520
+ // src/commands.ts
521
+ async function dispatch(parsed) {
522
+ if (parsed.flags.help === true || parsed.command === "help" || parsed.command === "--help") {
523
+ return ok(usage());
524
+ }
525
+ if (parsed.command === "version" || parsed.flags.version === true || parsed.flags.v === true) {
526
+ return ok(`${CLI_VERSION}
527
+ `);
528
+ }
529
+ switch (parsed.command) {
530
+ case "init":
531
+ return cmdInit(parsed);
532
+ case "config":
533
+ return parsed.subcommand === "validate" ? cmdConfigValidate(parsed) : fail(`Unknown config command. Use \`config validate\`.
534
+ `);
535
+ case "doctor":
536
+ return cmdDoctor(parsed);
537
+ case "db":
538
+ if (parsed.subcommand === "migrate") return cmdDbMigrate(parsed);
539
+ if (parsed.subcommand === "rollback") return cmdDbRollback(parsed);
540
+ return fail("Unknown db command. Use `db migrate` or `db rollback`.\n");
541
+ case "provider":
542
+ if (parsed.subcommand === "scaffold") return cmdProviderScaffold(parsed);
543
+ if (parsed.subcommand === "conformance") return cmdProviderConformance(parsed);
544
+ return fail("Unknown provider command. Use `provider scaffold` or `provider conformance`.\n");
545
+ case "registry":
546
+ return parsed.subcommand === "generate" ? cmdRegistryGenerate(parsed) : fail("Unknown registry command. Use `registry generate`.\n");
547
+ case "dev":
548
+ return cmdDev(parsed);
549
+ case "release":
550
+ return parsed.subcommand === "verify" ? cmdReleaseVerify(parsed) : fail("Unknown release command. Use `release verify`.\n");
551
+ default:
552
+ return fail(`Unknown command "${parsed.command}".
553
+
554
+ ${usage()}`);
555
+ }
556
+ }
557
+ function cmdInit(parsed) {
558
+ const cwd = resolveCwd(parsed.flags);
559
+ const force = flagBoolean(parsed.flags, "force");
560
+ const config = defaultConfig();
561
+ const path$1 = path.join(cwd, "verification.config.json");
562
+ if (fs.existsSync(path$1) && !force) {
563
+ return fail(`Refusing to overwrite ${path$1}. Pass --force to replace it.
564
+ `);
565
+ }
566
+ writeConfig(cwd, config);
567
+ writeText(path.join(cwd, ".env.example"), envExample());
568
+ writeText(path.join(cwd, "src/generated/.gitkeep"), "");
569
+ const stdout = [
570
+ "Initialized a disabled-by-default development configuration.",
571
+ `Wrote ${path$1}`,
572
+ "Wrote .env.example (empty placeholders only).",
573
+ "productionEnabled=false productionRoutesEnabled=false environment=sandbox",
574
+ "Fill sandbox credentials locally. The CLI redacts provider secrets and tokens.",
575
+ "Next: splitin-verification config validate && splitin-verification doctor",
576
+ ""
577
+ ].join("\n");
578
+ return ok(stdout);
579
+ }
580
+ function cmdConfigValidate(parsed) {
581
+ const cwd = resolveCwd(parsed.flags);
582
+ const config = loadConfig(cwd);
583
+ const compatibility = validateCompatibility(config);
584
+ const credentials = validateProviderCredentials(config);
585
+ const view = JSON.stringify(safeConfigView(config), null, 2);
586
+ const lines = [
587
+ `contractVersion=${config.contractVersion} engineCompatibility=${config.engineCompatibility}`,
588
+ `productionEnabled=${config.productionEnabled} productionRoutesEnabled=${config.productionRoutesEnabled}`,
589
+ "Credential check is shape-only. No Identity session, Persona inquiry, or Plaid IDV attempt is created.",
590
+ view
591
+ ];
592
+ if (compatibility.length || credentials.length) {
593
+ for (const issue of compatibility) lines.push(`ERROR ${issue.code}: ${issue.message}`);
594
+ for (const issue of credentials) lines.push(`ERROR ${issue.provider}/${issue.code}: ${issue.message}`);
595
+ return { exitCode: 1, stdout: redactSecrets(`${lines.join("\n")}
596
+ `), stderr: "" };
597
+ }
598
+ lines.push("Configuration is valid for sandbox development.");
599
+ return ok(`${lines.join("\n")}
600
+ `);
601
+ }
602
+ function cmdDoctor(parsed) {
603
+ const cwd = resolveCwd(parsed.flags);
604
+ const config = loadConfig(cwd);
605
+ const checks = [
606
+ diagnoseDatabase(config, cwd),
607
+ diagnoseWebhooks(config),
608
+ diagnoseProviders(config),
609
+ diagnoseBrowserKeys(config),
610
+ diagnoseRouting(config)
611
+ ];
612
+ const lines = ["Verification adapter doctor", ...checks.map((check) => `${check.ok ? "ok" : "fail"} ${check.name}: ${check.detail}`)];
613
+ const failed = checks.some((check) => !check.ok);
614
+ return { exitCode: failed ? 1 : 0, stdout: redactSecrets(`${lines.join("\n")}
615
+ `), stderr: "" };
616
+ }
617
+ function diagnoseDatabase(config, cwd) {
618
+ const applied = appliedMigrations(cwd);
619
+ if (!config.database.schema) {
620
+ return { ok: false, name: "database", detail: "Schema name is empty." };
621
+ }
622
+ if (!config.database.url) {
623
+ return {
624
+ ok: true,
625
+ name: "database",
626
+ detail: `No DATABASE_URL. Schema "${config.database.schema}" is configured. Applied local revisions: ${applied.join(", ") || "none"}.`
627
+ };
628
+ }
629
+ try {
630
+ const url = new URL(config.database.url);
631
+ return {
632
+ ok: url.protocol === "postgres:" || url.protocol === "postgresql:",
633
+ name: "database",
634
+ detail: `Host ${url.hostname} schema ${config.database.schema}. Doctor does not open a live connection or run SQL.`
635
+ };
636
+ } catch {
637
+ return { ok: false, name: "database", detail: "DATABASE_URL is not a valid URL. The value is not printed." };
638
+ }
639
+ }
640
+ function diagnoseWebhooks(config) {
641
+ if (!config.webhooks.publicBaseUrl) {
642
+ return { ok: true, name: "webhook", detail: "Public webhook base URL is unset (expected for local sandbox)." };
643
+ }
644
+ try {
645
+ const url = new URL(config.webhooks.publicBaseUrl);
646
+ const okHttps = url.protocol === "https:" || url.hostname === "127.0.0.1" || url.hostname === "localhost";
647
+ return {
648
+ ok: okHttps,
649
+ name: "webhook",
650
+ detail: okHttps ? `Endpoint origin ${url.origin} tolerance=${config.webhooks.toleranceSeconds}s.` : "Webhook public URL must be HTTPS or loopback."
651
+ };
652
+ } catch {
653
+ return { ok: false, name: "webhook", detail: "Webhook public URL is malformed." };
654
+ }
655
+ }
656
+ function diagnoseProviders(config) {
657
+ const credentials = validateProviderCredentials(config);
658
+ const enabled = Object.entries(config.providers).filter(([, value]) => value.enabled).map(([name]) => name);
659
+ if (credentials.length) {
660
+ return { ok: false, name: "provider", detail: credentials.map((issue) => `${issue.provider}:${issue.code}`).join(", ") };
661
+ }
662
+ return {
663
+ ok: true,
664
+ name: "provider",
665
+ detail: enabled.length ? `Enabled sandbox providers: ${enabled.join(", ")}. No billable production attempts.` : "No third-party provider enabled. Fake provider remains the default route."
666
+ };
667
+ }
668
+ function diagnoseBrowserKeys(config) {
669
+ const keys = Object.keys(config.browser.publishableKeys);
670
+ return {
671
+ ok: true,
672
+ name: "browser-key",
673
+ detail: keys.length ? `Publishable keys present for ${keys.join(", ")}. Values redacted.` : "No browser publishable keys configured (sandbox fake launcher does not need them)."
674
+ };
675
+ }
676
+ function diagnoseRouting(config) {
677
+ if (!config.routing.defaultProvider) {
678
+ return { ok: false, name: "routing", detail: "Default provider is missing." };
679
+ }
680
+ const known = config.providers[config.routing.defaultProvider];
681
+ if (!known && config.routing.defaultProvider !== "test_fake") {
682
+ return { ok: false, name: "routing", detail: `Default provider "${config.routing.defaultProvider}" is not in the config.` };
683
+ }
684
+ return {
685
+ ok: true,
686
+ name: "routing",
687
+ detail: `Default ${config.routing.defaultProvider}; ${config.routing.rules.length} package rule(s). Production routing is disabled.`
688
+ };
689
+ }
690
+ function cmdDbMigrate(parsed) {
691
+ const cwd = resolveCwd(parsed.flags);
692
+ const config = loadConfig(cwd);
693
+ const result = migrateUp(cwd, config);
694
+ const files = listedMigrationFiles(cwd, config);
695
+ return ok([
696
+ result.applied.length ? `Applied ${result.applied.join(", ")}.` : "No pending migrations.",
697
+ `Wrote SQL templates under ${config.database.migrationsDirectory} (${files.length} files).`,
698
+ "SQL is not executed against production. Apply with your own postgres client when ready.",
699
+ ""
700
+ ].join("\n"));
701
+ }
702
+ function cmdDbRollback(parsed) {
703
+ const cwd = resolveCwd(parsed.flags);
704
+ const config = loadConfig(cwd);
705
+ const result = migrateDown(cwd, config);
706
+ return ok(result.rolledBack ? `Rolled back ${result.rolledBack}. SQL was not executed against a live database.
707
+ ` : "No applied migrations to roll back.\n");
708
+ }
709
+ function cmdProviderScaffold(parsed) {
710
+ const cwd = resolveCwd(parsed.flags);
711
+ const directory = flagString(parsed.flags, "out") || parsed.positionals[0];
712
+ const root = scaffoldProvider(cwd, directory);
713
+ return ok([
714
+ `Wrote fourth-party adapter stub at ${root}`,
715
+ `Custom package: ${SCAFFOLD_PACKAGE}`,
716
+ "The stub is sandbox-only and does not create billable provider attempts.",
717
+ ""
718
+ ].join("\n"));
719
+ }
720
+ async function cmdProviderConformance(parsed) {
721
+ const command = {
722
+ attemptId: "att_conformance_cli",
723
+ subjectReference: "sub_opaque_conformance",
724
+ packageCode: "human_idv",
725
+ countryCode: "US",
726
+ idempotencyKey: "idem_conformance_cli",
727
+ configurationRevision: "cfg_cli"
728
+ };
729
+ const modulePath = flagString(parsed.flags, "module");
730
+ const lines = [];
731
+ try {
732
+ if (!modulePath) {
733
+ const adapter = verificationAdapterSdk.createFakeAdapterForScenario("input_required");
734
+ const results2 = await verificationAdapterSdk.runAdapterConformance(adapter, command);
735
+ lines.push(
736
+ "Running @splitin/verification-adapter-sdk runAdapterConformance against the sandbox fake adapter.",
737
+ "This does not call Stripe, Persona, or Plaid and cannot create billable attempts.",
738
+ ...formatConformance(results2)
739
+ );
740
+ const failed2 = results2.filter((result) => !result.passed);
741
+ return { exitCode: failed2.length ? 1 : 0, stdout: redactSecrets(`${lines.join("\n")}
742
+ `), stderr: "" };
743
+ }
744
+ const loaded = await loadConformanceModule(modulePath, resolveCwd(parsed.flags));
745
+ const results = await verificationAdapterSdk.runAdapterConformance(loaded.adapter, command);
746
+ lines.push(
747
+ `Running runAdapterConformance against module ${modulePath}.`,
748
+ "Adapter output is redacted. Secrets, tokens, and credential material are never printed.",
749
+ ...formatConformance(results)
750
+ );
751
+ if (loaded.factory) {
752
+ const scenarios = await verificationAdapterSdk.runAdapterConformanceScenarios(loaded.factory, command);
753
+ lines.push("Running runAdapterConformanceScenarios for the exported factory.", ...formatConformance(scenarios));
754
+ results.push(...scenarios);
755
+ }
756
+ const failed = results.filter((result) => !result.passed);
757
+ return { exitCode: failed.length ? 1 : 0, stdout: redactSecrets(`${lines.join("\n")}
758
+ `), stderr: "" };
759
+ } catch (error) {
760
+ const detail = error instanceof Error ? error.message : "Unknown conformance module failure.";
761
+ return fail(`Unable to run provider conformance. ${detail}
762
+ `);
763
+ }
764
+ }
765
+ function formatConformance(results) {
766
+ return results.map((result) => `${result.passed ? "pass" : "fail"} ${result.name}${result.detail ? ` ${result.detail}` : ""}`);
767
+ }
768
+ async function loadConformanceModule(modulePath, cwd) {
769
+ const absolute = path.isAbsolute(modulePath) ? modulePath : path.resolve(cwd, modulePath);
770
+ const imported = await import(url.pathToFileURL(absolute).href);
771
+ const factoryCandidate = pickFunction(
772
+ imported.createAdapterForScenario,
773
+ imported.createAdapter,
774
+ imported.default
775
+ );
776
+ const isScenarioFactory = typeof factoryCandidate === "function" && factoryCandidate.length >= 1;
777
+ const adapter = isAdapter(imported.default) ? imported.default : isAdapter(imported.createAdapter) ? imported.createAdapter : typeof factoryCandidate === "function" ? await factoryCandidate(isScenarioFactory ? "input_required" : void 0) : null;
778
+ if (!isAdapter(adapter)) {
779
+ throw new Error("Module must export a default adapter, createAdapter, or createAdapterForScenario.");
780
+ }
781
+ return {
782
+ adapter,
783
+ factory: isScenarioFactory ? factoryCandidate : null
784
+ };
785
+ }
786
+ function pickFunction(...candidates) {
787
+ for (const candidate of candidates) {
788
+ if (typeof candidate === "function") return candidate;
789
+ }
790
+ return null;
791
+ }
792
+ function isAdapter(value) {
793
+ return Boolean(value) && typeof value === "object" && typeof value.createAttempt === "function" && typeof value.retrieveAttempt === "function";
794
+ }
795
+ function cmdRegistryGenerate(parsed) {
796
+ const cwd = resolveCwd(parsed.flags);
797
+ const config = loadConfig(cwd);
798
+ const out = flagString(parsed.flags, "out", "src/generated/verification-registry.ts");
799
+ const contents = `/* Generated by splitin-verification registry generate. Do not edit. */
800
+ export const verificationAdapterContractVersion = ${JSON.stringify(verificationAdapterSdk.VERIFICATION_ADAPTER_CONTRACT_VERSION)} as const;
801
+ export const verificationEngineContractVersion = ${JSON.stringify(verificationAdapterSdk.ENGINE_CONTRACT_VERSION)} as const;
802
+ export const verificationCliVersion = ${JSON.stringify(CLI_VERSION)} as const;
803
+ export const standardPackageCodes = ${JSON.stringify(verificationAdapterSdk.STANDARD_PACKAGE_CODES, null, 2)} as const;
804
+ export const standardWebhookProtocols = ${JSON.stringify(verificationAdapterSdk.STANDARD_WEBHOOK_PROTOCOLS, null, 2)} as const;
805
+ export const canonicalStatuses = ${JSON.stringify(verificationAdapterSdk.CANONICAL_STATUSES, null, 2)} as const;
806
+ export const configuredProviders = ${JSON.stringify(Object.keys(config.providers), null, 2)} as const;
807
+ export const productionRoutesEnabled = false;
808
+ export type StandardPackageCode = typeof standardPackageCodes[number];
809
+ export type ConfiguredProvider = typeof configuredProviders[number];
810
+ `;
811
+ writeText(path.join(cwd, out), contents);
812
+ return ok(`Wrote typed registry ${out}. productionRoutesEnabled=false.
813
+ `);
814
+ }
815
+ function cmdDev(parsed) {
816
+ const cwd = resolveCwd(parsed.flags);
817
+ const config = loadConfig(cwd);
818
+ if (config.productionEnabled || config.productionRoutesEnabled) {
819
+ return fail("Refusing to start because production routes are enabled. Set productionEnabled=false.\n");
820
+ }
821
+ const port = Number(flagString(parsed.flags, "port", "8787")) || 8787;
822
+ if (flagBoolean(parsed.flags, "print-only")) {
823
+ return ok(`Sandbox dev listener would bind 127.0.0.1:${port}. Production routes stay disabled.
824
+ `);
825
+ }
826
+ const server = http.createServer((request, response) => {
827
+ void handleDevRequest(request, response);
828
+ });
829
+ server.listen(port, "127.0.0.1");
830
+ return ok(`Sandbox verification listener on http://127.0.0.1:${port}
831
+ Production routes disabled. GET /health, POST /sandbox/attempts.
832
+ `);
833
+ }
834
+ async function handleDevRequest(request, response) {
835
+ const url = new URL(request.url ?? "/", "http://127.0.0.1");
836
+ if (url.pathname === "/health") {
837
+ json(response, 200, { ok: true, environment: "sandbox", productionRoutesEnabled: false });
838
+ return;
839
+ }
840
+ if (url.pathname.startsWith("/v1/") || url.pathname.startsWith("/production/")) {
841
+ json(response, 403, { error: "production_routes_disabled" });
842
+ return;
843
+ }
844
+ if (request.method === "POST" && url.pathname === "/sandbox/attempts") {
845
+ const adapter = verificationAdapterSdk.createFakeAdapterForScenario("input_required");
846
+ const created = await adapter.createAttempt({
847
+ attemptId: "att_dev_local",
848
+ subjectReference: "sub_opaque_dev",
849
+ packageCode: "human_idv",
850
+ countryCode: "US",
851
+ idempotencyKey: "idem_dev_local",
852
+ configurationRevision: "cfg_dev"
853
+ });
854
+ json(response, 201, redactValue({
855
+ attemptId: created.attemptId,
856
+ providerResourceId: created.providerResourceId,
857
+ canonicalStatus: created.canonicalStatus,
858
+ launcherKey: created.launch.launcherKey
859
+ }));
860
+ return;
861
+ }
862
+ json(response, 404, { error: "not_found" });
863
+ }
864
+ function json(response, status, body) {
865
+ response.writeHead(status, { "content-type": "application/json" });
866
+ response.end(`${JSON.stringify(body)}
867
+ `);
868
+ }
869
+ function cmdReleaseVerify(parsed) {
870
+ const cwd = resolveCwd(parsed.flags);
871
+ const lines = [
872
+ `cli=${CLI_VERSION} contract=${verificationAdapterSdk.VERIFICATION_ADAPTER_CONTRACT_VERSION} engine=${verificationAdapterSdk.ENGINE_CONTRACT_VERSION}`,
873
+ "npm trusted publishing must use OIDC (id-token: write). Do not embed an npm token.",
874
+ "Publish 0.1.0-beta.0 first. Promote to 1.0.0 only after sandbox certification.",
875
+ "productionEnabled must remain false in distributed examples."
876
+ ];
877
+ if (fs.existsSync(path.join(cwd, "verification.config.json"))) {
878
+ const config = loadConfig(cwd);
879
+ if (config.productionEnabled || config.productionRoutesEnabled) {
880
+ return fail(`${lines.join("\n")}
881
+ ERROR production_disabled: release artifacts must keep production routes off.
882
+ `);
883
+ }
884
+ }
885
+ lines.push("Release preflight passed.");
886
+ return ok(`${lines.join("\n")}
887
+ `);
888
+ }
889
+ function usage() {
890
+ return `splitin-verification ${CLI_VERSION}
891
+
892
+ Usage: splitin-verification <command> [subcommand] [options]
893
+
894
+ Commands:
895
+ init Write disabled-by-default sandbox configuration
896
+ config validate Validate contract/API versions and credential shape
897
+ doctor Diagnose database, webhook, provider, browser-key, routing
898
+ db migrate Write and record SQL migrations (does not apply to prod)
899
+ db rollback Roll back the last recorded local migration
900
+ provider scaffold Write a fourth-party adapter stub (${SCAFFOLD_PACKAGE})
901
+ provider conformance Run @splitin/verification-adapter-sdk runAdapterConformance
902
+ registry generate Write a typed provider/package registry
903
+ dev Bind a sandbox-only listener (production routes disabled)
904
+ release verify Check versions, provenance policy, and production flags
905
+
906
+ Global options:
907
+ --cwd <dir> Working directory
908
+ --module <path> Adapter module for \`provider conformance\` (default: fake)
909
+ --help Show this help
910
+ `;
911
+ }
912
+ function ok(stdout) {
913
+ return { exitCode: 0, stdout: redactSecrets(stdout), stderr: "" };
914
+ }
915
+ function fail(stderr) {
916
+ return { exitCode: 1, stdout: "", stderr: redactSecrets(stderr) };
917
+ }
918
+
919
+ // src/cli.ts
920
+ async function run(argv = process.argv) {
921
+ const parsed = parseArgv(argv);
922
+ return dispatch(parsed);
923
+ }
924
+ var isMain = Boolean(process.argv[1]) && (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href)) === url.pathToFileURL(process.argv[1]).href;
925
+ if (isMain) {
926
+ void run().then((result) => {
927
+ if (result.stdout) process.stdout.write(result.stdout);
928
+ if (result.stderr) process.stderr.write(result.stderr);
929
+ const parsed = parseArgv(process.argv);
930
+ if (parsed.command === "dev" && parsed.flags["print-only"] !== true && result.exitCode === 0) {
931
+ return;
932
+ }
933
+ process.exit(result.exitCode);
934
+ }).catch((error) => {
935
+ const message = error instanceof Error ? error.message : "Unknown CLI failure.";
936
+ process.stderr.write(`${message}
937
+ `);
938
+ process.exit(1);
939
+ });
940
+ }
941
+
942
+ exports.defaultConfig = defaultConfig;
943
+ exports.dispatch = dispatch;
944
+ exports.parseArgv = parseArgv;
945
+ exports.redactSecrets = redactSecrets;
946
+ exports.redactValue = redactValue;
947
+ exports.run = run;
948
+ //# sourceMappingURL=cli.cjs.map
949
+ //# sourceMappingURL=cli.cjs.map