@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
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@volter/twin-veriff",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Local Veriff identity-verification twin — a faithful, stateful local stationapi.veriff.com/v1 your KYC integration talks to unmodified: sessions, the decision lifecycle, HMAC-signed decision/event webhooks, attempts, media and watchlist screening. Built on @volter/world-core.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"twin",
|
|
7
|
+
"local",
|
|
8
|
+
"mock",
|
|
9
|
+
"mirror",
|
|
10
|
+
"simulator",
|
|
11
|
+
"fixtures",
|
|
12
|
+
"testing",
|
|
13
|
+
"api",
|
|
14
|
+
"veriff",
|
|
15
|
+
"identity-verification",
|
|
16
|
+
"kyc",
|
|
17
|
+
"aml",
|
|
18
|
+
"webhooks"
|
|
19
|
+
],
|
|
20
|
+
"author": "Volter (https://github.com/volter-ai)",
|
|
21
|
+
"license": "Apache-2.0",
|
|
22
|
+
"files": [
|
|
23
|
+
"src",
|
|
24
|
+
"README.md",
|
|
25
|
+
"LICENSE",
|
|
26
|
+
"!**/*.test.ts",
|
|
27
|
+
"dist"
|
|
28
|
+
],
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/volter-ai/twin.git",
|
|
32
|
+
"directory": "packages/twin/veriff"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/volter-ai/twin/tree/main/packages/twin/veriff#readme",
|
|
35
|
+
"type": "module",
|
|
36
|
+
"exports": {
|
|
37
|
+
".": {
|
|
38
|
+
"types": "./dist/src/index.d.ts",
|
|
39
|
+
"default": "./dist/src/index.js"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"bin": {
|
|
43
|
+
"world-veriff": "dist/src/cli.js"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"test": "bun test src/*.test.ts",
|
|
47
|
+
"typecheck": "tsc --noEmit",
|
|
48
|
+
"build": "node ../../../scripts/publish/build.mjs",
|
|
49
|
+
"prepack": "node ../../../scripts/publish/prepare-publish.mjs prepack",
|
|
50
|
+
"postpack": "node ../../../scripts/publish/prepare-publish.mjs postpack"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"@volter/world-core": "2.0.0"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@volter/world-core": "2.0.0",
|
|
57
|
+
"@volter/world-tooling": "0.1.0",
|
|
58
|
+
"@types/bun": "^1.2.20",
|
|
59
|
+
"@types/node": "^24.0.0",
|
|
60
|
+
"typescript": "^5.9.0"
|
|
61
|
+
},
|
|
62
|
+
"engines": {
|
|
63
|
+
"node": ">=22.3"
|
|
64
|
+
}
|
|
65
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { keepProcessAlive } from '@volter/world-core/lifecycle';
|
|
3
|
+
// world-veriff CLI: serve the Veriff Station API twin, or run conformance.
|
|
4
|
+
//
|
|
5
|
+
// No `mirror` command: Veriff is an API-first vendor (the integrator's work is code — create a
|
|
6
|
+
// session, embed the hosted flow, receive the decision webhook), so this pack ships no React
|
|
7
|
+
// mirror. See README.md `## Coverage` → `### No UI mirror`.
|
|
8
|
+
import { hasFlag, optionValue } from '@volter/world-core/args';
|
|
9
|
+
import { createVeriffTwinServer } from './veriff-server.ts';
|
|
10
|
+
|
|
11
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
12
|
+
const port = Number(optionValue(rest, '--port', '0')) || undefined;
|
|
13
|
+
const root = optionValue(rest, '--root') || undefined;
|
|
14
|
+
const sharedSecret = optionValue(rest, '--shared-secret') || undefined;
|
|
15
|
+
const readOnly = hasFlag(rest, '--read-only'); // a twin accepts writes unless started read-only
|
|
16
|
+
|
|
17
|
+
if (cmd === 'serve') {
|
|
18
|
+
const s = await createVeriffTwinServer({ readOnly, ...(root ? { root } : {}), ...(port ? { port } : {}), ...(sharedSecret ? { sharedSecret } : {}) });
|
|
19
|
+
process.stdout.write(`veriff twin (Station API v1)${readOnly ? ' [read-only]' : ''} at http://127.0.0.1:${s.port}\n`);
|
|
20
|
+
await keepProcessAlive();
|
|
21
|
+
} else if (cmd === 'conformance') {
|
|
22
|
+
// dev-only; lazy so the bin runs without @volter/world-tooling in the runtime graph
|
|
23
|
+
const { checkVeriffConformance } = await import('./veriff-conformance.ts');
|
|
24
|
+
const report = await checkVeriffConformance({ ...(root ? { root } : {}) });
|
|
25
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
26
|
+
if (!report.ok) process.exitCode = 1;
|
|
27
|
+
} else {
|
|
28
|
+
process.stdout.write('Usage: world-veriff serve|conformance [--port N] [--root DIR] [--shared-secret S] [--read-only]\n');
|
|
29
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// @volter/twin-veriff — the Veriff identity-verification twin (one vendor, one package), built on
|
|
2
|
+
// the shared @volter/world-core kernel. REST transport over Veriff's Station API
|
|
3
|
+
// (`https://stationapi.veriff.com/v1`), a stateful session→attempt→decision lifecycle, REAL
|
|
4
|
+
// `X-HMAC-SIGNATURE` request signing and HMAC-signed decision/event webhooks.
|
|
5
|
+
//
|
|
6
|
+
// Veriff publishes NO server-side SDK — a real integration hand-rolls a `fetch` client (the
|
|
7
|
+
// reference consumer, dub, does exactly that in `apps/web/lib/veriff/client.ts`) — so this pack
|
|
8
|
+
// declares no vendor devDependency and proves fidelity over real-transport `fetch` instead.
|
|
9
|
+
//
|
|
10
|
+
// NO React mirror: Veriff is an API-first vendor for the party that integrates it (create a
|
|
11
|
+
// session, embed the hosted flow, receive the decision webhook, read the decision). See
|
|
12
|
+
// README.md `## Coverage` → `### No UI mirror`. Coverage = API + connector.
|
|
13
|
+
// (Conformance/capability tooling lives in @volter/world-tooling, a dev dependency.)
|
|
14
|
+
export {
|
|
15
|
+
handleVeriffTwinRequest,
|
|
16
|
+
TWIN_API_KEY,
|
|
17
|
+
TWIN_SHARED_SECRET,
|
|
18
|
+
VERIFF_DECISION_CODES,
|
|
19
|
+
VERIFF_DECISION_STATUSES,
|
|
20
|
+
VERIFF_ERROR_CODES,
|
|
21
|
+
VERIFF_ERROR_MESSAGES,
|
|
22
|
+
VERIFF_EVENT_CODES,
|
|
23
|
+
VERIFF_IMPLEMENTED_ENDPOINTS,
|
|
24
|
+
VERIFF_RESOURCE_TYPES,
|
|
25
|
+
VERIFF_SESSION_STATUSES,
|
|
26
|
+
} from './veriff-twin.ts';
|
|
27
|
+
export type { VeriffRequest, VeriffResponse, VeriffSessionStatus, VeriffDecisionStatus } from './veriff-twin.ts';
|
|
28
|
+
export { createVeriffTwinFetch, createVeriffTwinServer, veriffResponseHeaders, type VeriffTwinFetchOptions } from './veriff-server.ts';
|
|
29
|
+
export {
|
|
30
|
+
AUTH_CLIENT_HEADER,
|
|
31
|
+
HMAC_SIGNATURE_HEADER,
|
|
32
|
+
signaturesMatch,
|
|
33
|
+
veriffSignature,
|
|
34
|
+
veriffSignatureBytes,
|
|
35
|
+
verifyVeriffSignature,
|
|
36
|
+
} from './veriff-signature.ts';
|
|
37
|
+
export {
|
|
38
|
+
buildSignedDelivery,
|
|
39
|
+
emitVeriffWebhook,
|
|
40
|
+
VERIFF_EVENT_ACTIONS,
|
|
41
|
+
verifyWebhook,
|
|
42
|
+
VeriffWebhookVerificationError,
|
|
43
|
+
} from './veriff-events.ts';
|
|
44
|
+
export type { VeriffDecisionPayload, VeriffEventAction, VeriffEventPayload, VeriffWebhookDelivery, VeriffWebhookPayload } from './veriff-events.ts';
|
|
45
|
+
export {
|
|
46
|
+
liveVeriffExecute,
|
|
47
|
+
mapSessionDecision,
|
|
48
|
+
mapSessionAttempt,
|
|
49
|
+
mapSessionMedia,
|
|
50
|
+
pullVeriffAttempts,
|
|
51
|
+
pullVeriffDecisions,
|
|
52
|
+
pullVeriffMedia,
|
|
53
|
+
pushPendingVeriffActions,
|
|
54
|
+
signaturePayloadFor,
|
|
55
|
+
syncVeriffFromReal,
|
|
56
|
+
veriffRequestForAction,
|
|
57
|
+
} from './veriff-connector.ts';
|
|
58
|
+
export type { LiveVeriffOptions, VeriffExecute } from './veriff-connector.ts';
|
|
59
|
+
// The client-side rate budget — the fail-closed backstop `liveVeriffExecute` routes every live
|
|
60
|
+
// request through. The MECHANISM is the kernel's shared, vendor-agnostic `RateBudget`; what lives
|
|
61
|
+
// here is Veriff's DECLARATION (window/ceiling/per-endpoint weights) plus the vendor-bound
|
|
62
|
+
// bindings. Exported so an operator can inspect spend (`snapshot`) and so a caller can catch
|
|
63
|
+
// `VeriffBudgetError` by type; there is deliberately no export that disables the guard.
|
|
64
|
+
export {
|
|
65
|
+
VERIFF_BUDGET_CEILING,
|
|
66
|
+
VERIFF_BUDGET_MAX_RETRY_AFTER_S,
|
|
67
|
+
VERIFF_BUDGET_WINDOW_MS,
|
|
68
|
+
VERIFF_CALL_WEIGHTS,
|
|
69
|
+
VERIFF_RATE_BUDGET,
|
|
70
|
+
VeriffBudget,
|
|
71
|
+
VeriffBudgetError,
|
|
72
|
+
veriffBudgetPath,
|
|
73
|
+
veriffCallWeight,
|
|
74
|
+
} from './veriff-budget.ts';
|
|
75
|
+
export type { VeriffBudgetErrorKind, VeriffBudgetOptions, VeriffBudgetReservation, VeriffBudgetSnapshot } from './veriff-budget.ts';
|
|
76
|
+
|
|
77
|
+
// Registry descriptor: the pack self-describes so tooling can discover it.
|
|
78
|
+
import { registerPack, type TwinPack } from '@volter/world-core';
|
|
79
|
+
import { signaturePayloadFor } from './veriff-connector.ts';
|
|
80
|
+
import { HMAC_SIGNATURE_HEADER } from './veriff-signature.ts';
|
|
81
|
+
import { performVeriffAction, syncVeriffFromRemote } from './veriff-connector.ts';
|
|
82
|
+
import { VERIFF_RATE_BUDGET as RATE_BUDGET } from './veriff-budget.ts';
|
|
83
|
+
export const pack: TwinPack = {
|
|
84
|
+
// PROTOCOL 2 (docs/contributing/architecture.md#protocol-2-the-pack-is-a-plugin): the pack is a plugin — its wire, its tree, and its half of
|
|
85
|
+
// the real state system. Moved 2026-09-08.
|
|
86
|
+
protocol: '2',
|
|
87
|
+
// Veriff DOES publish webhooks (the decision callback is the whole integration), so a world that
|
|
88
|
+
// wires them hears about a decision as it lands; the poll is the backstop for one that has not.
|
|
89
|
+
refresh: { every: '5m', webhook: true, onDemand: { atMost: '30s' } },
|
|
90
|
+
stateSystem: { perform: performVeriffAction, refresh: syncVeriffFromRemote },
|
|
91
|
+
// The round trip is a session create — Veriff's own write, and the vendor mints a fresh id for
|
|
92
|
+
// every one, so a second send on a branch is a second session rather than a collision.
|
|
93
|
+
roundTrip: { method: 'POST', path: '/v1/sessions', body: { verification: { vendorData: 'round-trip' } }, headers: { 'x-auth-client': 'round-trip' } },
|
|
94
|
+
// Veriff signs EVERY call but one: `X-HMAC-SIGNATURE` over the request body for a write, and over
|
|
95
|
+
// the resource id in the path for a GET/DELETE. `signaturePayloadFor` is the pack's own pure
|
|
96
|
+
// statement of which bytes those are — it takes no secret, so the secret stays in the kernel
|
|
97
|
+
// (docs/concepts/the-model.md#the-rules, rule 5, "the secret never crossing into pack code").
|
|
98
|
+
//
|
|
99
|
+
// TWO THINGS THIS LINE HAS TO GET RIGHT, both measured against the pack's live executor:
|
|
100
|
+
// • `POST /sessions` is the one endpoint the vendor EXEMPTS from signing — there is no session
|
|
101
|
+
// to sign for yet — so the canonical answers `null` and the request goes out unsigned.
|
|
102
|
+
// • `signaturePayloadFor` reads the resource id POSITIONALLY out of a path with no version
|
|
103
|
+
// prefix (`/sessions/{id}` → the id). The kernel sends `/v1/...`, so the prefix comes off
|
|
104
|
+
// first; signing `sessions` instead of the session id would verify against nothing.
|
|
105
|
+
auth: {
|
|
106
|
+
in: 'signature',
|
|
107
|
+
algorithm: 'hmac-sha256',
|
|
108
|
+
encoding: 'hex',
|
|
109
|
+
header: HMAC_SIGNATURE_HEADER,
|
|
110
|
+
canonical: ({ method, path, body }) => {
|
|
111
|
+
const bare = path.replace(/^\/v1(?=\/|$)/, '');
|
|
112
|
+
if (method === 'POST' && bare === '/sessions') return null;
|
|
113
|
+
return signaturePayloadFor(method, bare, body);
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
parityOrigin: 'http://twin',
|
|
117
|
+
|
|
118
|
+
vendor: 'veriff',
|
|
119
|
+
// The SAME object veriff-budget.ts declares at module load — one source of truth, so registering
|
|
120
|
+
// the pack and importing the connector can never arm two different ceilings.
|
|
121
|
+
rateBudget: RATE_BUDGET,
|
|
122
|
+
transport: 'rest',
|
|
123
|
+
archetype: 'crud',
|
|
124
|
+
bin: 'world-veriff',
|
|
125
|
+
resources: ['session', 'attempt', 'media', 'delivery'],
|
|
126
|
+
specSource: 'docs.veriff.com / devdocs.veriff.com (hand-authored from the published Station API reference), cross-checked against the real consumer dub (apps/web/lib/veriff/*, app/api/veriff/webhook/*)',
|
|
127
|
+
description: 'Veriff Station API twin — sessions, the created→submitted→approved/declined decision lifecycle, HMAC-signed request auth and decision/event webhooks, attempts and media. Watchlist/AML screening is a filed todo area, deliberately unmodeled.',
|
|
128
|
+
// Adoption + interception, moved off the central maps unchanged (descriptor-first back-migration, adding-a-twin.md §3,
|
|
129
|
+
// 2026-08-31).
|
|
130
|
+
//
|
|
131
|
+
// Veriff publishes NO server-side SDK — a real integration hand-rolls a `fetch` client against
|
|
132
|
+
// the Station API (the reference consumer, dub, does exactly that in
|
|
133
|
+
// apps/web/lib/veriff/client.ts) — so the only npm package that is genuinely a CLIENT OF THE
|
|
134
|
+
// MODELED SURFACE is `@veriff/js-sdk`: v2.0.0's `createSession` XHRs `POST {host}/v1/sessions`
|
|
135
|
+
// straight from the browser with `x-auth-client`, and its `host` is a constructor option, so
|
|
136
|
+
// pointing it at a twin is configuration. Its siblings are covered by the `@veriff/` SCOPE
|
|
137
|
+
// rather than listed as sdks, because they are NOT API clients: `@veriff/incontext-sdk` makes
|
|
138
|
+
// ZERO network calls of its own (verified against the published v2.5.0 bundle: no fetch, no
|
|
139
|
+
// XMLHttpRequest, no hardcoded URL) — it iframes the session `url` the API already returned and
|
|
140
|
+
// listens for postMessage — and the react-native/Cordova packages launch the native capture
|
|
141
|
+
// flow. Listing them as sdks would claim the twin intercepts traffic they never send; leaving
|
|
142
|
+
// them out of the scope entirely would report the exact package the reference consumer installs
|
|
143
|
+
// as unknown-sdk.
|
|
144
|
+
//
|
|
145
|
+
// VERIFF_API_KEY is the credential that stems here — the name the reference consumer uses
|
|
146
|
+
// verbatim (dub's apps/web/.env.example). Its sibling VERIFF_SHARED_SECRET deliberately does
|
|
147
|
+
// NOT: it stems to `veriffshared` under the broad suffix list (`_SECRET` splits before
|
|
148
|
+
// `SHARED`), and it matches no STRICT suffix at all, so it never reaches the lookup and cannot
|
|
149
|
+
// raise a false unknown-vendor alarm either.
|
|
150
|
+
adoption: {
|
|
151
|
+
// Veriff ships no Python SDK - its clients are the JS `@veriff/js-sdk` and the raw REST API.
|
|
152
|
+
pypi: [],
|
|
153
|
+
sdks: ['@veriff/js-sdk'], scopes: ['@veriff/'], envStems: ['VERIFF'],
|
|
154
|
+
},
|
|
155
|
+
// The API URL is ACCOUNT-SPECIFIC by design — Veriff's reference tells you to read your BaseURL
|
|
156
|
+
// off the Customer Portal and its OpenAPI `servers` entry is the placeholder
|
|
157
|
+
// `https://example-base-url` — so BOTH hosts that appear in official material are routed: the
|
|
158
|
+
// reference consumer and the media code samples use `stationapi.veriff.com`, while
|
|
159
|
+
// `@veriff/js-sdk` v2.0.0 compiles in `api.veriff.me` as its default. Deliberately EXACT
|
|
160
|
+
// matches, not a `.veriff.com`/`.veriff.me` suffix: `magic.veriff.me` and `alchemy.veriff.com`
|
|
161
|
+
// serve the HOSTED end-user capture flow (Veriff's own first-party web app; the twin returns a
|
|
162
|
+
// real session `url` rather than rendering it), `cdn.veriff.me` serves the browser SDK bundles,
|
|
163
|
+
// and `feedback.api.veriff.com` is the separate Fraud API with its own VRF-* header scheme that
|
|
164
|
+
// this pack does not model. Routing any of those into the twin would intercept requests it
|
|
165
|
+
// cannot answer.
|
|
166
|
+
hosts: [{ host: 'stationapi.veriff.com' }, { host: 'api.veriff.me' }],
|
|
167
|
+
// A Veriff client calls stationapi.veriff.com/v1/… — the dev proxy forwards '/v1/' to the twin
|
|
168
|
+
// and strips the absolute host so calls come back same-origin.
|
|
169
|
+
browserRouting: { apiPathPrefix: '/v1/', loaderHost: 'https://stationapi.veriff.com' },
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
registerPack(pack);
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// Veriff's CLIENT-SIDE RATE BUDGET — the pack's DECLARATION (the numbers) plus the thin typed
|
|
2
|
+
// bindings `liveVeriffExecute` uses. The MECHANISM — the durable token-keyed ledger, the rolling
|
|
3
|
+
// window, reserve-under-lock, the `Retry-After`/429 cooldown, fail-CLOSED on a corrupt ledger —
|
|
4
|
+
// lives ONCE in the vendor-agnostic kernel (`@volter/world-core` → `rateBudget.ts`). Read that module's
|
|
5
|
+
// header for the full rationale AND for the honest list of what the guard does not guarantee (an
|
|
6
|
+
// injected clock or ledger path still defeats it — it guards carelessness, not malice). This module
|
|
7
|
+
// is modeled on calcom-budget.ts, the reference "the pack builds its own HTTP client" shape.
|
|
8
|
+
//
|
|
9
|
+
// ── WHY THIS EXISTS ─────────────────────────────────────────────────────────────────────────
|
|
10
|
+
// A real ~4.5-DAY vendor lockout (Figma, 2026-07-25) happened because raw API calls were made
|
|
11
|
+
// outside the pack's connector — no cache, no batching, no ceiling. Discipline only binds the code
|
|
12
|
+
// that follows it; a BUDGET binds the code that does not. For an identity vendor the stakes are not
|
|
13
|
+
// only the lockout: every session created is a BILLED verification against a real end user.
|
|
14
|
+
//
|
|
15
|
+
// ── HOW THE CEILING WAS CHOSEN ──────────────────────────────────────────────────────────────
|
|
16
|
+
// Veriff DOES publish scalar limits — unusually — but not in the API reference. Two, both read
|
|
17
|
+
// 2026-08-20:
|
|
18
|
+
//
|
|
19
|
+
// • devdocs.veriff.com/docs/general-faq, "How many sessions can I create per minute?":
|
|
20
|
+
// "Enterprise customer: 600 sessions per minute" / "Self-Serve customer: 30 sessions per
|
|
21
|
+
// minute".
|
|
22
|
+
// • devdocs.veriff.com/apidocs/v1sessionsid-3 (DELETE /v1/sessions/{id}), "Rate limiting":
|
|
23
|
+
// "This endpoint is rate limited: 10 sessions per 24 hours and 5 sessions per 1 hour".
|
|
24
|
+
//
|
|
25
|
+
// The ceiling is pinned to the SELF-SERVE tier, because a pack cannot know which contract the
|
|
26
|
+
// credential in front of it is on and the enterprise number is not a floor anyone is entitled to.
|
|
27
|
+
// 60 weighted units / 60s at the default weight of 2 is 30 calls a minute — exactly the documented
|
|
28
|
+
// self-serve session ceiling, and identical to the kernel's undeclared fallback. Being MORE
|
|
29
|
+
// permissive would require a documented number that applies to every account, and there isn't one.
|
|
30
|
+
//
|
|
31
|
+
// Veriff publishes NO limit at all for the read endpoints (decision polling, attempts, media), so
|
|
32
|
+
// those are not widened either: they spend from the same conservative allowance rather than being
|
|
33
|
+
// declared free on the strength of an absence.
|
|
34
|
+
//
|
|
35
|
+
// ── WHAT THIS DOES NOT DO: PACE ─────────────────────────────────────────────────────────────
|
|
36
|
+
// It bounds the 60s AVERAGE; it does NOT bound the instantaneous rate. In a tight `await` loop the
|
|
37
|
+
// vendor's own 429 can arrive first, and the backstop is then the COOLDOWN armed from that
|
|
38
|
+
// response. Veriff documents its 429 body (`{"status":"fail","code":"1004","message":"Too many
|
|
39
|
+
// requests."}`) but documents NO `Retry-After` and NO `X-RateLimit-*` response headers — checked
|
|
40
|
+
// across the whole published reference. The kernel reads those headers IF PRESENT and never assumes
|
|
41
|
+
// them; on a bare 429 the cooldown still arms. Do not read the header names below as a vendor fact.
|
|
42
|
+
//
|
|
43
|
+
// ── HOW THE WEIGHTS WERE CHOSEN (and what is a judgement call) ──────────────────────────────
|
|
44
|
+
// • `POST /sessions` costs the default 2 — the faithful price, since 30/min at weight 2 is
|
|
45
|
+
// exactly the documented self-serve cap.
|
|
46
|
+
// • `DELETE /sessions/{id}` costs 12, so one window admits 5 rather than 30. That is a JUDGEMENT
|
|
47
|
+
// CALL standing in for a cap this ledger structurally cannot enforce: the documented limit is
|
|
48
|
+
// 5 per HOUR and 10 per 24 HOURS, and a 60-second rolling window cannot express either. What
|
|
49
|
+
// weight 12 buys is that a delete LOOP is stopped inside the first window instead of burning
|
|
50
|
+
// the whole daily allowance in seconds; staying under 5/hour remains the caller's
|
|
51
|
+
// responsibility, and this module says so rather than implying enforcement it does not have.
|
|
52
|
+
import {
|
|
53
|
+
declareRateBudget,
|
|
54
|
+
rateBudgetPath,
|
|
55
|
+
rateBudgetWeight,
|
|
56
|
+
RateBudget,
|
|
57
|
+
type RateBudgetDeclaration,
|
|
58
|
+
type RateBudgetOptions,
|
|
59
|
+
type RateBudgetReservation,
|
|
60
|
+
type RateBudgetSnapshot,
|
|
61
|
+
} from '@volter/world-core';
|
|
62
|
+
|
|
63
|
+
const VENDOR = 'veriff';
|
|
64
|
+
|
|
65
|
+
/** Rolling window, in ms. Spend older than this is pruned. */
|
|
66
|
+
export const VERIFF_BUDGET_WINDOW_MS = 60_000;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Weighted units allowed inside one window. 60/60s = 30 calls a minute at the default weight —
|
|
70
|
+
* exactly Veriff's documented SELF-SERVE session-creation cap, and no more permissive than the
|
|
71
|
+
* kernel's undeclared fallback.
|
|
72
|
+
*/
|
|
73
|
+
export const VERIFF_BUDGET_CEILING = 60;
|
|
74
|
+
|
|
75
|
+
/** Seconds. A `Retry-After` above this means the credential is throttled hard — fail loudly, don't
|
|
76
|
+
* sleep. Veriff documents no `Retry-After`; this caps one if a live response ever carries it. */
|
|
77
|
+
export const VERIFF_BUDGET_MAX_RETRY_AFTER_S = 300;
|
|
78
|
+
|
|
79
|
+
/** Per-call cost, keyed by `"<METHOD> <path>"`. See the header for what is documented vs. judged. */
|
|
80
|
+
export const VERIFF_CALL_WEIGHTS = {
|
|
81
|
+
/** `DELETE /sessions/{id}` — documented at 5/hour + 10/day, which a 60s window cannot express. */
|
|
82
|
+
deleteSession: 12,
|
|
83
|
+
/** Everything else: session create (the documented 30/min tier), decision/attempt/media reads. */
|
|
84
|
+
other: 2,
|
|
85
|
+
} as const;
|
|
86
|
+
|
|
87
|
+
/** THE PACK'S DECLARATION — pure data, the only Veriff-specific thing in the whole budget. */
|
|
88
|
+
export const VERIFF_RATE_BUDGET: RateBudgetDeclaration = {
|
|
89
|
+
windowMs: VERIFF_BUDGET_WINDOW_MS,
|
|
90
|
+
ceiling: VERIFF_BUDGET_CEILING,
|
|
91
|
+
defaultWeight: VERIFF_CALL_WEIGHTS.other,
|
|
92
|
+
maxRetryAfterSeconds: VERIFF_BUDGET_MAX_RETRY_AFTER_S,
|
|
93
|
+
rules: [
|
|
94
|
+
{ match: '^DELETE /sessions/', weight: VERIFF_CALL_WEIGHTS.deleteSession },
|
|
95
|
+
],
|
|
96
|
+
reason:
|
|
97
|
+
'Veriff publishes two scalar limits, neither of them in the API reference (both read 2026-08-20). ' +
|
|
98
|
+
'devdocs.veriff.com/docs/general-faq: "Enterprise customer: 600 sessions per minute", "Self-Serve ' +
|
|
99
|
+
'customer: 30 sessions per minute". devdocs.veriff.com/apidocs/v1sessionsid-3 (DELETE ' +
|
|
100
|
+
'/v1/sessions/{id}): "This endpoint is rate limited: 10 sessions per 24 hours and 5 sessions per ' +
|
|
101
|
+
'1 hour". The ceiling is pinned to the SELF-SERVE tier — 60 weighted units / 60s at defaultWeight ' +
|
|
102
|
+
'2 = 30 calls a minute, exactly that documented cap — because a pack cannot know which contract ' +
|
|
103
|
+
"the credential in front of it is on, and the enterprise number is not a floor anyone is " +
|
|
104
|
+
'entitled to. That is also identical to the kernel\'s undeclared fallback, so this declaration ' +
|
|
105
|
+
'buys weighting, not permission. Veriff publishes NO limit for the read endpoints (decision ' +
|
|
106
|
+
'polling, attempts, media), so those are not widened on the strength of an absence. DELETE costs ' +
|
|
107
|
+
'12 so one window admits 5 rather than 30 — a JUDGEMENT CALL, not a published cost: the real cap ' +
|
|
108
|
+
'is 5/hour and 10/day, which a 60-second rolling window structurally cannot enforce, so staying ' +
|
|
109
|
+
"under it remains the caller's responsibility and the weight only stops a delete LOOP inside the " +
|
|
110
|
+
'first window. Veriff documents its 429 body ({"status":"fail","code":"1004","message":"Too many ' +
|
|
111
|
+
'requests."}) but NO Retry-After and NO X-RateLimit-* response headers anywhere in the published ' +
|
|
112
|
+
'reference; the kernel reads those names if present and never assumes them, and a bare 429 still ' +
|
|
113
|
+
'arms the cooldown.',
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// Declared at module load, so merely importing this module (which `veriff-connector.ts` does) is
|
|
117
|
+
// enough to arm the real ceiling. `RateBudget` reads its policy live precisely so this declaration
|
|
118
|
+
// takes effect the moment it lands, and constructing through the subclass below (which imports this
|
|
119
|
+
// module) is what makes the ordering a non-issue in practice.
|
|
120
|
+
declareRateBudget(VENDOR, VERIFF_RATE_BUDGET);
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Price one call. The key is `"<METHOD> <path>"` (the v1 path as the connector states it, without
|
|
124
|
+
* the `/v1` prefix the live executor adds) with the query string split off.
|
|
125
|
+
*
|
|
126
|
+
* NORMALIZED: `fetch` upper-cases a known method before sending, so `execute('delete', …)` really
|
|
127
|
+
* does issue a DELETE and must be priced as one; and a trailing slash otherwise makes a path miss
|
|
128
|
+
* an anchored rule while every router treats it as the same endpoint. Both are input variations,
|
|
129
|
+
* not attacks, and either one silently voids the "expensive endpoints are priced up" claim.
|
|
130
|
+
*/
|
|
131
|
+
export function veriffCallWeight(method: string, path: string): number {
|
|
132
|
+
const { bare, query } = splitQuery(path);
|
|
133
|
+
return rateBudgetWeight(VENDOR, `${String(method).toUpperCase()} ${bare}`, query);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** `/x?a=1` -> `{ bare: '/x', query: { a: '1' } }`. Rules match the path; `whenQuery*` the query. */
|
|
137
|
+
function splitQuery(path: string): { bare: string; query: Record<string, string> } {
|
|
138
|
+
const at = path.indexOf('?');
|
|
139
|
+
const query: Record<string, string> = {};
|
|
140
|
+
if (at !== -1) for (const [k, v] of new URLSearchParams(path.slice(at + 1))) query[k] = v;
|
|
141
|
+
const raw = at === -1 ? path : path.slice(0, at);
|
|
142
|
+
const bare = raw.length > 1 && raw.endsWith('/') ? raw.replace(/\/+$/, '') : raw;
|
|
143
|
+
return { bare, query };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Where Veriff's ledger lives. Token-keyed and cwd-independent by default (the limits are per
|
|
147
|
+
* integration/API key, so a cwd-scoped ledger would hand the same key a fresh allowance in every
|
|
148
|
+
* checkout, worktree and CI matrix leg); pass `root` to opt into world-scoped accounting. */
|
|
149
|
+
export function veriffBudgetPath(opts: { root?: string; token?: string } | string = {}): string {
|
|
150
|
+
const o = typeof opts === 'string' ? { root: opts } : opts;
|
|
151
|
+
// VENDOR spread LAST: a loosely-typed `{ vendor: 'other', … }` slipping through (TypeScript's
|
|
152
|
+
// excess-property check only catches object literals) must not redirect this pack's ledger to
|
|
153
|
+
// another vendor's file.
|
|
154
|
+
return rateBudgetPath({ ...o, vendor: VENDOR });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Construction options for Veriff's budget. The vendor is fixed; everything else may only TIGHTEN. */
|
|
158
|
+
export type VeriffBudgetOptions = Omit<RateBudgetOptions, 'vendor'>;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Veriff's budget — the shared kernel guard bound to this vendor's declaration. A real subclass,
|
|
162
|
+
* not an alias, so `budget instanceof VeriffBudget` in `liveVeriffExecute` means "a budget that
|
|
163
|
+
* accounts against VERIFF's ledger under VERIFF's ceiling": another vendor's `RateBudget` (with its
|
|
164
|
+
* own, possibly larger, ceiling) is NOT assignable there.
|
|
165
|
+
*/
|
|
166
|
+
export class VeriffBudget extends RateBudget {
|
|
167
|
+
constructor(opts: VeriffBudgetOptions = {}) {
|
|
168
|
+
super({ ...opts, vendor: VENDOR });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** The typed refusal. One error class shared with every other vendor's budget; `err.vendor` says
|
|
173
|
+
* which one refused, and `err.kind` says why. */
|
|
174
|
+
export { RateBudgetError as VeriffBudgetError } from '@volter/world-core';
|
|
175
|
+
export type { RateBudgetErrorKind as VeriffBudgetErrorKind } from '@volter/world-core';
|
|
176
|
+
export type VeriffBudgetReservation = RateBudgetReservation;
|
|
177
|
+
export type VeriffBudgetSnapshot = RateBudgetSnapshot;
|