@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 ADDED
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,247 @@
1
+ # @volter/twin-veriff
2
+
3
+ A local, stateful, vendor-faithful twin of **Veriff's Public API v1** — the identity-verification
4
+ Station API (`stationapi.veriff.com` / `api.veriff.me`). Point your unmodified KYC client at it and
5
+ get vendor-correct responses — sessions, the `created → submitted → approved/declined` decision
6
+ lifecycle, HMAC-signed decision and event webhooks, attempts, media and the vendor's exact error
7
+ envelope — entirely offline, with no real API key, no real end user and no billed verification.
8
+ Built on the shared [`@volter/world-core`](../../..) kernel; see [the model](../../../docs/concepts/the-model.md) for storage and branching, not an
9
+ ad-hoc mock.
10
+
11
+ **The API URL is account-specific by design.** Veriff's reference tells you to read your BaseURL off
12
+ the Customer Portal, and its OpenAPI `servers` entry is the placeholder `https://example-base-url`.
13
+ Two hosts appear in official material — `stationapi.veriff.com` (the media code samples, and the
14
+ real consumer) and `api.veriff.me` (`@veriff/js-sdk` v2.0.0's compiled default) — and the injection
15
+ map routes **both** here. The paths are identical either way.
16
+
17
+ **The HMAC is real, not faked.** `X-AUTH-CLIENT` is accepted-but-never-checked (a twin fakes auth
18
+ locally), but `X-HMAC-SIGNATURE` is verified for real, including the vendor's asymmetric payload
19
+ rule: **POST/PATCH sign the request body; GET/DELETE sign the resource id in the path** — the
20
+ *attempt* id for `/v1/attempts/{id}/media`, the *media* id for `/v1/media/{id}`. The implementation
21
+ is pinned against **Veriff's own published mock vector**, and responses carry the documented
22
+ `X-AUTH-CLIENT` + `X-HMAC-SIGNATURE` sender headers, so a consumer that authenticates the sender
23
+ gets the real thing.
24
+
25
+ ## Usage
26
+
27
+ ```bash
28
+ # serve the twin (writable by default; --read-only to reject writes with 405)
29
+ bun packages/twin/veriff/src/cli.ts serve --port 8123
30
+
31
+ # check the routing table against the handler (dev-only)
32
+ bun packages/twin/veriff/src/cli.ts conformance
33
+ ```
34
+
35
+ Point a client at it exactly as you would at Veriff — the reference consumer's shape:
36
+
37
+ ```ts
38
+ const res = await fetch('http://127.0.0.1:8123/v1/sessions', {
39
+ method: 'POST',
40
+ headers: { 'X-AUTH-CLIENT': process.env.VERIFF_API_KEY!, 'Content-Type': 'application/json' },
41
+ body: JSON.stringify({ verification: { vendorData: partner.id, person: { firstName, lastName } } }),
42
+ });
43
+ // → 201 { status: 'success', verification: { id, url, host, status, sessionToken, endUserId, vendorData } }
44
+ ```
45
+
46
+ The twin's default credentials are fixtures, not secrets: `VERIFF_API_KEY` =
47
+ `11111111-2222-4333-8444-555555555555` and `VERIFF_SHARED_SECRET` =
48
+ `abcdef12-abcd-abcd-abcd-abcdef012345` (the secret from Veriff's own published mock vector). Pass
49
+ `--shared-secret` / `createVeriffTwinServer({ apiKey, sharedSecret })` to use your own.
50
+
51
+ ### Driving the decision lifecycle
52
+
53
+ Real Veriff decides a session with OCR, face matching, liveness and sometimes a human reviewer.
54
+ What the twin models faithfully is the
55
+ **lifecycle** those decisions move through, driven deterministically by the caller through a
56
+ **twin-only** control route (namespaced out of the vendor's surface, and deliberately absent from the
57
+ capability manifest — it is test scaffolding, not coverage):
58
+
59
+ ```
60
+ POST /v1/sessions → created
61
+ PATCH /v1/sessions/{id} {status:'submitted'} → submitted (+ the 7002 event webhook)
62
+ POST /v1/_twin/sessions/{id}/decision → approved | declined | resubmission_requested
63
+ | review | expired | abandoned (+ the decision webhook)
64
+ POST /v1/_twin/sessions/{id}/event {started} → started (+ the 7001 event webhook)
65
+ GET /v1/_twin/deliveries[?sessionId=] → every webhook the twin would have PUSHED, signed
66
+ ```
67
+
68
+ Webhook deliveries are **recorded, signed exactly as they would go on the wire, and never put on a
69
+ socket** — so a consumer can read one back and run its real verification code against it offline,
70
+ with no receiver to stand up.
71
+
72
+ ## Coverage
73
+
74
+ Coverage is measured against the **real Veriff surface** as the denominator (see
75
+ `src/veriff-capabilities.ts`) — it reads partial-but-honest and grows toward 100%. Run
76
+ `bun scripts/manifest-baseline-one.ts veriff` for the live numbers.
77
+
78
+ The denominator was enumerated top-down from `devdocs.veriff.com/llms.txt` (Veriff's own
79
+ machine-readable index of all 140 doc pages, whose `/apidocs/*` pages embed the real OpenAPI 3.0.0
80
+ documents) — the 18 documented Public API v1 operations, the three Feedback/Fraud API operations, the
81
+ sync-api registry operation, and the per-solution pages (Document+Selfie, Document-only, Biometric
82
+ Authentication/Liveness, Selfie2Selfie, Age Estimation, Unstructured Docs, Proof of Address, AML
83
+ screening, NFC/ePassport, UK DIATF, and the nine database verifications).
84
+
85
+ **Modeled (done, with failable offline verifies):**
86
+
87
+ - **Sessions** — create (`201`, all seven required `verification` fields, `url = host + '/v/' +
88
+ sessionToken`), `PATCH … {status:'submitted'}` (the only value the vendor accepts), delete;
89
+ the documented negative paths: missing `verification` (`400`/`1101`), non-string / over-1,000-char
90
+ `vendorData` (`1501` / `1500`), non-HTTPS `callback` (`1302`), re-submit (`Session has already been
91
+ submitted`), delete-while-submitted (`1306` `Session in progress.`) and delete-in-review (`1305`
92
+ `Session is not in a completed status.`).
93
+ - **Decisions** — `verification: null` before a decision (including while `submitted`), then
94
+ approved `9001` / declined `9102` + granular reason & reasonCode / resubmission `9103` (which
95
+ re-opens the session for capture) / expired `9104` / abandoned `9121` / review with a **null**
96
+ code (Veriff publishes none — see below); extracted
97
+ `person` + `document` kept distinct from the hints supplied at creation; `riskLabels`; the
98
+ state-machine guard (`1304`) in both directions — no jump straight from `created`, and no second
99
+ decision over a terminal one.
100
+ - **Attempts** — the `verifications[]` array with `id`/`status`/`userDefinedData`/`createdTime`,
101
+ reverse-chronological across a resubmission cycle, empty (not `404`) before the first attempt.
102
+ - **Person** — `GET /v1/sessions/{id}/person`, `null` until a decision exists.
103
+ - **Media** — upload (`200` — the media endpoint declares exactly one success response and it is
104
+ not 201; the status-code table's 201 row is scoped to *session* creation — with the documented
105
+ image object; `name` mirrors `context`; `timestamp` is
106
+ deprecated and always `null`; `size` is the *decoded* byte count), context-enum (`1402`) and
107
+ base64 (`1401`) rejection, the `409` on upload-after-submit, per-session and per-attempt listing
108
+ (`images`/`videos`/`nfcDocuments`), and download as **real bytes under the object's own mimetype**.
109
+ - **Auth** — missing `X-AUTH-CLIENT` (`401`, the vendor's verbatim message); missing/wrong
110
+ `X-HMAC-SIGNATURE` (`401`/`1812`); the `POST /v1/sessions` signing exemption *and* that every other
111
+ endpoint still requires one; body-vs-path-resource-id signing; the published mock vector; and the
112
+ documented response sender headers.
113
+ - **Webhooks** — the decision delivery carrying byte-identical content to the decision endpoint; a
114
+ real consumer's verification passing over the raw body; rejection of a tampered body, a wrong
115
+ `x-auth-client`, a missing signature and a wrong secret — each by its own reason; the 7002
116
+ `submitted` and 7001 `started` event payloads (no `verification` key, which is the discriminator a
117
+ real receiver routes on); and no delivery at all when no callback was configured.
118
+ - **Errors + platform** — the `{status:'fail', code, message}` envelope with `code` as a **string**
119
+ (the schema's declared type, despite every value looking numeric); unmodeled routes failing like
120
+ the vendor (`404`); `readOnly` refusing every write with `405`; and two deliberate **dirty-state**
121
+ verifies (ids never reused across delete→recreate; a decision after a resubmission cycle reports
122
+ the *latest* attempt).
123
+ - **Connector** — pull decisions/attempts/media over an injected client, idempotent re-pull
124
+ (`deltasAppended` → 0), no id collision in **either** direction (local mint after a pull, pull
125
+ after a local mint), push create/submit/delete with confirmation **only** on `status:'success'`,
126
+ the pure action→request and signing-payload rules, mappers that rename the kernel's reserved META
127
+ keys, and the client-side rate budget refusing past the ceiling with the injected fake's call count
128
+ **unchanged**.
129
+
130
+ A **fidelity test** (`src/veriff-fetch.integration.test.ts`) drives the twin over **real-transport
131
+ `fetch`** with a client rebuilt from the reference consumer's own source — same headers, same signing
132
+ payload, same shape checks. Veriff publishes **no server-side SDK** (its npm org ships browser and
133
+ mobile packages only), so a real server integration hand-rolls exactly this, and
134
+ [Adding a twin](../../../docs/contributing/adding-a-twin.md) §10 sanctions it for precisely this case. The same
135
+ file additionally pins **`@veriff/js-sdk` v2.0.0's wire contract**, read from the published bundle.
136
+
137
+ **`todo` (real surface enumerated but not yet modeled):** watchlist screening / AML (`GET`+`PATCH`,
138
+ hits, `202` in-progress, `402` not-enabled, its webhook family, `pepSanctionMatch`); collected-data
139
+ / device intelligence and `technicalData`; bulk face import; `validate-registry` (both the Public API
140
+ and the synchronous `sync-api.veriff.me` variant) and the three legacy registry decisions; the ten
141
+ database verifications; the ten IDV solutions and their decision-payload objects (`riskScore`,
142
+ `additionalVerifiedData`, `biometricAuthentication`, `udocs`, `comments`, `submissionTime`, `tag`);
143
+ the full granular decline/resubmission reason-code tables; the complete `person`/`document` field
144
+ sets; Proof of Address (validation, matching, fraud check); PDF/NFC/video media and the documented
145
+ size limits; the web-flow event actions 7007-7011 and the `context` object; the user-defined-statuses,
146
+ Full Auto and registry webhook families; delivery retries; multiple shared secrets with a master
147
+ signing key; the Feedback/Fraud API on its own host with the `VRF-*` header scheme; 7-day session
148
+ expiry; the 9-resubmission cap; the served `429`; the precise credential (1801-1819) and
149
+ troubleshooting (1001-2104) code taxonomies; and five connector gaps (media bytes, watchlist, person,
150
+ media push, mid-batch back-off) filed in `pull-audit.json`.
151
+
152
+ ### The twin refuses to invent a `GET /v1/sessions/{id}`
153
+
154
+ Veriff **has no such endpoint** — its API reference index lists only `POST`/`PATCH`/`DELETE` on that
155
+ path, and GETs on the sub-resources (`/decision`, `/person`, `/attempts`, `/media`,
156
+ `/watchlist-screening`). An integration learns a session's state from the webhooks and the decision
157
+ endpoint, which is exactly why the reference consumer stores its own `veriffSessionId` and status.
158
+ Serving a convenient read here would be the mirror image of the "unmodeled ops fail like the vendor"
159
+ rule — an *invented* op — so a `GET` on that path answers the vendor's `404`, and
160
+ `veriff.sessions.no_get_endpoint` pins it while asserting the sub-resources still answer for the
161
+ same live session. Verifies that need to read a session's status back use the twin-only
162
+ `GET /v1/_twin/sessions/{id}`, which is scaffolding and is not in the manifest.
163
+
164
+ ### Where Veriff's own docs contradict themselves
165
+
166
+ Recorded rather than papered over, because a twin that silently picks a side teaches the wrong thing:
167
+
168
+ 1. **`POST /v1/sessions` — 200 or 201?** The embedded OpenAPI declares the success response `200`;
169
+ the vendor's own HTTP-status-codes table says `201` ("returned when session creation was a
170
+ success"); and `@veriff/js-sdk` v2.0.0 hard-checks `201 === xhr.status`. **This twin answers 201**,
171
+ because answering 200 would break the shipped SDK.
172
+ 2. **`review` has no published code.** The decision endpoint's `code` enum is exactly
173
+ `[9001, 9102, 9103, 9104, 9121]` and its `status` enum is
174
+ `[approved, declined, resubmission_requested, expired, abandoned]` — yet the decision **webhook**
175
+ page lists `review` among the statuses it sends (it is opt-in: "Only if previously agreed with
176
+ Veriff"). So `review` is a real status with no number of its own. **This twin emits `code: null`
177
+ for it** rather than borrowing 9121, which belongs to `abandoned` (the endpoint's prose says so,
178
+ and its `session_abandoned_generic` example pairs them explicitly, as
179
+ `session_expired_generic` pairs `expired` with 9104). Borrowing would tell a consumer branching
180
+ on `code` that a manual-review case was abandoned. `veriff.decisions.review_code` and
181
+ `veriff.decisions.code_numbering` are the filed todos. Note the reference consumer keys off
182
+ `status`, never `code` — which is the behaviour a twin should encourage.
183
+
184
+ Two smaller ones, both recorded in the code where they bite. The DELETE endpoint's prose lists seven
185
+ deletable statuses (`created`, `started`, `approved`, `declined`, `resubmission_requested`, `expired`,
186
+ `abandoned`) while its own `1305` example says the session "must be in a completed state (`approved`,
187
+ `declined`, `expired`, `abandoned`)" — the twin follows the prose list, because the same page also
188
+ describes a decision webhook firing when a `created`/`started`/`resubmission_requested` session is
189
+ deleted, which only makes sense if those are deletable. And the attempts endpoint's published `status`
190
+ enum omits `approved` while its own examples show it — the twin follows the examples
191
+ (`veriff.attempts.status_enum` is filed to pin it). The decision schema declares `code` and
192
+ `reasonCode` as integers and the decision *webhook* sample agrees (`"code": 9001`), while the
193
+ decision *endpoint*'s own examples quote them (`"code": "9121"`) — the twin emits numbers and files
194
+ `veriff.decisions.code_wire_type`. And the DELETE page publishes both a blanket
195
+ `1305` refusal for every non-deletable status *and* a `1306` "Session in progress." whose stated
196
+ trigger (`started`) is on the deletable list — so routing `submitted` → `1306` is this twin's
197
+ disambiguation, said plainly as such in the code and filed as `veriff.sessions.delete_refusal_codes`.
198
+
199
+ A third: the HMAC doc's Python sample emits a `sha256=` prefix that contradicts the same
200
+ page's mock vector and every other sample on it. The twin follows the vector — bare lowercase hex.
201
+
202
+ ### No UI mirror
203
+
204
+ **The API is the product here.** When someone does Veriff's core job *the way the party integrating
205
+ it does* — POST a session, embed the hosted flow, receive the decision webhook, read the decision —
206
+ they write code. The only screen in that loop is Veriff's **own hosted capture flow** (camera,
207
+ document framing, retries, localisation) served from the session `url`, which is Veriff's first-party
208
+ product surface rather than a customer-operated console. The twin returns a real session `url`, so
209
+ an integration's redirect/embed path is exercised end to end. Coverage is API + connector, and the
210
+ manifest has zero `ui` capabilities that claim a mirror.
211
+
212
+ The counter-argument is real and is recorded in `ui-scope.json` rather than dismissed: **Veriff
213
+ Station** *is* a dashboard where a compliance team reviews sessions, decisions and captured media,
214
+ and [Adding a twin](../../../docs/contributing/adding-a-twin.md) warns that a thin REST API is evidence *for* a
215
+ mirror, not against it. What settles it here is that the UI in question is the *end user's* one-time
216
+ capture flow rather than the customer's workspace — and that reproducing the review console would
217
+ mean rendering document photographs a local twin never has. The reference consumer agrees in
218
+ practice: dub embeds `@veriff/incontext-sdk` and then built its **own** admin identity-verification
219
+ screen rather than working in Veriff Station. Re-opening this is legitimate.
220
+
221
+ ## Architecture
222
+
223
+ State uses the shared kernel; see [the model](../../../docs/concepts/the-model.md) for log,
224
+ checkpoint and branch semantics. There is no parallel mutable truth store.
225
+ There is **no vendor devDependency** — Veriff publishes no server-side SDK, so fidelity is proven
226
+ over real-transport `fetch`. The pack **`peerDepends`** on `@volter/world-core`, and conformance tooling
227
+ (`@volter/world-tooling`) is dev-only and imported lazily by the CLI.
228
+
229
+ Ids are **UUID v4**, because that is what Veriff's ids are (`"format": "uuid"` on every id in the
230
+ published schemas) — and because entropy, never a row count, is what makes a local mint and a pulled
231
+ vendor id incapable of colliding in either direction. Veriff's payloads are full of `id` fields, and
232
+ the kernel's projection RESERVES `type`/`id`/`updatedAt` as META (a resource field with one of those
233
+ names is silently dropped), so every vendor id is stored under a renamed key (`sessionId`,
234
+ `attemptId`, `mediaId`) and mapped back at the boundary — on the local-mint path and the connector's
235
+ pull path alike.
236
+
237
+ **Pull is handle-driven, and that is the vendor's doing:** Veriff publishes no account-wide list
238
+ endpoint — there is no `GET /v1/sessions`, and every documented read is scoped to an id the caller
239
+ already holds — so `syncVeriffFromReal` takes the session ids to pull, exactly as a real integration
240
+ does. `pull-audit.json` records this rather than filing an enumeration gap for surface the API does
241
+ not expose.
242
+
243
+ Every live call goes through **one guarded client** (`liveVeriffExecute`) with a persistent,
244
+ token-keyed spend ledger that throws instead of calling past the ceiling — see `src/veriff-budget.ts`
245
+ for how Veriff's two published limits (600/min enterprise, 30/min self-serve session creation;
246
+ 10-per-24h + 5-per-1h on delete) were turned into numbers, and for what the guard honestly does not
247
+ enforce.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,30 @@
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.js";
10
+ const [cmd, ...rest] = process.argv.slice(2);
11
+ const port = Number(optionValue(rest, '--port', '0')) || undefined;
12
+ const root = optionValue(rest, '--root') || undefined;
13
+ const sharedSecret = optionValue(rest, '--shared-secret') || undefined;
14
+ const readOnly = hasFlag(rest, '--read-only'); // a twin accepts writes unless started read-only
15
+ if (cmd === 'serve') {
16
+ const s = await createVeriffTwinServer({ readOnly, ...(root ? { root } : {}), ...(port ? { port } : {}), ...(sharedSecret ? { sharedSecret } : {}) });
17
+ process.stdout.write(`veriff twin (Station API v1)${readOnly ? ' [read-only]' : ''} at http://127.0.0.1:${s.port}\n`);
18
+ await keepProcessAlive();
19
+ }
20
+ else if (cmd === 'conformance') {
21
+ // dev-only; lazy so the bin runs without @volter/world-tooling in the runtime graph
22
+ const { checkVeriffConformance } = await import("./veriff-conformance.js");
23
+ const report = await checkVeriffConformance({ ...(root ? { root } : {}) });
24
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
25
+ if (!report.ok)
26
+ process.exitCode = 1;
27
+ }
28
+ else {
29
+ process.stdout.write('Usage: world-veriff serve|conformance [--port N] [--root DIR] [--shared-secret S] [--read-only]\n');
30
+ }
@@ -0,0 +1,12 @@
1
+ export { handleVeriffTwinRequest, TWIN_API_KEY, TWIN_SHARED_SECRET, VERIFF_DECISION_CODES, VERIFF_DECISION_STATUSES, VERIFF_ERROR_CODES, VERIFF_ERROR_MESSAGES, VERIFF_EVENT_CODES, VERIFF_IMPLEMENTED_ENDPOINTS, VERIFF_RESOURCE_TYPES, VERIFF_SESSION_STATUSES, } from './veriff-twin.js';
2
+ export type { VeriffRequest, VeriffResponse, VeriffSessionStatus, VeriffDecisionStatus } from './veriff-twin.js';
3
+ export { createVeriffTwinFetch, createVeriffTwinServer, veriffResponseHeaders, type VeriffTwinFetchOptions } from './veriff-server.js';
4
+ export { AUTH_CLIENT_HEADER, HMAC_SIGNATURE_HEADER, signaturesMatch, veriffSignature, veriffSignatureBytes, verifyVeriffSignature, } from './veriff-signature.js';
5
+ export { buildSignedDelivery, emitVeriffWebhook, VERIFF_EVENT_ACTIONS, verifyWebhook, VeriffWebhookVerificationError, } from './veriff-events.js';
6
+ export type { VeriffDecisionPayload, VeriffEventAction, VeriffEventPayload, VeriffWebhookDelivery, VeriffWebhookPayload } from './veriff-events.js';
7
+ export { liveVeriffExecute, mapSessionDecision, mapSessionAttempt, mapSessionMedia, pullVeriffAttempts, pullVeriffDecisions, pullVeriffMedia, pushPendingVeriffActions, signaturePayloadFor, syncVeriffFromReal, veriffRequestForAction, } from './veriff-connector.js';
8
+ export type { LiveVeriffOptions, VeriffExecute } from './veriff-connector.js';
9
+ export { VERIFF_BUDGET_CEILING, VERIFF_BUDGET_MAX_RETRY_AFTER_S, VERIFF_BUDGET_WINDOW_MS, VERIFF_CALL_WEIGHTS, VERIFF_RATE_BUDGET, VeriffBudget, VeriffBudgetError, veriffBudgetPath, veriffCallWeight, } from './veriff-budget.js';
10
+ export type { VeriffBudgetErrorKind, VeriffBudgetOptions, VeriffBudgetReservation, VeriffBudgetSnapshot } from './veriff-budget.js';
11
+ import { type TwinPack } from '@volter/world-core';
12
+ export declare const pack: TwinPack;
@@ -0,0 +1,119 @@
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 { handleVeriffTwinRequest, TWIN_API_KEY, TWIN_SHARED_SECRET, VERIFF_DECISION_CODES, VERIFF_DECISION_STATUSES, VERIFF_ERROR_CODES, VERIFF_ERROR_MESSAGES, VERIFF_EVENT_CODES, VERIFF_IMPLEMENTED_ENDPOINTS, VERIFF_RESOURCE_TYPES, VERIFF_SESSION_STATUSES, } from "./veriff-twin.js";
15
+ export { createVeriffTwinFetch, createVeriffTwinServer, veriffResponseHeaders } from "./veriff-server.js";
16
+ export { AUTH_CLIENT_HEADER, HMAC_SIGNATURE_HEADER, signaturesMatch, veriffSignature, veriffSignatureBytes, verifyVeriffSignature, } from "./veriff-signature.js";
17
+ export { buildSignedDelivery, emitVeriffWebhook, VERIFF_EVENT_ACTIONS, verifyWebhook, VeriffWebhookVerificationError, } from "./veriff-events.js";
18
+ export { liveVeriffExecute, mapSessionDecision, mapSessionAttempt, mapSessionMedia, pullVeriffAttempts, pullVeriffDecisions, pullVeriffMedia, pushPendingVeriffActions, signaturePayloadFor, syncVeriffFromReal, veriffRequestForAction, } from "./veriff-connector.js";
19
+ // The client-side rate budget — the fail-closed backstop `liveVeriffExecute` routes every live
20
+ // request through. The MECHANISM is the kernel's shared, vendor-agnostic `RateBudget`; what lives
21
+ // here is Veriff's DECLARATION (window/ceiling/per-endpoint weights) plus the vendor-bound
22
+ // bindings. Exported so an operator can inspect spend (`snapshot`) and so a caller can catch
23
+ // `VeriffBudgetError` by type; there is deliberately no export that disables the guard.
24
+ export { VERIFF_BUDGET_CEILING, VERIFF_BUDGET_MAX_RETRY_AFTER_S, VERIFF_BUDGET_WINDOW_MS, VERIFF_CALL_WEIGHTS, VERIFF_RATE_BUDGET, VeriffBudget, VeriffBudgetError, veriffBudgetPath, veriffCallWeight, } from "./veriff-budget.js";
25
+ // Registry descriptor: the pack self-describes so tooling can discover it.
26
+ import { registerPack } from '@volter/world-core';
27
+ import { signaturePayloadFor } from "./veriff-connector.js";
28
+ import { HMAC_SIGNATURE_HEADER } from "./veriff-signature.js";
29
+ import { performVeriffAction, syncVeriffFromRemote } from "./veriff-connector.js";
30
+ import { VERIFF_RATE_BUDGET as RATE_BUDGET } from "./veriff-budget.js";
31
+ export const pack = {
32
+ // 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
33
+ // the real state system. Moved 2026-09-08.
34
+ protocol: '2',
35
+ // Veriff DOES publish webhooks (the decision callback is the whole integration), so a world that
36
+ // wires them hears about a decision as it lands; the poll is the backstop for one that has not.
37
+ refresh: { every: '5m', webhook: true, onDemand: { atMost: '30s' } },
38
+ stateSystem: { perform: performVeriffAction, refresh: syncVeriffFromRemote },
39
+ // The round trip is a session create — Veriff's own write, and the vendor mints a fresh id for
40
+ // every one, so a second send on a branch is a second session rather than a collision.
41
+ roundTrip: { method: 'POST', path: '/v1/sessions', body: { verification: { vendorData: 'round-trip' } }, headers: { 'x-auth-client': 'round-trip' } },
42
+ // Veriff signs EVERY call but one: `X-HMAC-SIGNATURE` over the request body for a write, and over
43
+ // the resource id in the path for a GET/DELETE. `signaturePayloadFor` is the pack's own pure
44
+ // statement of which bytes those are — it takes no secret, so the secret stays in the kernel
45
+ // (docs/concepts/the-model.md#the-rules, rule 5, "the secret never crossing into pack code").
46
+ //
47
+ // TWO THINGS THIS LINE HAS TO GET RIGHT, both measured against the pack's live executor:
48
+ // • `POST /sessions` is the one endpoint the vendor EXEMPTS from signing — there is no session
49
+ // to sign for yet — so the canonical answers `null` and the request goes out unsigned.
50
+ // • `signaturePayloadFor` reads the resource id POSITIONALLY out of a path with no version
51
+ // prefix (`/sessions/{id}` → the id). The kernel sends `/v1/...`, so the prefix comes off
52
+ // first; signing `sessions` instead of the session id would verify against nothing.
53
+ auth: {
54
+ in: 'signature',
55
+ algorithm: 'hmac-sha256',
56
+ encoding: 'hex',
57
+ header: HMAC_SIGNATURE_HEADER,
58
+ canonical: ({ method, path, body }) => {
59
+ const bare = path.replace(/^\/v1(?=\/|$)/, '');
60
+ if (method === 'POST' && bare === '/sessions')
61
+ return null;
62
+ return signaturePayloadFor(method, bare, body);
63
+ },
64
+ },
65
+ parityOrigin: 'http://twin',
66
+ vendor: 'veriff',
67
+ // The SAME object veriff-budget.ts declares at module load — one source of truth, so registering
68
+ // the pack and importing the connector can never arm two different ceilings.
69
+ rateBudget: RATE_BUDGET,
70
+ transport: 'rest',
71
+ archetype: 'crud',
72
+ bin: 'world-veriff',
73
+ resources: ['session', 'attempt', 'media', 'delivery'],
74
+ 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/*)',
75
+ 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.',
76
+ // Adoption + interception, moved off the central maps unchanged (descriptor-first back-migration, adding-a-twin.md §3,
77
+ // 2026-08-31).
78
+ //
79
+ // Veriff publishes NO server-side SDK — a real integration hand-rolls a `fetch` client against
80
+ // the Station API (the reference consumer, dub, does exactly that in
81
+ // apps/web/lib/veriff/client.ts) — so the only npm package that is genuinely a CLIENT OF THE
82
+ // MODELED SURFACE is `@veriff/js-sdk`: v2.0.0's `createSession` XHRs `POST {host}/v1/sessions`
83
+ // straight from the browser with `x-auth-client`, and its `host` is a constructor option, so
84
+ // pointing it at a twin is configuration. Its siblings are covered by the `@veriff/` SCOPE
85
+ // rather than listed as sdks, because they are NOT API clients: `@veriff/incontext-sdk` makes
86
+ // ZERO network calls of its own (verified against the published v2.5.0 bundle: no fetch, no
87
+ // XMLHttpRequest, no hardcoded URL) — it iframes the session `url` the API already returned and
88
+ // listens for postMessage — and the react-native/Cordova packages launch the native capture
89
+ // flow. Listing them as sdks would claim the twin intercepts traffic they never send; leaving
90
+ // them out of the scope entirely would report the exact package the reference consumer installs
91
+ // as unknown-sdk.
92
+ //
93
+ // VERIFF_API_KEY is the credential that stems here — the name the reference consumer uses
94
+ // verbatim (dub's apps/web/.env.example). Its sibling VERIFF_SHARED_SECRET deliberately does
95
+ // NOT: it stems to `veriffshared` under the broad suffix list (`_SECRET` splits before
96
+ // `SHARED`), and it matches no STRICT suffix at all, so it never reaches the lookup and cannot
97
+ // raise a false unknown-vendor alarm either.
98
+ adoption: {
99
+ // Veriff ships no Python SDK - its clients are the JS `@veriff/js-sdk` and the raw REST API.
100
+ pypi: [],
101
+ sdks: ['@veriff/js-sdk'], scopes: ['@veriff/'], envStems: ['VERIFF'],
102
+ },
103
+ // The API URL is ACCOUNT-SPECIFIC by design — Veriff's reference tells you to read your BaseURL
104
+ // off the Customer Portal and its OpenAPI `servers` entry is the placeholder
105
+ // `https://example-base-url` — so BOTH hosts that appear in official material are routed: the
106
+ // reference consumer and the media code samples use `stationapi.veriff.com`, while
107
+ // `@veriff/js-sdk` v2.0.0 compiles in `api.veriff.me` as its default. Deliberately EXACT
108
+ // matches, not a `.veriff.com`/`.veriff.me` suffix: `magic.veriff.me` and `alchemy.veriff.com`
109
+ // serve the HOSTED end-user capture flow (Veriff's own first-party web app; the twin returns a
110
+ // real session `url` rather than rendering it), `cdn.veriff.me` serves the browser SDK bundles,
111
+ // and `feedback.api.veriff.com` is the separate Fraud API with its own VRF-* header scheme that
112
+ // this pack does not model. Routing any of those into the twin would intercept requests it
113
+ // cannot answer.
114
+ hosts: [{ host: 'stationapi.veriff.com' }, { host: 'api.veriff.me' }],
115
+ // A Veriff client calls stationapi.veriff.com/v1/… — the dev proxy forwards '/v1/' to the twin
116
+ // and strips the absolute host so calls come back same-origin.
117
+ browserRouting: { apiPathPrefix: '/v1/', loaderHost: 'https://stationapi.veriff.com' },
118
+ };
119
+ registerPack(pack);