@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,36 @@
|
|
|
1
|
+
/** The header Veriff carries the public API key in (case-insensitive on the wire). */
|
|
2
|
+
export declare const AUTH_CLIENT_HEADER = "x-auth-client";
|
|
3
|
+
/** The header Veriff carries the HMAC in (case-insensitive on the wire). */
|
|
4
|
+
export declare const HMAC_SIGNATURE_HEADER = "x-hmac-signature";
|
|
5
|
+
/**
|
|
6
|
+
* Compute Veriff's signature for a payload: lowercase-hex HMAC-SHA256(sharedSecret, payload).
|
|
7
|
+
*
|
|
8
|
+
* `payload` is the raw request/webhook body for anything with a body, and the bare path parameter
|
|
9
|
+
* (session id, attempt id, media id) for a GET. Callers pass the exact bytes — this function never
|
|
10
|
+
* re-serializes, because a re-serialized body signs different bytes than the ones sent.
|
|
11
|
+
*/
|
|
12
|
+
export declare function veriffSignature(payload: string, sharedSecret: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* The same signature over RAW BYTES rather than a string.
|
|
15
|
+
*
|
|
16
|
+
* Needed because one response is not JSON: `GET /v1/media/{id}` answers with the media itself under
|
|
17
|
+
* its own mimetype, and the documented `X-HMAC-SIGNATURE` header is "Response body signed with the
|
|
18
|
+
* shared secret key" — the body a receiver actually got. Signing the base64 TEXT the twin happens to
|
|
19
|
+
* store it as produces a digest that verifies against nothing the consumer holds; a §9 round-two
|
|
20
|
+
* review measured exactly that mismatch before this existed.
|
|
21
|
+
*/
|
|
22
|
+
export declare function veriffSignatureBytes(payload: Uint8Array, sharedSecret: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* Constant-time compare of two signature strings.
|
|
25
|
+
*
|
|
26
|
+
* Length is compared first and NOT with `timingSafeEqual` — the node primitive THROWS on a length
|
|
27
|
+
* mismatch rather than returning false, so an unequal-length forgery would crash the handler
|
|
28
|
+
* instead of being rejected. The real consumer (dub) guards the same way.
|
|
29
|
+
*/
|
|
30
|
+
export declare function signaturesMatch(a: string, b: string): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Verify a presented signature against the payload. Returns the reason it failed, or `null` when
|
|
33
|
+
* it is valid — so a caller can map a MISSING header and a WRONG one to the different statuses
|
|
34
|
+
* Veriff uses for them.
|
|
35
|
+
*/
|
|
36
|
+
export declare function verifyVeriffSignature(payload: string, presented: string | undefined | null, sharedSecret: string): 'missing' | 'mismatch' | null;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { nodeBuiltin } from '@volter/world-core';
|
|
2
|
+
function nodeCrypto() {
|
|
3
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
4
|
+
return nodeBuiltin('node:crypto');
|
|
5
|
+
}
|
|
6
|
+
/** The header Veriff carries the public API key in (case-insensitive on the wire). */
|
|
7
|
+
export const AUTH_CLIENT_HEADER = 'x-auth-client';
|
|
8
|
+
/** The header Veriff carries the HMAC in (case-insensitive on the wire). */
|
|
9
|
+
export const HMAC_SIGNATURE_HEADER = 'x-hmac-signature';
|
|
10
|
+
/**
|
|
11
|
+
* Compute Veriff's signature for a payload: lowercase-hex HMAC-SHA256(sharedSecret, payload).
|
|
12
|
+
*
|
|
13
|
+
* `payload` is the raw request/webhook body for anything with a body, and the bare path parameter
|
|
14
|
+
* (session id, attempt id, media id) for a GET. Callers pass the exact bytes — this function never
|
|
15
|
+
* re-serializes, because a re-serialized body signs different bytes than the ones sent.
|
|
16
|
+
*/
|
|
17
|
+
export function veriffSignature(payload, sharedSecret) {
|
|
18
|
+
return nodeCrypto().createHmac('sha256', sharedSecret).update(payload, 'utf8').digest('hex');
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The same signature over RAW BYTES rather than a string.
|
|
22
|
+
*
|
|
23
|
+
* Needed because one response is not JSON: `GET /v1/media/{id}` answers with the media itself under
|
|
24
|
+
* its own mimetype, and the documented `X-HMAC-SIGNATURE` header is "Response body signed with the
|
|
25
|
+
* shared secret key" — the body a receiver actually got. Signing the base64 TEXT the twin happens to
|
|
26
|
+
* store it as produces a digest that verifies against nothing the consumer holds; a §9 round-two
|
|
27
|
+
* review measured exactly that mismatch before this existed.
|
|
28
|
+
*/
|
|
29
|
+
export function veriffSignatureBytes(payload, sharedSecret) {
|
|
30
|
+
return nodeCrypto().createHmac('sha256', sharedSecret).update(payload).digest('hex');
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Constant-time compare of two signature strings.
|
|
34
|
+
*
|
|
35
|
+
* Length is compared first and NOT with `timingSafeEqual` — the node primitive THROWS on a length
|
|
36
|
+
* mismatch rather than returning false, so an unequal-length forgery would crash the handler
|
|
37
|
+
* instead of being rejected. The real consumer (dub) guards the same way.
|
|
38
|
+
*/
|
|
39
|
+
export function signaturesMatch(a, b) {
|
|
40
|
+
const ab = Buffer.from(a, 'utf8');
|
|
41
|
+
const bb = Buffer.from(b, 'utf8');
|
|
42
|
+
if (ab.length !== bb.length)
|
|
43
|
+
return false;
|
|
44
|
+
return nodeCrypto().timingSafeEqual(Uint8Array.from(ab), Uint8Array.from(bb));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Verify a presented signature against the payload. Returns the reason it failed, or `null` when
|
|
48
|
+
* it is valid — so a caller can map a MISSING header and a WRONG one to the different statuses
|
|
49
|
+
* Veriff uses for them.
|
|
50
|
+
*/
|
|
51
|
+
export function verifyVeriffSignature(payload, presented, sharedSecret) {
|
|
52
|
+
if (presented === undefined || presented === null || presented === '')
|
|
53
|
+
return 'missing';
|
|
54
|
+
return signaturesMatch(veriffSignature(payload, sharedSecret), presented) ? null : 'mismatch';
|
|
55
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
export type VeriffRequest = {
|
|
2
|
+
method: string;
|
|
3
|
+
path: string;
|
|
4
|
+
body?: string;
|
|
5
|
+
headers?: Record<string, string | undefined>;
|
|
6
|
+
occurredAt?: string;
|
|
7
|
+
root?: string;
|
|
8
|
+
readOnly?: boolean;
|
|
9
|
+
/** The shared secret this twin verifies `X-HMAC-SIGNATURE` against and signs webhooks with. */
|
|
10
|
+
sharedSecret?: string;
|
|
11
|
+
/** The API key this twin stamps into `x-auth-client` on outbound webhook deliveries. */
|
|
12
|
+
apiKey?: string;
|
|
13
|
+
};
|
|
14
|
+
export type VeriffResponse = {
|
|
15
|
+
status: number;
|
|
16
|
+
body: unknown;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* The default local credentials. A twin fakes auth, so these are FIXTURES, not secrets: the point
|
|
20
|
+
* of pinning them is that a consumer's `.env` can point `VERIFF_API_KEY` / `VERIFF_SHARED_SECRET`
|
|
21
|
+
* at the twin and the HMAC round-trip works end to end with no per-run coordination. Shaped like
|
|
22
|
+
* the real thing — Veriff's API key and shared secret are both UUIDs.
|
|
23
|
+
*/
|
|
24
|
+
export declare const TWIN_API_KEY = "11111111-2222-4333-8444-555555555555";
|
|
25
|
+
export declare const TWIN_SHARED_SECRET = "abcdef12-abcd-abcd-abcd-abcdef012345";
|
|
26
|
+
/**
|
|
27
|
+
* The troubleshooting/credential codes this twin actually emits, each with the verbatim message
|
|
28
|
+
* from the endpoint's own published example. `1101` is heavily overloaded by the vendor — it is the
|
|
29
|
+
* generic code across 400 (validation), 401 (missing key), 404 (not found) and 500.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* The verbatim message the vendor's troubleshooting-codes table pairs with each code this twin
|
|
33
|
+
* emits. Kept as data next to the codes so the two can never drift apart, and so a consumer that
|
|
34
|
+
* surfaces `message` to a support engineer sees the string Veriff's own docs use.
|
|
35
|
+
*/
|
|
36
|
+
export declare const VERIFF_ERROR_MESSAGES: {
|
|
37
|
+
readonly '1302': "Only HTTPS return URLs are allowed.";
|
|
38
|
+
readonly '1401': "Image is not in valid `base64`.";
|
|
39
|
+
readonly '1402': "Image context is not supported.";
|
|
40
|
+
readonly '1500': "`vendorData` field cannot be more than 1000 symbols. We require only non-semantic data to be submitted (UUID-s etc., that can not be resolved or used outside the customer's domain)";
|
|
41
|
+
readonly '1501': "`vendorData` must be a string. We require only non-semantic data to be submitted (UUID-s etc., that can not be resolved or used outside the customer's domain)";
|
|
42
|
+
};
|
|
43
|
+
export declare const VERIFF_ERROR_CODES: {
|
|
44
|
+
readonly generic: "1101";
|
|
45
|
+
readonly invalidHmac: "1812";
|
|
46
|
+
readonly notCompleted: "1305";
|
|
47
|
+
readonly inProgress: "1306";
|
|
48
|
+
readonly badTransition: "1304";
|
|
49
|
+
readonly invalidBase64: "1401";
|
|
50
|
+
readonly unsupportedContext: "1402";
|
|
51
|
+
readonly httpsOnlyCallback: "1302";
|
|
52
|
+
readonly vendorDataLength: "1500";
|
|
53
|
+
readonly vendorDataType: "1501";
|
|
54
|
+
};
|
|
55
|
+
/** Every state a session can be in, across both halves of the lifecycle. */
|
|
56
|
+
export declare const VERIFF_SESSION_STATUSES: readonly ["created", "started", "submitted", "approved", "declined", "resubmission_requested", "review", "expired", "abandoned"];
|
|
57
|
+
export type VeriffSessionStatus = (typeof VERIFF_SESSION_STATUSES)[number];
|
|
58
|
+
/** The terminal decision statuses — the exact set the decision webhook documents, and the exact set
|
|
59
|
+
* the reference consumer's zod enum accepts (dub's `veriffDecisionEventSchema`). */
|
|
60
|
+
export declare const VERIFF_DECISION_STATUSES: readonly ["approved", "declined", "resubmission_requested", "review", "expired", "abandoned"];
|
|
61
|
+
export type VeriffDecisionStatus = (typeof VERIFF_DECISION_STATUSES)[number];
|
|
62
|
+
/**
|
|
63
|
+
* The numeric `code` Veriff carries alongside a decision status.
|
|
64
|
+
*
|
|
65
|
+
* Grounded four ways in the decision endpoint's own published document, which is the endpoint this
|
|
66
|
+
* twin serves: its prose list ("`9104` - Expired", "`9121` - Abandoned"), its
|
|
67
|
+
* `session_abandoned_generic` example (`"code":"9121"` with `"status":"abandoned"`), its
|
|
68
|
+
* `session_expired_generic` example (`"code":"9104"` with `"status":"expired"`), and the schema's
|
|
69
|
+
* `code` enum, which is exactly [9001, 9102, 9103, 9104, 9121].
|
|
70
|
+
*
|
|
71
|
+
* `review` therefore gets **null**, not a borrowed code. It is a real status — the decision-webhook
|
|
72
|
+
* page lists it among `approved|declined|resubmission_requested|review|expired|abandoned`, and it is
|
|
73
|
+
* opt-in ("Only if previously agreed with Veriff") — but it appears in NEITHER the decision schema's
|
|
74
|
+
* `status` enum NOR its `code` enum, so no published number belongs to it. Handing it another
|
|
75
|
+
* status's code would be invention, and a consumer branching on `code` would silently treat a
|
|
76
|
+
* manual-review case as abandoned. `veriff.decisions.review_code` is the filed todo.
|
|
77
|
+
*
|
|
78
|
+
* (An earlier revision of this pack mapped `abandoned: 9104` / `review: 9121` on the strength of a
|
|
79
|
+
* status-codes table it had not actually read — a §9 review caught it. Recorded because the lesson
|
|
80
|
+
* generalises: prefer the document you can quote over the one you were told about.)
|
|
81
|
+
*
|
|
82
|
+
* Emitted as a NUMBER, matching the decision-webhook sample (`"code": 9001`) and the schema's
|
|
83
|
+
* declared `"type": "integer"`.
|
|
84
|
+
*/
|
|
85
|
+
export declare const VERIFF_DECISION_CODES: Record<VeriffDecisionStatus, number | null>;
|
|
86
|
+
/** The event-webhook `code` per action. 7001/7002 are the two the event-webhook page documents as
|
|
87
|
+
* needing no extra configuration; the 7007-7011 web-flow actions are a filed todo. */
|
|
88
|
+
export declare const VERIFF_EVENT_CODES: Record<'started' | 'submitted', number>;
|
|
89
|
+
/**
|
|
90
|
+
* The subject types this twin projects.
|
|
91
|
+
*
|
|
92
|
+
* `delivery` is the twin's own outbound-webhook log: every decision/event webhook the twin would
|
|
93
|
+
* have PUSHED is recorded here, signed exactly as it would go on the wire, so a consumer can read
|
|
94
|
+
* one back and verify it offline instead of standing up a receiver.
|
|
95
|
+
*/
|
|
96
|
+
export declare const VERIFF_RESOURCE_TYPES: readonly ["session", "attempt", "media", "delivery"];
|
|
97
|
+
/**
|
|
98
|
+
* Every endpoint this twin DISPATCHES, as a `METHOD /path` template. The conformance check drives
|
|
99
|
+
* one real request per entry and asserts the OUTCOME a live handler produces, so deleting a route —
|
|
100
|
+
* or a single method branch — from the switchboard below turns it red. Its census is a two-way
|
|
101
|
+
* bijection with this list, so adding a route here without implementing it is red too.
|
|
102
|
+
*/
|
|
103
|
+
export declare const VERIFF_IMPLEMENTED_ENDPOINTS: readonly ["POST /v1/sessions", "PATCH /v1/sessions/{sessionId}", "DELETE /v1/sessions/{sessionId}", "GET /v1/sessions/{sessionId}/decision", "GET /v1/sessions/{sessionId}/person", "GET /v1/sessions/{sessionId}/attempts", "GET /v1/sessions/{sessionId}/media", "POST /v1/sessions/{sessionId}/media", "GET /v1/attempts/{attemptId}/media", "GET /v1/media/{mediaId}"];
|
|
104
|
+
export declare function handleVeriffTwinRequest(req: VeriffRequest): Promise<VeriffResponse>;
|