@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,86 @@
|
|
|
1
|
+
import { nodeBuiltin } from '@volter/world-core';
|
|
2
|
+
// Veriff's HMAC request/webhook signing — REAL crypto, reproduced byte-for-byte.
|
|
3
|
+
//
|
|
4
|
+
// Veriff authenticates two directions with the SAME primitive and two different payloads:
|
|
5
|
+
//
|
|
6
|
+
// 1. OUTBOUND (you → Veriff). `X-AUTH-CLIENT` carries the public API key. Endpoints that require
|
|
7
|
+
// signing add `X-HMAC-SIGNATURE` = lowercase-hex HMAC-SHA256 over the **payload**, keyed by
|
|
8
|
+
// the account's shared secret. The payload is the raw JSON request BODY for a request that has
|
|
9
|
+
// one, and the **path parameter itself** (the session id / attempt id / media id) for a GET,
|
|
10
|
+
// which has no body. The real consumer does exactly this — dub's
|
|
11
|
+
// `apps/web/lib/veriff/client.ts` signs `fetchSessionDecision` with
|
|
12
|
+
// `createHmac('sha256', VERIFF_SHARED_SECRET).update(sessionId).digest('hex')` and sends it as
|
|
13
|
+
// `X-HMAC-SIGNATURE`.
|
|
14
|
+
//
|
|
15
|
+
// 2. INBOUND (Veriff → you). Every webhook delivery carries `x-auth-client` (the same public API
|
|
16
|
+
// key, so a receiver can tell WHICH account) and `x-hmac-signature` = lowercase-hex
|
|
17
|
+
// HMAC-SHA256 over the **raw delivered body**, keyed by the same shared secret. dub's
|
|
18
|
+
// `app/api/veriff/webhook/route.ts` verifies exactly this, timing-safe, over `req.text()`.
|
|
19
|
+
//
|
|
20
|
+
// Both are pure local computations over a secret, so a twin can hold the scheme HONESTLY rather
|
|
21
|
+
// than faking it: a consumer that signs wrongly gets the vendor's rejection here too, and a
|
|
22
|
+
// consumer verifying a twin-delivered webhook with the real `crypto` code path passes unchanged.
|
|
23
|
+
//
|
|
24
|
+
// `node:crypto` is required LAZILY (server-only) so this module is safe to import anywhere.
|
|
25
|
+
type NodeCrypto = typeof import('node:crypto');
|
|
26
|
+
function nodeCrypto(): NodeCrypto {
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
28
|
+
return nodeBuiltin('node:crypto') as NodeCrypto;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The header Veriff carries the public API key in (case-insensitive on the wire). */
|
|
32
|
+
export const AUTH_CLIENT_HEADER = 'x-auth-client';
|
|
33
|
+
/** The header Veriff carries the HMAC in (case-insensitive on the wire). */
|
|
34
|
+
export const HMAC_SIGNATURE_HEADER = 'x-hmac-signature';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Compute Veriff's signature for a payload: lowercase-hex HMAC-SHA256(sharedSecret, payload).
|
|
38
|
+
*
|
|
39
|
+
* `payload` is the raw request/webhook body for anything with a body, and the bare path parameter
|
|
40
|
+
* (session id, attempt id, media id) for a GET. Callers pass the exact bytes — this function never
|
|
41
|
+
* re-serializes, because a re-serialized body signs different bytes than the ones sent.
|
|
42
|
+
*/
|
|
43
|
+
export function veriffSignature(payload: string, sharedSecret: string): string {
|
|
44
|
+
return nodeCrypto().createHmac('sha256', sharedSecret).update(payload, 'utf8').digest('hex');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The same signature over RAW BYTES rather than a string.
|
|
49
|
+
*
|
|
50
|
+
* Needed because one response is not JSON: `GET /v1/media/{id}` answers with the media itself under
|
|
51
|
+
* its own mimetype, and the documented `X-HMAC-SIGNATURE` header is "Response body signed with the
|
|
52
|
+
* shared secret key" — the body a receiver actually got. Signing the base64 TEXT the twin happens to
|
|
53
|
+
* store it as produces a digest that verifies against nothing the consumer holds; a §9 round-two
|
|
54
|
+
* review measured exactly that mismatch before this existed.
|
|
55
|
+
*/
|
|
56
|
+
export function veriffSignatureBytes(payload: Uint8Array, sharedSecret: string): string {
|
|
57
|
+
return nodeCrypto().createHmac('sha256', sharedSecret).update(payload).digest('hex');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Constant-time compare of two signature strings.
|
|
62
|
+
*
|
|
63
|
+
* Length is compared first and NOT with `timingSafeEqual` — the node primitive THROWS on a length
|
|
64
|
+
* mismatch rather than returning false, so an unequal-length forgery would crash the handler
|
|
65
|
+
* instead of being rejected. The real consumer (dub) guards the same way.
|
|
66
|
+
*/
|
|
67
|
+
export function signaturesMatch(a: string, b: string): boolean {
|
|
68
|
+
const ab = Buffer.from(a, 'utf8');
|
|
69
|
+
const bb = Buffer.from(b, 'utf8');
|
|
70
|
+
if (ab.length !== bb.length) return false;
|
|
71
|
+
return nodeCrypto().timingSafeEqual(Uint8Array.from(ab), Uint8Array.from(bb));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Verify a presented signature against the payload. Returns the reason it failed, or `null` when
|
|
76
|
+
* it is valid — so a caller can map a MISSING header and a WRONG one to the different statuses
|
|
77
|
+
* Veriff uses for them.
|
|
78
|
+
*/
|
|
79
|
+
export function verifyVeriffSignature(
|
|
80
|
+
payload: string,
|
|
81
|
+
presented: string | undefined | null,
|
|
82
|
+
sharedSecret: string,
|
|
83
|
+
): 'missing' | 'mismatch' | null {
|
|
84
|
+
if (presented === undefined || presented === null || presented === '') return 'missing';
|
|
85
|
+
return signaturesMatch(veriffSignature(payload, sharedSecret), presented) ? null : 'mismatch';
|
|
86
|
+
}
|