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