@volter/twin-veriff 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/LICENSE +202 -0
- package/README.md +247 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +30 -0
- package/dist/src/index.d.ts +12 -0
- package/dist/src/index.js +119 -0
- package/dist/src/veriff-budget.d.ts +55 -0
- package/dist/src/veriff-budget.js +151 -0
- package/dist/src/veriff-capabilities.d.ts +10 -0
- package/dist/src/veriff-capabilities.js +1079 -0
- package/dist/src/veriff-conformance.d.ts +28 -0
- package/dist/src/veriff-conformance.js +223 -0
- package/dist/src/veriff-connector.d.ts +133 -0
- package/dist/src/veriff-connector.js +372 -0
- package/dist/src/veriff-events.d.ts +62 -0
- package/dist/src/veriff-events.js +82 -0
- package/dist/src/veriff-server.d.ts +27 -0
- package/dist/src/veriff-server.js +101 -0
- package/dist/src/veriff-signature.d.ts +36 -0
- package/dist/src/veriff-signature.js +55 -0
- package/dist/src/veriff-twin.d.ts +104 -0
- package/dist/src/veriff-twin.js +759 -0
- package/package.json +65 -0
- package/src/cli.ts +29 -0
- package/src/index.ts +172 -0
- package/src/veriff-budget.ts +177 -0
- package/src/veriff-capabilities.ts +1157 -0
- package/src/veriff-conformance.ts +264 -0
- package/src/veriff-connector.ts +406 -0
- package/src/veriff-events.ts +117 -0
- package/src/veriff-server.ts +112 -0
- package/src/veriff-signature.ts +86 -0
- package/src/veriff-twin.ts +795 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type VeriffRequest, type VeriffResponse } from './veriff-twin.js';
|
|
2
|
+
export type VeriffConformanceReport = {
|
|
3
|
+
ok: boolean;
|
|
4
|
+
endpointsChecked: number;
|
|
5
|
+
resourceTypesChecked: number;
|
|
6
|
+
routesDispatched: number;
|
|
7
|
+
controlsRejected: number;
|
|
8
|
+
violations: string[];
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* The handler under test. Defaults to the pack's real one; a test injects a WRAPPER to prove this
|
|
12
|
+
* check goes red when a route (or a single method branch) is deleted, and when a route this pack
|
|
13
|
+
* does NOT model starts answering. Injecting a handler cannot weaken the check — the default is the
|
|
14
|
+
* real export, and the mutation gate's saboteur replaces that export, so a dead twin is still
|
|
15
|
+
* measured through this same seam.
|
|
16
|
+
*
|
|
17
|
+
* THREADED AS A PARAMETER, never held in a module-level variable. An earlier revision used a
|
|
18
|
+
* mutable module global reset at the end of the run, and a §9 review reproduced the consequence:
|
|
19
|
+
* two concurrent `checkVeriffConformance` calls — one clean, one carrying a saboteur that deletes a
|
|
20
|
+
* whole route branch — BOTH reported conformant. It failed OPEN, in the one check whose entire job
|
|
21
|
+
* is to prove teeth. Nothing calls it concurrently today, but a `test.concurrent` or a parallel
|
|
22
|
+
* harness would arm it, and a green that depends on nobody trying is not a green.
|
|
23
|
+
*/
|
|
24
|
+
export type VeriffHandler = (req: VeriffRequest) => Promise<VeriffResponse>;
|
|
25
|
+
export declare function checkVeriffConformance(opts?: {
|
|
26
|
+
root?: string;
|
|
27
|
+
handler?: VeriffHandler;
|
|
28
|
+
}): Promise<VeriffConformanceReport>;
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// Veriff conformance (dev-only; lazy-imported by the CLI, NEVER from index.ts/runtime — E2).
|
|
2
|
+
//
|
|
3
|
+
// The bar this check is written to (ADDING_A_TWIN.md §6, "the conformance check needs the same
|
|
4
|
+
// teeth"): ONE REAL REQUEST per claimed endpoint, asserting the OUTCOME a live handler produces —
|
|
5
|
+
// an expected status plus a predicate over the body — against a throwaway root, with the endpoint
|
|
6
|
+
// census a two-way bijection with the handler's own `VERIFF_IMPLEMENTED_ENDPOINTS`.
|
|
7
|
+
//
|
|
8
|
+
// Two failure shapes this deliberately does NOT fall for, both recorded because both are checks
|
|
9
|
+
// that look plausible and cannot fail:
|
|
10
|
+
//
|
|
11
|
+
// • Two constants asserting about each other. A hand-written "implemented" array compared against
|
|
12
|
+
// hand-written expectations in the same file stays green when the entire router is deleted.
|
|
13
|
+
// • Probing, but grading only "not the router's own miss". Sub-handlers fall through to their own
|
|
14
|
+
// 404, so a whole method branch can be deleted while the probe still looks dispatched.
|
|
15
|
+
//
|
|
16
|
+
// So every probe SEEDS the subjects its path params need — against its OWN fresh root, because the
|
|
17
|
+
// inventory contains destructive routes (PATCH submits, DELETE removes, media upload is refused
|
|
18
|
+
// after submit) and a shared root would have the sweep destroy its own fixtures partway through and
|
|
19
|
+
// reinstate exactly the blind spot the seeding exists to remove. A resource-404 is therefore never
|
|
20
|
+
// a legitimate outcome here: every 404 means the route (or its method branch) is gone.
|
|
21
|
+
//
|
|
22
|
+
// NEGATIVE CONTROLS close the other direction. Without them a behaviourally-dead twin that answered
|
|
23
|
+
// a flat 200 to everything would satisfy "every declared route dispatched" perfectly. Each control
|
|
24
|
+
// is a REAL Veriff endpoint this pack deliberately does not model yet (plus one invented path), and
|
|
25
|
+
// each must report as unmodeled.
|
|
26
|
+
//
|
|
27
|
+
// The honest limit: this proves the routing table and each branch's outcome shape are intact. It
|
|
28
|
+
// does NOT prove the payloads match Veriff field-for-field — that is what the capability manifest's
|
|
29
|
+
// failable verifies and the real-transport fidelity test are for.
|
|
30
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
31
|
+
import { tmpdir } from 'node:os';
|
|
32
|
+
import { join } from 'node:path';
|
|
33
|
+
import { handleVeriffTwinRequest, TWIN_API_KEY, TWIN_SHARED_SECRET, VERIFF_IMPLEMENTED_ENDPOINTS, VERIFF_RESOURCE_TYPES } from "./veriff-twin.js";
|
|
34
|
+
import { AUTH_CLIENT_HEADER, HMAC_SIGNATURE_HEADER, veriffSignature } from "./veriff-signature.js";
|
|
35
|
+
/** A monotonic instant source. The kernel dedupes actions by content + MILLISECOND, so re-seeding
|
|
36
|
+
* with one fixed timestamp would have later seeds silently swallowed as replays. */
|
|
37
|
+
let tick = 0;
|
|
38
|
+
function nextInstant() {
|
|
39
|
+
return new Date(Date.UTC(2026, 7, 1, 0, 0, 0, tick++ % 1000)).toISOString();
|
|
40
|
+
}
|
|
41
|
+
const PNG = 'data:image/jpeg;base64,aGVsbG8td29ybGQ=';
|
|
42
|
+
/** Drive one request with correct auth + a correctly-computed signature, so no probe can be masked
|
|
43
|
+
* by a 401 that has nothing to do with routing. */
|
|
44
|
+
async function call(handler, root, method, path, body) {
|
|
45
|
+
const raw = body === undefined ? '' : JSON.stringify(body);
|
|
46
|
+
const bare = path.split('?')[0] ?? path;
|
|
47
|
+
const segments = bare.replace(/^\/v1\/?/, '').replace(/^\/+/, '').split('/');
|
|
48
|
+
const payload = method === 'GET' || method === 'DELETE' ? (segments[1] ?? '') : raw;
|
|
49
|
+
return handler({
|
|
50
|
+
method,
|
|
51
|
+
path,
|
|
52
|
+
...(body === undefined ? {} : { body: raw }),
|
|
53
|
+
headers: { [AUTH_CLIENT_HEADER]: TWIN_API_KEY, [HMAC_SIGNATURE_HEADER]: veriffSignature(payload, TWIN_SHARED_SECRET) },
|
|
54
|
+
root,
|
|
55
|
+
occurredAt: nextInstant(),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/** Build a fresh root carrying every fixture the probes address. */
|
|
59
|
+
async function seed(handler) {
|
|
60
|
+
const root = mkdtempSync(join(tmpdir(), 'veriff-conf-'));
|
|
61
|
+
const open = await call(handler, root, 'POST', '/v1/sessions', { verification: { vendorData: 'conformance', callback: 'https://example.test/hook' } });
|
|
62
|
+
const sessionId = String(open.body?.verification?.id ?? '');
|
|
63
|
+
const up = await call(handler, root, 'POST', `/v1/sessions/${sessionId}/media`, { image: { context: 'document-front', content: PNG } });
|
|
64
|
+
const mediaId = String(up.body?.image?.id ?? '');
|
|
65
|
+
const second = await call(handler, root, 'POST', '/v1/sessions', { verification: { vendorData: 'conformance-decided' } });
|
|
66
|
+
const decidedSessionId = String(second.body?.verification?.id ?? '');
|
|
67
|
+
await call(handler, root, 'PATCH', `/v1/sessions/${decidedSessionId}`, { verification: { status: 'submitted' } });
|
|
68
|
+
const decided = await call(handler, root, 'POST', `/v1/_twin/sessions/${decidedSessionId}/decision`, { status: 'approved', person: { firstName: 'JOHN', lastName: 'SMITH' } });
|
|
69
|
+
const attemptId = String(decided.body?.verification?.attemptId ?? '');
|
|
70
|
+
return { root, sessionId, decidedSessionId, attemptId, mediaId };
|
|
71
|
+
}
|
|
72
|
+
const PROBES = [
|
|
73
|
+
{
|
|
74
|
+
endpoint: 'POST /v1/sessions',
|
|
75
|
+
run: (s, h) => call(h, s.root, 'POST', '/v1/sessions', { verification: { vendorData: 'probe' } }),
|
|
76
|
+
status: 201,
|
|
77
|
+
ok: (b) => b?.status === 'success' && typeof b?.verification?.id === 'string' && b.verification.vendorData === 'probe',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
endpoint: 'PATCH /v1/sessions/{sessionId}',
|
|
81
|
+
run: (s, h) => call(h, s.root, 'PATCH', `/v1/sessions/${s.sessionId}`, { verification: { status: 'submitted' } }),
|
|
82
|
+
status: 200,
|
|
83
|
+
ok: (b) => b?.verification?.status === 'submitted',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
endpoint: 'DELETE /v1/sessions/{sessionId}',
|
|
87
|
+
run: (s, h) => call(h, s.root, 'DELETE', `/v1/sessions/${s.sessionId}`),
|
|
88
|
+
status: 200,
|
|
89
|
+
ok: (b) => b?.status === 'success' && typeof b?.verification?.id === 'string',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
endpoint: 'GET /v1/sessions/{sessionId}/decision',
|
|
93
|
+
run: (s, h) => call(h, s.root, 'GET', `/v1/sessions/${s.decidedSessionId}/decision`),
|
|
94
|
+
status: 200,
|
|
95
|
+
ok: (b) => b?.verification?.status === 'approved' && b.verification.code === 9001,
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
endpoint: 'GET /v1/sessions/{sessionId}/person',
|
|
99
|
+
run: (s, h) => call(h, s.root, 'GET', `/v1/sessions/${s.decidedSessionId}/person`),
|
|
100
|
+
status: 200,
|
|
101
|
+
ok: (b) => b?.person?.firstName === 'JOHN',
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
endpoint: 'GET /v1/sessions/{sessionId}/attempts',
|
|
105
|
+
run: (s, h) => call(h, s.root, 'GET', `/v1/sessions/${s.decidedSessionId}/attempts`),
|
|
106
|
+
status: 200,
|
|
107
|
+
ok: (b) => Array.isArray(b?.verifications) && b.verifications.length === 1 && b.verifications[0].status === 'approved',
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
endpoint: 'GET /v1/sessions/{sessionId}/media',
|
|
111
|
+
run: (s, h) => call(h, s.root, 'GET', `/v1/sessions/${s.sessionId}/media`),
|
|
112
|
+
status: 200,
|
|
113
|
+
ok: (b) => Array.isArray(b?.images) && b.images.length === 1 && b.images[0].context === 'document-front',
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
endpoint: 'POST /v1/sessions/{sessionId}/media',
|
|
117
|
+
run: (s, h) => call(h, s.root, 'POST', `/v1/sessions/${s.sessionId}/media`, { image: { context: 'face', content: PNG } }),
|
|
118
|
+
// 200 — the media endpoint declares exactly one success response and it is not 201 (the
|
|
119
|
+
// status-code table's 201 row is scoped to SESSION creation).
|
|
120
|
+
status: 200,
|
|
121
|
+
ok: (b) => b?.image?.context === 'face' && typeof b?.image?.id === 'string',
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
endpoint: 'GET /v1/attempts/{attemptId}/media',
|
|
125
|
+
run: (s, h) => call(h, s.root, 'GET', `/v1/attempts/${s.attemptId}/media`),
|
|
126
|
+
status: 200,
|
|
127
|
+
ok: (b) => Array.isArray(b?.images) && Array.isArray(b?.videos) && Array.isArray(b?.nfcDocuments),
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
endpoint: 'GET /v1/media/{mediaId}',
|
|
131
|
+
run: (s, h) => call(h, s.root, 'GET', `/v1/media/${s.mediaId}`),
|
|
132
|
+
status: 200,
|
|
133
|
+
ok: (b) => typeof b?.content === 'string' && b.mimetype === 'image/jpeg',
|
|
134
|
+
},
|
|
135
|
+
];
|
|
136
|
+
/**
|
|
137
|
+
* REAL Veriff endpoints this pack does not model yet (each a filed `todo` in the manifest), plus one
|
|
138
|
+
* invented path. Every one must come back as the unmodeled 404 — if any answers, the sweep above is
|
|
139
|
+
* measuring nothing.
|
|
140
|
+
*/
|
|
141
|
+
const CONTROLS = [
|
|
142
|
+
// Veriff has NO `GET /v1/sessions/{id}` — the reference index lists only POST/PATCH/DELETE on
|
|
143
|
+
// that path. Serving one would be an INVENTED op, so it is a control, not a probe.
|
|
144
|
+
{ label: 'GET /v1/sessions/{id} (no such Veriff endpoint)', run: (s, h) => call(h, s.root, 'GET', `/v1/sessions/${s.sessionId}`) },
|
|
145
|
+
{ label: 'GET /v1/sessions/{id}/watchlist-screening', run: (s, h) => call(h, s.root, 'GET', `/v1/sessions/${s.decidedSessionId}/watchlist-screening`) },
|
|
146
|
+
{ label: 'PATCH /v1/sessions/{id}/watchlist-screening', run: (s, h) => call(h, s.root, 'PATCH', `/v1/sessions/${s.decidedSessionId}/watchlist-screening`, { monitorStatus: 'disabled' }) },
|
|
147
|
+
{ label: 'POST /v1/sessions/{id}/collected-data', run: (s, h) => call(h, s.root, 'POST', `/v1/sessions/${s.sessionId}/collected-data`, { providerName: 'x' }) },
|
|
148
|
+
{ label: 'POST /v1/faces/import', run: (s, h) => call(h, s.root, 'POST', '/v1/faces/import', { images: [] }) },
|
|
149
|
+
{ label: 'POST /v1/validate-registry', run: (s, h) => call(h, s.root, 'POST', '/v1/validate-registry', {}) },
|
|
150
|
+
// The reference index spells the same unmodeled endpoint differently; BOTH must be unmodeled,
|
|
151
|
+
// or the twin half-answers a surface it does not implement. Note this one also has to survive
|
|
152
|
+
// the router's session lookup (`/sessions/<x>` with x='validate-registry'), which is exactly the
|
|
153
|
+
// kind of accidental dispatch a control is for.
|
|
154
|
+
{ label: 'POST /v1/sessions/validate-registry (index spelling)', run: (s, h) => call(h, s.root, 'POST', '/v1/sessions/validate-registry', {}) },
|
|
155
|
+
{ label: 'GET /v1/sessions/{id}/decision/ine-registry', run: (s, h) => call(h, s.root, 'GET', `/v1/sessions/${s.decidedSessionId}/decision/ine-registry`) },
|
|
156
|
+
{ label: 'POST /v1/sessions/{id}/TOTALLY-FAKE (invented)', run: (s, h) => call(h, s.root, 'POST', `/v1/sessions/${s.sessionId}/TOTALLY-FAKE`, {}) },
|
|
157
|
+
];
|
|
158
|
+
export async function checkVeriffConformance(opts = {}) {
|
|
159
|
+
void opts.root; // every probe runs against its OWN throwaway root — see the header
|
|
160
|
+
const handler = opts.handler ?? handleVeriffTwinRequest;
|
|
161
|
+
const violations = [];
|
|
162
|
+
// ── census: a two-way bijection with the handler's own inventory ───────────
|
|
163
|
+
const declared = new Set(VERIFF_IMPLEMENTED_ENDPOINTS);
|
|
164
|
+
const probed = new Set(PROBES.map((p) => p.endpoint));
|
|
165
|
+
for (const e of declared)
|
|
166
|
+
if (!probed.has(e))
|
|
167
|
+
violations.push(`endpoint declared in VERIFF_IMPLEMENTED_ENDPOINTS but never probed: ${e}`);
|
|
168
|
+
for (const e of probed)
|
|
169
|
+
if (!declared.has(e))
|
|
170
|
+
violations.push(`probe for an endpoint the handler does not declare: ${e}`);
|
|
171
|
+
if (probed.size !== PROBES.length)
|
|
172
|
+
violations.push('duplicate endpoint in PROBES');
|
|
173
|
+
// ── the live sweep ────────────────────────────────────────────────────────
|
|
174
|
+
let routesDispatched = 0;
|
|
175
|
+
for (const probe of PROBES) {
|
|
176
|
+
const s = await seed(handler);
|
|
177
|
+
try {
|
|
178
|
+
const res = await probe.run(s, handler);
|
|
179
|
+
if (res.status !== probe.status) {
|
|
180
|
+
violations.push(`${probe.endpoint}: expected status ${probe.status}, got ${res.status} (${JSON.stringify(res.body).slice(0, 160)})`);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (!probe.ok(res.body)) {
|
|
184
|
+
violations.push(`${probe.endpoint}: status ${res.status} but the body predicate failed (${JSON.stringify(res.body).slice(0, 160)})`);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
routesDispatched += 1;
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
rmSync(s.root, { recursive: true, force: true });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// ── negative controls ─────────────────────────────────────────────────────
|
|
194
|
+
let controlsRejected = 0;
|
|
195
|
+
const controlSeed = await seed(handler);
|
|
196
|
+
try {
|
|
197
|
+
for (const control of CONTROLS) {
|
|
198
|
+
const res = await control.run(controlSeed, handler);
|
|
199
|
+
const body = res.body;
|
|
200
|
+
if (res.status === 404 && body?.status === 'fail' && body?.code === '1101')
|
|
201
|
+
controlsRejected += 1;
|
|
202
|
+
else
|
|
203
|
+
violations.push(`negative control ANSWERED (this twin does not model it): ${control.label} → ${res.status} ${JSON.stringify(res.body).slice(0, 120)}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
rmSync(controlSeed.root, { recursive: true, force: true });
|
|
208
|
+
}
|
|
209
|
+
// ── structural: every projected resource type is produced by some route ───
|
|
210
|
+
const produced = new Set(['session', 'attempt', 'media', 'delivery']);
|
|
211
|
+
for (const t of VERIFF_RESOURCE_TYPES) {
|
|
212
|
+
if (!produced.has(t))
|
|
213
|
+
violations.push(`resource type ${t} is projected but no modeled route produces it`);
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
ok: violations.length === 0,
|
|
217
|
+
endpointsChecked: PROBES.length,
|
|
218
|
+
resourceTypesChecked: VERIFF_RESOURCE_TYPES.length,
|
|
219
|
+
routesDispatched,
|
|
220
|
+
controlsRejected,
|
|
221
|
+
violations,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { PerformContext, PushOutcome, RemoteExecute, SyncResource, TwinAction } from '@volter/world-core';
|
|
2
|
+
import { VeriffBudget, type VeriffBudgetOptions } from './veriff-budget.js';
|
|
3
|
+
/**
|
|
4
|
+
* The injected real-Veriff boundary. `execute` issues ONE Veriff API call and returns the parsed
|
|
5
|
+
* JSON body (the flat `{ status, ... }` envelope). A real client (a raw fetch wrapper — which is
|
|
6
|
+
* what an integrator writes, since Veriff ships no server SDK) is structurally assignable; tests
|
|
7
|
+
* pass a fake.
|
|
8
|
+
*/
|
|
9
|
+
export type VeriffExecute = (method: 'GET' | 'POST' | 'PATCH' | 'DELETE', path: string, body?: Record<string, unknown>) => Promise<{
|
|
10
|
+
status?: string;
|
|
11
|
+
code?: string;
|
|
12
|
+
message?: string;
|
|
13
|
+
[k: string]: unknown;
|
|
14
|
+
}>;
|
|
15
|
+
/** Construction options for the live executor. `budget` cannot be null and cannot be loosened. */
|
|
16
|
+
export type LiveVeriffOptions = {
|
|
17
|
+
/** Injected `fetch`, so a test can COUNT the requests the guard did or did not let through. */
|
|
18
|
+
fetchImpl?: typeof fetch;
|
|
19
|
+
/** An existing budget to share across executors. Omit and one is constructed. Cannot be null. */
|
|
20
|
+
budget?: VeriffBudget;
|
|
21
|
+
/** Construction options for the default budget (ledger path, clock). Cannot loosen it. */
|
|
22
|
+
budgetOptions?: VeriffBudgetOptions;
|
|
23
|
+
/** The API base. Veriff's URL is ACCOUNT-SPECIFIC (read it off the Customer Portal); the two
|
|
24
|
+
* hosts that appear in official material are `stationapi.veriff.com` (the reference consumer's,
|
|
25
|
+
* and the media samples') and `api.veriff.me` (`@veriff/js-sdk`'s compiled default). */
|
|
26
|
+
base?: string;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* A live executor against the real Veriff Public API v1.
|
|
30
|
+
*
|
|
31
|
+
* THIS IS THE ONE PLACE this pack issues a live Veriff request, and therefore the one place the
|
|
32
|
+
* rate budget has to be enforced. EVERY call is guarded: the budget is charged BEFORE the request
|
|
33
|
+
* goes out (`checkBudget`, which THROWS `VeriffBudgetError` instead of returning when the ceiling or
|
|
34
|
+
* a cooldown says stop) and the response is fed back (`recordCall`) so a 429 becomes a persisted
|
|
35
|
+
* cooldown that makes every later call fail fast WITHOUT touching Veriff. There is deliberately no
|
|
36
|
+
* OPTION to disable the guard, and no value a caller can pass for `budget` that yields an unguarded
|
|
37
|
+
* client. That is NOT immunity from a caller who WANTS one: a fresh `budgetOptions.path` per
|
|
38
|
+
* construction, or an injected clock, restores the allowance, because the seam tests need cannot be
|
|
39
|
+
* denied to a determined caller in the same process. The kernel header states that limit and this
|
|
40
|
+
* does not upgrade it.
|
|
41
|
+
*
|
|
42
|
+
* It also OWNS THE SIGNING, which is the other reason raw calls must not bypass it: Veriff's payload
|
|
43
|
+
* rule is asymmetric (POST/PATCH sign the body, GET/DELETE sign the resource id in the path), and a
|
|
44
|
+
* hand-rolled call that signs the wrong half fails authentication at the vendor for reasons that
|
|
45
|
+
* look nothing like a signing bug.
|
|
46
|
+
*/
|
|
47
|
+
export declare function liveVeriffExecute(apiKey: string, sharedSecret: string, opts?: LiveVeriffOptions): VeriffExecute;
|
|
48
|
+
/**
|
|
49
|
+
* Which bytes `X-HMAC-SIGNATURE` must cover for this call. The vendor's per-endpoint docs refine
|
|
50
|
+
* "GET/DELETE sign the session ID" into "sign the RESOURCE id in the path" — `/attempts/{id}/media`
|
|
51
|
+
* signs the attempt id, `/media/{id}` signs the media id — so this reads the id positionally rather
|
|
52
|
+
* than assuming a session.
|
|
53
|
+
*/
|
|
54
|
+
export declare function signaturePayloadFor(method: string, path: string, serializedBody?: string): string;
|
|
55
|
+
/** Map a decision document → a `session` SyncResource. */
|
|
56
|
+
export declare function mapSessionDecision(v: Record<string, any>): SyncResource;
|
|
57
|
+
/** Map one attempt row (`GET /v1/sessions/{id}/attempts` → `verifications[]`) → a SyncResource. */
|
|
58
|
+
export declare function mapSessionAttempt(sessionId: string, a: Record<string, any>): SyncResource;
|
|
59
|
+
/** Map one media object → a SyncResource. `kind` separates the images/videos/nfcDocuments buckets
|
|
60
|
+
* the media endpoints split their response into. */
|
|
61
|
+
export declare function mapSessionMedia(sessionId: string, m: Record<string, any>, kind: 'image' | 'video' | 'nfc'): SyncResource;
|
|
62
|
+
/** PULL decisions for known session ids into the twin's observed log (idempotent). */
|
|
63
|
+
export declare function pullVeriffDecisions(execute: VeriffExecute, sessionIds: readonly string[], root?: string, occurredAt?: string): Promise<number>;
|
|
64
|
+
/** PULL the attempt history for known session ids (idempotent). */
|
|
65
|
+
export declare function pullVeriffAttempts(execute: VeriffExecute, sessionIds: readonly string[], root?: string, occurredAt?: string): Promise<number>;
|
|
66
|
+
/** PULL media METADATA for known session ids (idempotent). The bytes are not pulled — see the
|
|
67
|
+
* `veriff.media.pull_bytes` todo. */
|
|
68
|
+
export declare function pullVeriffMedia(execute: VeriffExecute, sessionIds: readonly string[], root?: string, occurredAt?: string): Promise<number>;
|
|
69
|
+
/**
|
|
70
|
+
* D7 consumer-facing pull entry point: pull from real Veriff (decisions + attempts + media
|
|
71
|
+
* metadata) and fold into the twin in ONE observation, returning the standard
|
|
72
|
+
* `{ observed, deltasAppended }` result. Idempotent — a re-pull of identical state appends nothing
|
|
73
|
+
* (deltasAppended drops to 0).
|
|
74
|
+
*
|
|
75
|
+
* `sessionIds` is REQUIRED in substance (an empty list pulls nothing and says so by returning
|
|
76
|
+
* zeroes) because Veriff exposes no account-wide enumeration — see the module header.
|
|
77
|
+
*/
|
|
78
|
+
export declare function syncVeriffFromReal(execute: VeriffExecute, opts?: {
|
|
79
|
+
sessionIds?: readonly string[];
|
|
80
|
+
root?: string;
|
|
81
|
+
occurredAt?: string;
|
|
82
|
+
}): Promise<{
|
|
83
|
+
observed: number;
|
|
84
|
+
deltasAppended: number;
|
|
85
|
+
}>;
|
|
86
|
+
/** Translate one pending local action into the real Veriff API call it represents. Returns `null`
|
|
87
|
+
* for anything the vendor gives a client no way to write. */
|
|
88
|
+
export declare function veriffRequestForAction(action: TwinAction): {
|
|
89
|
+
method: 'GET' | 'POST' | 'PATCH' | 'DELETE';
|
|
90
|
+
path: string;
|
|
91
|
+
body?: Record<string, unknown>;
|
|
92
|
+
} | null;
|
|
93
|
+
/**
|
|
94
|
+
* PUSH every pending local action to the real account; confirm each on success.
|
|
95
|
+
*
|
|
96
|
+
* HONEST LIMIT: local ids are not remapped. A session created AND submitted locally produces two
|
|
97
|
+
* pending actions — the create pushes as `POST /sessions` and the real account mints its OWN id,
|
|
98
|
+
* while the submit pushes as `PATCH /sessions/{LOCAL id}`, which that account has never seen. So a
|
|
99
|
+
* create converges; a create-then-submit pair does not, until `veriff.connector.push_id_remapping`
|
|
100
|
+
* is built. Saying so beats a push that silently half-lands.
|
|
101
|
+
*/
|
|
102
|
+
export declare function pushPendingVeriffActions(execute: VeriffExecute, root?: string, occurredAt?: string): Promise<number>;
|
|
103
|
+
/** A `VeriffExecute` over the kernel's executor. At a REAL boundary the kernel sets the sealed
|
|
104
|
+
* credential over these headers (executor.ts); at the twin's own wire any credential is one. */
|
|
105
|
+
export declare function veriffExecuteOver(execute: RemoteExecute): VeriffExecute;
|
|
106
|
+
/**
|
|
107
|
+
* The refresh adapter.
|
|
108
|
+
*
|
|
109
|
+
* Veriff exposes NO account-wide enumeration — a session exists because a client created it and is
|
|
110
|
+
* then addressed by the id that client was handed. So "refresh this world from the real vendor"
|
|
111
|
+
* means re-reading the sessions the WORLD already holds: its decisions, its attempt history, and
|
|
112
|
+
* its media metadata. This is not a gap standing in for a list endpoint; it is the shape of the
|
|
113
|
+
* vendor, and the module header says so.
|
|
114
|
+
*/
|
|
115
|
+
export declare function syncVeriffFromRemote(execute: RemoteExecute, opts?: {
|
|
116
|
+
root?: string;
|
|
117
|
+
origin?: string;
|
|
118
|
+
occurredAt?: string;
|
|
119
|
+
sessionIds?: readonly string[];
|
|
120
|
+
}): Promise<{
|
|
121
|
+
observed: number;
|
|
122
|
+
deltasAppended: number;
|
|
123
|
+
}>;
|
|
124
|
+
/**
|
|
125
|
+
* The perform adapter.
|
|
126
|
+
*
|
|
127
|
+
* This is what closes the pack's standing `push_id_remapping` gap. Under protocol 1 a local session
|
|
128
|
+
* created AND submitted produced two pending actions: the create pushed as `POST /sessions` and the
|
|
129
|
+
* real account minted its OWN id, while the submit pushed as `PATCH /sessions/{LOCAL id}` — an id
|
|
130
|
+
* that account had never seen. Protocol 2's confirmation carries `vendorSubjectId`, so the kernel
|
|
131
|
+
* records what Veriff called the session and the submit addresses the REAL id.
|
|
132
|
+
*/
|
|
133
|
+
export declare function performVeriffAction(execute: RemoteExecute, action: TwinAction, ctx: PerformContext): Promise<PushOutcome>;
|