@thirdfactor/web 1.0.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/README.md +311 -0
- package/dist/client.d.ts +22 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +63 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/modal.d.ts +3 -0
- package/dist/modal.d.ts.map +1 -0
- package/dist/modal.js +121 -0
- package/dist/modal.js.map +1 -0
- package/dist/popup.d.ts +3 -0
- package/dist/popup.d.ts.map +1 -0
- package/dist/popup.js +49 -0
- package/dist/popup.js.map +1 -0
- package/dist/protocol.d.ts +27 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +92 -0
- package/dist/protocol.js.map +1 -0
- package/dist/runner.d.ts +16 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +84 -0
- package/dist/runner.js.map +1 -0
- package/dist/server.d.ts +57 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +82 -0
- package/dist/server.js.map +1 -0
- package/dist/types.d.ts +69 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +14 -0
- package/dist/types.js.map +1 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
# @thirdfactor/web
|
|
2
|
+
|
|
3
|
+
Web client SDK for the **ThirdFactor / Obsidian** native KYC engine. It opens the
|
|
4
|
+
hosted verification flow in a **popup**, a cross-origin **modal** iframe, or a
|
|
5
|
+
full-page **redirect**, listens to the flow over the shared bridge protocol, and
|
|
6
|
+
resolves a single typed `VerificationResult`.
|
|
7
|
+
|
|
8
|
+
The SDK is a thin shell: your backend creates the session and gets a hosted `url`;
|
|
9
|
+
the browser hands that `url` to this SDK. Session creation needs your **secret API
|
|
10
|
+
key** and is done on your server (an optional `@thirdfactor/web/server` helper is
|
|
11
|
+
included).
|
|
12
|
+
|
|
13
|
+
> **The client result is advisory.** It is a UX signal only. Grant access / move
|
|
14
|
+
> money **only after** your backend confirms the decision from the signed webhook
|
|
15
|
+
> (`identity.kyc.session.approved` / `.declined` / `.review` / `.expired`) or
|
|
16
|
+
> `GET /v3/session/<id>/decision/`. A user can close the tab after approval or
|
|
17
|
+
> tamper with the browser; the webhook is HMAC-signed and cannot be spoofed.
|
|
18
|
+
|
|
19
|
+
- **Bridge protocol:** [`../../docs/sdk/PROTOCOL.md`](../../docs/sdk/PROTOCOL.md)
|
|
20
|
+
- **REST reference:** [`../../OBSIDIAN_API.md`](../../OBSIDIAN_API.md)
|
|
21
|
+
|
|
22
|
+
## Requirements
|
|
23
|
+
|
|
24
|
+
| | |
|
|
25
|
+
| --- | --- |
|
|
26
|
+
| Browser | Any modern evergreen browser (Chromium, Firefox, Safari) with `postMessage` + camera (`getUserMedia`). |
|
|
27
|
+
| Bundler / module system | ESM. Ships `.js` + `.d.ts`; works with Vite, webpack, Next.js, etc. |
|
|
28
|
+
| Backend (for the session helper) | Node 18+ (global `fetch`) or any runtime with a WHATWG `fetch`. |
|
|
29
|
+
| TypeScript | Optional; full types are bundled. |
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install @thirdfactor/web
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Two entry points, deliberately separated so your **browser bundle never imports
|
|
38
|
+
anything that touches your API key**:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { ThirdFactor } from "@thirdfactor/web"; // browser
|
|
42
|
+
import { createSession } from "@thirdfactor/web/server"; // server only
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Permissions
|
|
46
|
+
|
|
47
|
+
The hosted flow captures the camera (document + selfie/liveness) and may use the
|
|
48
|
+
microphone. There is no manifest on the web — the browser prompts the user the
|
|
49
|
+
first time the flow calls `getUserMedia`. What matters is the **browsing context**
|
|
50
|
+
the camera runs in:
|
|
51
|
+
|
|
52
|
+
- **`popup`** (and `redirect`) run the flow in a top-level context, so the camera
|
|
53
|
+
prompt and stream work everywhere, including iOS Safari.
|
|
54
|
+
- **`modal`** runs the flow in a cross-origin iframe. The browser only grants it
|
|
55
|
+
camera/mic if the flow deployment allowlists your origin (see
|
|
56
|
+
[Configuration → `modal`](#enabling-modal-cross-origin-iframe)), and camera in a
|
|
57
|
+
cross-origin iframe is still unreliable on iOS Safari.
|
|
58
|
+
|
|
59
|
+
The SDK already sets `allow="camera; microphone"` on the modal iframe; your app's
|
|
60
|
+
own CSP `frame-src` must also permit the flow origin when you use `modal`.
|
|
61
|
+
|
|
62
|
+
## Quick Start
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
// 1) Backend — create the session (secret key, never in the browser).
|
|
66
|
+
import { createSession } from "@thirdfactor/web/server";
|
|
67
|
+
const session = await createSession(
|
|
68
|
+
{ baseUrl: process.env.TF_BASE_URL!, apiKey: process.env.TF_API_KEY! },
|
|
69
|
+
{ vendorData: user.id, callbackUrl: "https://app.acme.com/kyc/done" },
|
|
70
|
+
);
|
|
71
|
+
// send { url: session.url, id: session.id } to the browser
|
|
72
|
+
|
|
73
|
+
// 2) Browser — open the flow from a real click.
|
|
74
|
+
import { ThirdFactor } from "@thirdfactor/web";
|
|
75
|
+
button.addEventListener("click", async () => {
|
|
76
|
+
const result = await ThirdFactor.verify({ url: session.url });
|
|
77
|
+
if (result.status === "approved") showSuccess(); // then confirm server-side
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
> Call `verify()` from a genuine click/tap — popup mode is blocked otherwise.
|
|
82
|
+
|
|
83
|
+
## Integration Methods
|
|
84
|
+
|
|
85
|
+
### Method 1 — Backend session helper (recommended)
|
|
86
|
+
|
|
87
|
+
`@thirdfactor/web/server` wraps `POST /v3/session/` so you don't hand-roll the
|
|
88
|
+
request. It runs **only** on your server.
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { createSession } from "@thirdfactor/web/server";
|
|
92
|
+
|
|
93
|
+
const session = await createSession(
|
|
94
|
+
{ baseUrl: process.env.TF_BASE_URL!, apiKey: process.env.TF_API_KEY! },
|
|
95
|
+
{
|
|
96
|
+
vendorData: user.id, // your reference for this user
|
|
97
|
+
callbackUrl: "https://app.acme.com/kyc/done",
|
|
98
|
+
contactEmail: user.email,
|
|
99
|
+
locale: "en", // hosted-flow language
|
|
100
|
+
prefill: { full_name: user.name }, // pre-populate the flow
|
|
101
|
+
idempotencyKey: `kyc-${user.id}`, // safe retries, never double-charged
|
|
102
|
+
},
|
|
103
|
+
);
|
|
104
|
+
// → { id, url, status, decision, expiresAt, ... }; send { url, id } to the browser
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
On a non-2xx response it throws `ThirdFactorApiError` with `.status` and a stable
|
|
108
|
+
`.code` (e.g. `insufficient_credits`).
|
|
109
|
+
|
|
110
|
+
### Method 2 — Any backend + `verify({ url })`
|
|
111
|
+
|
|
112
|
+
The helper is optional. `POST /v3/session/` is a single authenticated call; create
|
|
113
|
+
the session however you like (cURL / Python / Go — see
|
|
114
|
+
[`../../docs/sdk/README.md`](../../docs/sdk/README.md)) and pass the returned `url`
|
|
115
|
+
to `ThirdFactor.verify({ url })`. The browser side is identical either way.
|
|
116
|
+
|
|
117
|
+
### Presentation modes
|
|
118
|
+
|
|
119
|
+
| Mode | Call | Best for | Notes |
|
|
120
|
+
| --- | --- | --- | --- |
|
|
121
|
+
| `auto` (default) | `verify({ url })` | Anything | Resolves to **popup** — a top-level context with reliable camera everywhere. |
|
|
122
|
+
| `popup` | `verify({ url, mode: "popup" })` | Mobile web, zero-config | New window; most reliable camera. |
|
|
123
|
+
| `modal` | `verify({ url, mode: "modal" })` | Desktop, in-page UX | Cross-origin iframe. **Opt-in** — the flow must allowlist your origin. |
|
|
124
|
+
| `redirect` | `ThirdFactor.redirect(url)` | Max compatibility | Full-page navigate; does not return a Promise. Result arrives via `callback_url` + webhook. |
|
|
125
|
+
|
|
126
|
+
## Configuration
|
|
127
|
+
|
|
128
|
+
### `ThirdFactor.verify(options): Promise<VerificationResult>`
|
|
129
|
+
|
|
130
|
+
Never rejects — always resolves a `VerificationResult`.
|
|
131
|
+
|
|
132
|
+
| Option | Type | Default | Notes |
|
|
133
|
+
| --- | --- | --- | --- |
|
|
134
|
+
| `url` | `string` | — (required) | Hosted session URL from `POST /v3/session/`. |
|
|
135
|
+
| `mode` | `"modal" \| "popup" \| "auto"` | `"auto"` | `auto` = popup. `modal` is opt-in (see below). |
|
|
136
|
+
| `onEvent` | `(e: VerificationEvent) => void` | — | Called for every lifecycle event: `ready` / `completed` / `error` / `close`. |
|
|
137
|
+
| `title` | `string` | `"Identity verification"` | Accessible aria-label for the modal dialog. |
|
|
138
|
+
| `autoClose` | `boolean` | `true` | `false` keeps the flow's own result screen up until the user taps Done. |
|
|
139
|
+
| `expectedOrigin` | `string` | origin of `url` | Only override behind a proxy that serves the flow on a different origin. |
|
|
140
|
+
|
|
141
|
+
`ThirdFactor.redirect(url: string): void` navigates the whole page to the flow and
|
|
142
|
+
does not return.
|
|
143
|
+
|
|
144
|
+
### `createSession(config, input)` — server helper
|
|
145
|
+
|
|
146
|
+
`config`: `{ baseUrl, apiKey, fetchImpl? }`. `input` (all optional):
|
|
147
|
+
|
|
148
|
+
| Field | Type | Maps to `POST /v3/session/` | Notes |
|
|
149
|
+
| --- | --- | --- | --- |
|
|
150
|
+
| `workflowId` | `string` | `workflow_id` | KYC workflow UUID; tenant default when omitted. |
|
|
151
|
+
| `vendorData` | `string` | `vendor_data` | Your external user reference (also the linked identity's external id). |
|
|
152
|
+
| `callbackUrl` | `string` | `callback_url` | Where the applicant is redirected after finishing. |
|
|
153
|
+
| `contactEmail` / `contactPhone` | `string` | `contact_email` / `contact_phone` | OTP + notifications. |
|
|
154
|
+
| `expiresInHours` | `number` | `expires_in_hours` | Hosted-URL lifetime, `1..720` (default `48`). |
|
|
155
|
+
| `locale` | `string` | `locale` | Hosted-flow language (default `en`). |
|
|
156
|
+
| `prefill` | `Record<string,string>` | `prefill` | Applicant fields to pre-populate (only documented keys honored). |
|
|
157
|
+
| `idempotencyKey` | `string` | `Idempotency-Key` header | Repeat creates return the original session, uncharged. |
|
|
158
|
+
|
|
159
|
+
See [`OBSIDIAN_API.md`](../../OBSIDIAN_API.md#create-verification-session) for the
|
|
160
|
+
authoritative field list.
|
|
161
|
+
|
|
162
|
+
### Enabling `modal` (cross-origin iframe)
|
|
163
|
+
|
|
164
|
+
`modal` embeds the flow in an iframe on **your** origin. For that to work the flow
|
|
165
|
+
deployment must allowlist your app origin — otherwise the browser refuses the frame
|
|
166
|
+
(`X-Frame-Options`) and denies the iframe camera (`Permissions-Policy`). On the
|
|
167
|
+
Obsidian flow deployment set:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
FLOW_EMBED_ORIGINS="https://app.acme.com https://staging.acme.com"
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
This adds your origins to the flow route's `frame-ancestors` and grants them
|
|
174
|
+
`camera`/`microphone`. Every other route keeps `X-Frame-Options: DENY`. With it
|
|
175
|
+
unset (the default), cross-origin `modal` is refused by design — use `auto` (popup)
|
|
176
|
+
or `redirect`. Even correctly allowlisted, camera in a cross-origin iframe is
|
|
177
|
+
unreliable on iOS Safari; that's why `auto` uses popup.
|
|
178
|
+
|
|
179
|
+
## Language / Localization
|
|
180
|
+
|
|
181
|
+
The hosted flow's language and copy are chosen at **session-create time**, not in
|
|
182
|
+
the browser SDK. Pass `locale` to `createSession` (or `"locale"` in the raw
|
|
183
|
+
`POST /v3/session/` body); the SDK renders whatever the flow serves — no
|
|
184
|
+
per-platform i18n code. See
|
|
185
|
+
[`OBSIDIAN_API.md`](../../OBSIDIAN_API.md#create-verification-session).
|
|
186
|
+
|
|
187
|
+
## Advanced Options
|
|
188
|
+
|
|
189
|
+
Prefill, expected identity details, and metadata are also **session-create
|
|
190
|
+
concerns**, not browser options:
|
|
191
|
+
|
|
192
|
+
- **Prefill** — pass `prefill` to `createSession` to pre-populate the flow
|
|
193
|
+
(`full_name`, `first_name`, `last_name`, `display_name`, `date_of_birth`,
|
|
194
|
+
`email`, `phone`, `nationality`, `address_line1/2`, `city`, `state`, `country`,
|
|
195
|
+
`zip_code`). Unknown keys are ignored.
|
|
196
|
+
- **Metadata / your reference** — `vendorData` becomes the session's external
|
|
197
|
+
reference and the linked identity's `external_user_id`; it comes back on the
|
|
198
|
+
session and every webhook.
|
|
199
|
+
- **Workflow selection** — `workflowId` picks a specific KYC workflow; omit it for
|
|
200
|
+
the tenant default.
|
|
201
|
+
|
|
202
|
+
Browser-side advanced knobs are just `expectedOrigin` (proxy override) and
|
|
203
|
+
`autoClose` (keep the flow's result screen visible until Done).
|
|
204
|
+
|
|
205
|
+
## Result Handling
|
|
206
|
+
|
|
207
|
+
Every SDK resolves the same small vocabulary:
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
interface VerificationResult {
|
|
211
|
+
status: "approved" | "declined" | "in_review" | "cancelled" | "error";
|
|
212
|
+
sessionId?: string; // echoed from the flow when available
|
|
213
|
+
error?: { code: string; message: string }; // present only when status === "error"
|
|
214
|
+
rawStatus?: string; // raw server status (e.g. "abandoned")
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
| `status` | When | Your move |
|
|
219
|
+
| --- | --- | --- |
|
|
220
|
+
| `approved` | Flow approved the applicant | Continue (after server confirm) |
|
|
221
|
+
| `declined` | Flow rejected the applicant | Block / offer appeal |
|
|
222
|
+
| `in_review` | Needs an operator / AML hit / any unknown terminal status (conservative) | Show "pending", wait for webhook |
|
|
223
|
+
| `cancelled` | User closed the surface, or server `abandoned` | Let them retry |
|
|
224
|
+
| `error` | Invalid/expired session or load failure | Surface `error.code`, retry / re-mint session |
|
|
225
|
+
|
|
226
|
+
### Error cases
|
|
227
|
+
|
|
228
|
+
`status === "error"` carries a stable `error.code` — branch on it, never on
|
|
229
|
+
`error.message`:
|
|
230
|
+
|
|
231
|
+
| `error.code` | Cause |
|
|
232
|
+
| --- | --- |
|
|
233
|
+
| `invalid_session` | Token is not a valid session. |
|
|
234
|
+
| `expired` | Session/URL expired. |
|
|
235
|
+
| `popup_blocked` | `verify()` was not called from a user gesture. |
|
|
236
|
+
| `load_error` | The flow surface failed to load. |
|
|
237
|
+
| `missing_url` | `options.url` was empty. |
|
|
238
|
+
| `no_window` | `verify()` ran outside a browser. |
|
|
239
|
+
| `network` / `unknown` | Transport failure / unrecognized error. |
|
|
240
|
+
|
|
241
|
+
### Full example
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
import { ThirdFactor } from "@thirdfactor/web";
|
|
245
|
+
|
|
246
|
+
const result = await ThirdFactor.verify({
|
|
247
|
+
url: session.url,
|
|
248
|
+
mode: "auto",
|
|
249
|
+
autoClose: true,
|
|
250
|
+
onEvent: (e) => console.log(e.type), // ready | completed | error | close
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
switch (result.status) {
|
|
254
|
+
case "approved": showSuccess(); break; // confirm server-side before trusting
|
|
255
|
+
case "declined": showFailure(); break;
|
|
256
|
+
case "in_review": showPending(); break;
|
|
257
|
+
case "cancelled": /* user closed the flow — let them retry */ break;
|
|
258
|
+
case "error":
|
|
259
|
+
if (result.error?.code === "expired") reMintSession();
|
|
260
|
+
else showError(result.error);
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## NFC Verification
|
|
266
|
+
|
|
267
|
+
**The web SDK has no NFC module.** Reading an ePassport / eID chip requires the
|
|
268
|
+
device NFC radio, which a browser/WebView cannot reach. NFC is therefore a
|
|
269
|
+
**native-only** capability, implemented in the mobile SDKs:
|
|
270
|
+
|
|
271
|
+
- [iOS — `ThirdFactorKYCNFC`](../ios/README.md#nfc-verification-epassport--eid)
|
|
272
|
+
- [Android — `ThirdFactorNfc`](../android/README.md#nfc-verification-epassport--eid)
|
|
273
|
+
- [Flutter — `ThirdFactorNfc`](../flutter/README.md#nfc-verification-epassport--eid)
|
|
274
|
+
|
|
275
|
+
On the web, when a workflow includes an `NFC_VERIFICATION` step, the flow
|
|
276
|
+
advertises no `nfc` capability, so the server-side fallback applies (the NFC step
|
|
277
|
+
resolves to manual review). See [`PROTOCOL.md` §8](../../docs/sdk/PROTOCOL.md#8-native-capability-channel-bidirectional).
|
|
278
|
+
|
|
279
|
+
## End-to-End Workflow
|
|
280
|
+
|
|
281
|
+
```
|
|
282
|
+
1. Backend POST /v3/session/ (x-api-key: SECRET) → { id, url }
|
|
283
|
+
2. Backend send { url } to the browser
|
|
284
|
+
3. Browser ThirdFactor.verify({ url }) → opens url?embed=1&sdk=web
|
|
285
|
+
4. Flow bridge events over postMessage → SDK resolves ONE advisory result
|
|
286
|
+
5. Backend AUTHORITATIVE decision — do this before granting access:
|
|
287
|
+
webhook identity.kyc.session.{approved|declined|review|expired}
|
|
288
|
+
or GET /v3/session/<id>/decision/
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Webhook signature verification (`X-Webhook-Signature: t=…,v1=…`) is documented in
|
|
292
|
+
[`OBSIDIAN_API.md`](../../OBSIDIAN_API.md#verifying-webhook-signatures).
|
|
293
|
+
|
|
294
|
+
### How it works
|
|
295
|
+
|
|
296
|
+
`verify()` opens the hosted flow with `?embed=1&sdk=web` appended. In embed mode
|
|
297
|
+
the flow reports lifecycle over `postMessage` instead of redirecting. The SDK
|
|
298
|
+
validates every message against the flow's origin and the `thirdfactor` source tag
|
|
299
|
+
before acting. The same bridge protocol backs the Flutter, iOS, and Android SDKs.
|
|
300
|
+
|
|
301
|
+
## Local development
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
npm install
|
|
305
|
+
npm run build
|
|
306
|
+
# exercise the SDK against a mock flow — no backend required:
|
|
307
|
+
npx http-server . -p 8899 # then visit /example/index.html
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
`example/` contains a self-contained test harness (`index.html`) and a mock flow
|
|
311
|
+
(`mock-flow.html`) that speaks the real bridge protocol.
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { VerificationResult, VerifyOptions } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Open the hosted verification flow and resolve once it reaches a terminal
|
|
4
|
+
* state, the user cancels, or an error occurs. Never rejects — inspect
|
|
5
|
+
* `result.status` (and `result.error` when it is `"error"`).
|
|
6
|
+
*
|
|
7
|
+
* Call this from a user gesture (click/tap) so popup mode is not blocked.
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* const result = await ThirdFactor.verify({ url: session.url });
|
|
11
|
+
* if (result.status === "approved") showSuccess();
|
|
12
|
+
* // Then confirm server-side before trusting it.
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export declare function verify(options: VerifyOptions): Promise<VerificationResult>;
|
|
16
|
+
/**
|
|
17
|
+
* Navigate the whole page to the hosted flow. The flow returns the user to the
|
|
18
|
+
* session's `callback_url` on completion; read the authoritative decision from
|
|
19
|
+
* your backend / webhook there. Does not return.
|
|
20
|
+
*/
|
|
21
|
+
export declare function redirect(url: string): void;
|
|
22
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,kBAAkB,EAAc,aAAa,EAAE,MAAM,YAAY,CAAC;AAYhF;;;;;;;;;;;;GAYG;AACH,wBAAgB,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAqB1E;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAK1C"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Public entry points: verify() (modal/popup, resolves a result) and
|
|
2
|
+
// redirect() (full-page navigation, result arrives via your callback_url +
|
|
3
|
+
// webhook).
|
|
4
|
+
import { createModalSurface } from "./modal.js";
|
|
5
|
+
import { createPopupSurface } from "./popup.js";
|
|
6
|
+
import { originOf, withEmbedParams } from "./protocol.js";
|
|
7
|
+
import { runSession } from "./runner.js";
|
|
8
|
+
function resolveMode(mode) {
|
|
9
|
+
if (mode === "modal" || mode === "popup")
|
|
10
|
+
return mode;
|
|
11
|
+
// `auto`: popup is a top-level browsing context, so it works with the flow's
|
|
12
|
+
// default (strict, anti-clickjacking) headers AND has the most reliable
|
|
13
|
+
// camera access on every platform. Cross-origin `modal` needs the operator to
|
|
14
|
+
// allowlist the app origin via FLOW_EMBED_ORIGINS on the flow deployment, and
|
|
15
|
+
// that can't be feature-detected from here — so it stays explicit opt-in.
|
|
16
|
+
return "popup";
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Open the hosted verification flow and resolve once it reaches a terminal
|
|
20
|
+
* state, the user cancels, or an error occurs. Never rejects — inspect
|
|
21
|
+
* `result.status` (and `result.error` when it is `"error"`).
|
|
22
|
+
*
|
|
23
|
+
* Call this from a user gesture (click/tap) so popup mode is not blocked.
|
|
24
|
+
*
|
|
25
|
+
* ```ts
|
|
26
|
+
* const result = await ThirdFactor.verify({ url: session.url });
|
|
27
|
+
* if (result.status === "approved") showSuccess();
|
|
28
|
+
* // Then confirm server-side before trusting it.
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export function verify(options) {
|
|
32
|
+
if (typeof window === "undefined") {
|
|
33
|
+
return Promise.resolve({
|
|
34
|
+
status: "error",
|
|
35
|
+
error: { code: "no_window", message: "verify() must run in a browser." },
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
if (!options?.url) {
|
|
39
|
+
return Promise.resolve({
|
|
40
|
+
status: "error",
|
|
41
|
+
error: { code: "missing_url", message: "options.url is required." },
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
const mode = resolveMode(options.mode);
|
|
45
|
+
const expectedOrigin = options.expectedOrigin || originOf(options.url);
|
|
46
|
+
const embeddedUrl = withEmbedParams(options.url, "web");
|
|
47
|
+
const title = options.title || "Identity verification";
|
|
48
|
+
const surface = mode === "popup" ? createPopupSurface() : createModalSurface(title);
|
|
49
|
+
return runSession(embeddedUrl, expectedOrigin, options, surface);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Navigate the whole page to the hosted flow. The flow returns the user to the
|
|
53
|
+
* session's `callback_url` on completion; read the authoritative decision from
|
|
54
|
+
* your backend / webhook there. Does not return.
|
|
55
|
+
*/
|
|
56
|
+
export function redirect(url) {
|
|
57
|
+
if (typeof window === "undefined")
|
|
58
|
+
return;
|
|
59
|
+
// Redirect mode has no embedding host, so the flow uses its normal
|
|
60
|
+
// callback_url redirect — do NOT append the embed flag.
|
|
61
|
+
window.location.assign(url);
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,2EAA2E;AAC3E,YAAY;AAEZ,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAGzC,SAAS,WAAW,CAAC,IAA4B;IAC/C,IAAI,IAAI,KAAK,OAAO,IAAI,IAAI,KAAK,OAAO;QAAE,OAAO,IAAI,CAAC;IACtD,6EAA6E;IAC7E,wEAAwE;IACxE,8EAA8E;IAC9E,8EAA8E;IAC9E,0EAA0E;IAC1E,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,MAAM,CAAC,OAAsB;IAC3C,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE,CAAC;QAClC,OAAO,OAAO,CAAC,OAAO,CAAC;YACrB,MAAM,EAAE,OAAO;YACf,KAAK,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,iCAAiC,EAAE;SACzE,CAAC,CAAC;IACL,CAAC;IACD,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC;QAClB,OAAO,OAAO,CAAC,OAAO,CAAC;YACrB,MAAM,EAAE,OAAO;YACf,KAAK,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,0BAA0B,EAAE;SACpE,CAAC,CAAC;IACL,CAAC;IAED,MAAM,IAAI,GAAG,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,IAAI,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACvE,MAAM,WAAW,GAAG,eAAe,CAAC,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACxD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,uBAAuB,CAAC;IAEvD,MAAM,OAAO,GAAG,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,kBAAkB,EAAE,CAAC,CAAC,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC;IACpF,OAAO,UAAU,CAAC,WAAW,EAAE,cAAc,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;AACnE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,QAAQ,CAAC,GAAW;IAClC,IAAI,OAAO,MAAM,KAAK,WAAW;QAAE,OAAO;IAC1C,mEAAmE;IACnE,wDAAwD;IACxD,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;AAC9B,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { redirect, verify } from "./client.js";
|
|
2
|
+
export { verify, redirect };
|
|
3
|
+
export type { VerificationEvent, VerificationError, VerificationResult, VerificationStatus, VerifyMode, VerifyOptions, } from "./types.js";
|
|
4
|
+
export { BRIDGE_SOURCE, normalizeStatus, parseBridgeMessage } from "./protocol.js";
|
|
5
|
+
/** Namespace object for `import { ThirdFactor } from "@thirdfactor/web"`. */
|
|
6
|
+
export declare const ThirdFactor: {
|
|
7
|
+
readonly verify: typeof verify;
|
|
8
|
+
readonly redirect: typeof redirect;
|
|
9
|
+
};
|
|
10
|
+
export default ThirdFactor;
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE/C,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;AAC5B,YAAY,EACV,iBAAiB,EACjB,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,UAAU,EACV,aAAa,GACd,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAEnF,6EAA6E;AAC7E,eAAO,MAAM,WAAW;;;CAAgC,CAAC;AAEzD,eAAe,WAAW,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// @thirdfactor/web — browser entry point.
|
|
2
|
+
//
|
|
3
|
+
// Server-side session creation lives in the separate "@thirdfactor/web/server"
|
|
4
|
+
// entry so the browser bundle never imports anything that touches your API key.
|
|
5
|
+
import { redirect, verify } from "./client.js";
|
|
6
|
+
export { verify, redirect };
|
|
7
|
+
export { BRIDGE_SOURCE, normalizeStatus, parseBridgeMessage } from "./protocol.js";
|
|
8
|
+
/** Namespace object for `import { ThirdFactor } from "@thirdfactor/web"`. */
|
|
9
|
+
export const ThirdFactor = { verify, redirect };
|
|
10
|
+
export default ThirdFactor;
|
|
11
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,0CAA0C;AAC1C,EAAE;AACF,+EAA+E;AAC/E,gFAAgF;AAEhF,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE/C,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;AAS5B,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAEnF,6EAA6E;AAC7E,MAAM,CAAC,MAAM,WAAW,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAW,CAAC;AAEzD,eAAe,WAAW,CAAC"}
|
package/dist/modal.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"modal.d.ts","sourceRoot":"","sources":["../src/modal.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAK3C,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAyHzD"}
|
package/dist/modal.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Full-screen overlay modal surface: a branded backdrop + centered iframe with
|
|
2
|
+
// a close affordance. Camera/microphone are granted to the iframe via `allow`;
|
|
3
|
+
// note that some mobile browsers (notably iOS Safari) restrict getUserMedia in
|
|
4
|
+
// cross-origin iframes — that is why `mode: "auto"` prefers a popup on mobile.
|
|
5
|
+
const OVERLAY_ID = "tf-verify-overlay";
|
|
6
|
+
const Z = 2147483000; // just under max, above app chrome
|
|
7
|
+
export function createModalSurface(title) {
|
|
8
|
+
let overlay = null;
|
|
9
|
+
let cancelHandler = null;
|
|
10
|
+
let onKeyDown = null;
|
|
11
|
+
let priorOverflow = "";
|
|
12
|
+
let priorActive = null;
|
|
13
|
+
const triggerCancel = () => cancelHandler?.();
|
|
14
|
+
return {
|
|
15
|
+
onCancel(handler) {
|
|
16
|
+
cancelHandler = handler;
|
|
17
|
+
},
|
|
18
|
+
mount(url) {
|
|
19
|
+
priorActive = document.activeElement;
|
|
20
|
+
priorOverflow = document.body.style.overflow;
|
|
21
|
+
document.body.style.overflow = "hidden"; // lock background scroll
|
|
22
|
+
overlay = document.createElement("div");
|
|
23
|
+
overlay.id = OVERLAY_ID;
|
|
24
|
+
overlay.setAttribute("role", "dialog");
|
|
25
|
+
overlay.setAttribute("aria-modal", "true");
|
|
26
|
+
overlay.setAttribute("aria-label", title);
|
|
27
|
+
Object.assign(overlay.style, {
|
|
28
|
+
position: "fixed",
|
|
29
|
+
inset: "0",
|
|
30
|
+
zIndex: String(Z),
|
|
31
|
+
display: "flex",
|
|
32
|
+
alignItems: "center",
|
|
33
|
+
justifyContent: "center",
|
|
34
|
+
padding: "0",
|
|
35
|
+
background: "rgba(15, 23, 42, 0.55)",
|
|
36
|
+
backdropFilter: "blur(2px)",
|
|
37
|
+
// Fade in without a stylesheet dependency.
|
|
38
|
+
animation: "none",
|
|
39
|
+
opacity: "1",
|
|
40
|
+
});
|
|
41
|
+
const frameWrap = document.createElement("div");
|
|
42
|
+
Object.assign(frameWrap.style, {
|
|
43
|
+
position: "relative",
|
|
44
|
+
width: "100%",
|
|
45
|
+
height: "100%",
|
|
46
|
+
maxWidth: "480px",
|
|
47
|
+
maxHeight: "820px",
|
|
48
|
+
background: "#fff",
|
|
49
|
+
borderRadius: "16px",
|
|
50
|
+
overflow: "hidden",
|
|
51
|
+
boxShadow: "0 24px 60px rgba(0,0,0,0.35)",
|
|
52
|
+
});
|
|
53
|
+
// On small viewports go edge-to-edge.
|
|
54
|
+
if (window.innerWidth < 520 || window.innerHeight < 860) {
|
|
55
|
+
frameWrap.style.maxWidth = "100%";
|
|
56
|
+
frameWrap.style.maxHeight = "100%";
|
|
57
|
+
frameWrap.style.borderRadius = "0";
|
|
58
|
+
}
|
|
59
|
+
const iframe = document.createElement("iframe");
|
|
60
|
+
iframe.src = url;
|
|
61
|
+
iframe.title = title;
|
|
62
|
+
iframe.allow = "camera; microphone; fullscreen; geolocation; clipboard-write";
|
|
63
|
+
iframe.setAttribute("allowfullscreen", "true");
|
|
64
|
+
Object.assign(iframe.style, {
|
|
65
|
+
width: "100%",
|
|
66
|
+
height: "100%",
|
|
67
|
+
border: "0",
|
|
68
|
+
display: "block",
|
|
69
|
+
});
|
|
70
|
+
const closeBtn = document.createElement("button");
|
|
71
|
+
closeBtn.type = "button";
|
|
72
|
+
closeBtn.setAttribute("aria-label", "Close verification");
|
|
73
|
+
closeBtn.innerHTML =
|
|
74
|
+
'<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">' +
|
|
75
|
+
'<path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" stroke-width="1.75" stroke-linecap="round"/></svg>';
|
|
76
|
+
Object.assign(closeBtn.style, {
|
|
77
|
+
position: "absolute",
|
|
78
|
+
top: "12px",
|
|
79
|
+
right: "12px",
|
|
80
|
+
width: "32px",
|
|
81
|
+
height: "32px",
|
|
82
|
+
display: "grid",
|
|
83
|
+
placeItems: "center",
|
|
84
|
+
borderRadius: "9999px",
|
|
85
|
+
border: "0",
|
|
86
|
+
cursor: "pointer",
|
|
87
|
+
color: "#0f172a",
|
|
88
|
+
background: "rgba(255,255,255,0.9)",
|
|
89
|
+
boxShadow: "0 1px 4px rgba(0,0,0,0.2)",
|
|
90
|
+
});
|
|
91
|
+
closeBtn.addEventListener("click", triggerCancel);
|
|
92
|
+
frameWrap.appendChild(iframe);
|
|
93
|
+
frameWrap.appendChild(closeBtn);
|
|
94
|
+
overlay.appendChild(frameWrap);
|
|
95
|
+
document.body.appendChild(overlay);
|
|
96
|
+
// Esc closes; backdrop click closes.
|
|
97
|
+
onKeyDown = (e) => {
|
|
98
|
+
if (e.key === "Escape")
|
|
99
|
+
triggerCancel();
|
|
100
|
+
};
|
|
101
|
+
document.addEventListener("keydown", onKeyDown);
|
|
102
|
+
overlay.addEventListener("click", (e) => {
|
|
103
|
+
if (e.target === overlay)
|
|
104
|
+
triggerCancel();
|
|
105
|
+
});
|
|
106
|
+
closeBtn.focus();
|
|
107
|
+
},
|
|
108
|
+
unmount() {
|
|
109
|
+
if (onKeyDown) {
|
|
110
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
111
|
+
onKeyDown = null;
|
|
112
|
+
}
|
|
113
|
+
overlay?.remove();
|
|
114
|
+
overlay = null;
|
|
115
|
+
document.body.style.overflow = priorOverflow;
|
|
116
|
+
if (priorActive instanceof HTMLElement)
|
|
117
|
+
priorActive.focus();
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=modal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"modal.js","sourceRoot":"","sources":["../src/modal.ts"],"names":[],"mappings":"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"}
|
package/dist/popup.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"popup.d.ts","sourceRoot":"","sources":["../src/popup.ts"],"names":[],"mappings":"AAIA,OAAO,EAAqB,KAAK,OAAO,EAAE,MAAM,aAAa,CAAC;AAE9D,wBAAgB,kBAAkB,IAAI,OAAO,CA6C5C"}
|
package/dist/popup.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Popup / new-tab surface. A top-level browsing context has the most reliable
|
|
2
|
+
// camera access, which is why `mode: "auto"` uses it on mobile. Must be invoked
|
|
3
|
+
// synchronously from a user gesture or the browser will block window.open.
|
|
4
|
+
import { PopupBlockedError } from "./runner.js";
|
|
5
|
+
export function createPopupSurface() {
|
|
6
|
+
let popup = null;
|
|
7
|
+
let poll = null;
|
|
8
|
+
let cancelHandler = null;
|
|
9
|
+
const stopPoll = () => {
|
|
10
|
+
if (poll !== null) {
|
|
11
|
+
window.clearInterval(poll);
|
|
12
|
+
poll = null;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
return {
|
|
16
|
+
onCancel(handler) {
|
|
17
|
+
cancelHandler = handler;
|
|
18
|
+
},
|
|
19
|
+
mount(url) {
|
|
20
|
+
const w = 480;
|
|
21
|
+
const h = Math.min(820, window.screen.availHeight || 820);
|
|
22
|
+
const left = window.screenX + Math.max(0, ((window.outerWidth || w) - w) / 2);
|
|
23
|
+
const top = window.screenY + Math.max(0, ((window.outerHeight || h) - h) / 2);
|
|
24
|
+
const features = `popup=yes,width=${w},height=${h},left=${Math.round(left)},top=${Math.round(top)}`;
|
|
25
|
+
popup = window.open(url, "thirdfactor-verify", features);
|
|
26
|
+
if (!popup)
|
|
27
|
+
throw new PopupBlockedError();
|
|
28
|
+
// Detect a user closing the tab/window before the flow reports a result.
|
|
29
|
+
poll = window.setInterval(() => {
|
|
30
|
+
if (popup && popup.closed) {
|
|
31
|
+
stopPoll();
|
|
32
|
+
cancelHandler?.();
|
|
33
|
+
}
|
|
34
|
+
}, 500);
|
|
35
|
+
},
|
|
36
|
+
unmount() {
|
|
37
|
+
stopPoll();
|
|
38
|
+
try {
|
|
39
|
+
if (popup && !popup.closed)
|
|
40
|
+
popup.close();
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
/* cross-origin close guard */
|
|
44
|
+
}
|
|
45
|
+
popup = null;
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=popup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"popup.js","sourceRoot":"","sources":["../src/popup.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,gFAAgF;AAChF,2EAA2E;AAE3E,OAAO,EAAE,iBAAiB,EAAgB,MAAM,aAAa,CAAC;AAE9D,MAAM,UAAU,kBAAkB;IAChC,IAAI,KAAK,GAAkB,IAAI,CAAC;IAChC,IAAI,IAAI,GAAkB,IAAI,CAAC;IAC/B,IAAI,aAAa,GAAwB,IAAI,CAAC;IAE9C,MAAM,QAAQ,GAAG,GAAG,EAAE;QACpB,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;YAClB,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;YAC3B,IAAI,GAAG,IAAI,CAAC;QACd,CAAC;IACH,CAAC,CAAC;IAEF,OAAO;QACL,QAAQ,CAAC,OAAO;YACd,aAAa,GAAG,OAAO,CAAC;QAC1B,CAAC;QAED,KAAK,CAAC,GAAG;YACP,MAAM,CAAC,GAAG,GAAG,CAAC;YACd,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,CAAC,WAAW,IAAI,GAAG,CAAC,CAAC;YAC1D,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,UAAU,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YAC9E,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,WAAW,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YAC9E,MAAM,QAAQ,GAAG,mBAAmB,CAAC,WAAW,CAAC,SAAS,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;YACpG,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,oBAAoB,EAAE,QAAQ,CAAC,CAAC;YACzD,IAAI,CAAC,KAAK;gBAAE,MAAM,IAAI,iBAAiB,EAAE,CAAC;YAE1C,yEAAyE;YACzE,IAAI,GAAG,MAAM,CAAC,WAAW,CAAC,GAAG,EAAE;gBAC7B,IAAI,KAAK,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;oBAC1B,QAAQ,EAAE,CAAC;oBACX,aAAa,EAAE,EAAE,CAAC;gBACpB,CAAC;YACH,CAAC,EAAE,GAAG,CAAC,CAAC;QACV,CAAC;QAED,OAAO;YACL,QAAQ,EAAE,CAAC;YACX,IAAI,CAAC;gBACH,IAAI,KAAK,IAAI,CAAC,KAAK,CAAC,MAAM;oBAAE,KAAK,CAAC,KAAK,EAAE,CAAC;YAC5C,CAAC;YAAC,MAAM,CAAC;gBACP,8BAA8B;YAChC,CAAC;YACD,KAAK,GAAG,IAAI,CAAC;QACf,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { VerificationEvent, VerificationStatus } from "./types.js";
|
|
2
|
+
/** Must match TF_BRIDGE_SOURCE in the flow bridge. */
|
|
3
|
+
export declare const BRIDGE_SOURCE = "thirdfactor";
|
|
4
|
+
/**
|
|
5
|
+
* Validate and narrow an untrusted postMessage payload into a typed event.
|
|
6
|
+
* Returns null for anything that isn't one of our bridge events, so callers can
|
|
7
|
+
* safely ignore unrelated messages (analytics beacons, other embeds, etc.).
|
|
8
|
+
*
|
|
9
|
+
* Origin checking is the CALLER's job and must happen before this — see
|
|
10
|
+
* `messageMatches`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function parseBridgeMessage(data: unknown): VerificationEvent | null;
|
|
13
|
+
/**
|
|
14
|
+
* Map a raw server session status onto the normalized SDK status.
|
|
15
|
+
* Unknown terminal statuses fall back to `in_review` — the conservative choice,
|
|
16
|
+
* since an unrecognized outcome should get a human look rather than silently
|
|
17
|
+
* pass or fail.
|
|
18
|
+
*/
|
|
19
|
+
export declare function normalizeStatus(raw: string): VerificationStatus;
|
|
20
|
+
/** Origin of a URL string, or "" if it can't be parsed. */
|
|
21
|
+
export declare function originOf(url: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* Append the embed flag + platform tag the flow's bridge looks for, preserving
|
|
24
|
+
* any existing query/hash on the URL.
|
|
25
|
+
*/
|
|
26
|
+
export declare function withEmbedParams(url: string, platform?: string): string;
|
|
27
|
+
//# sourceMappingURL=protocol.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAExE,sDAAsD;AACtD,eAAO,MAAM,aAAa,gBAAgB,CAAC;AAE3C;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,OAAO,GAAG,iBAAiB,GAAG,IAAI,CAwB1E;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,kBAAkB,CAkB/D;AAED,2DAA2D;AAC3D,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAM5C;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,SAAQ,GAAG,MAAM,CAWrE"}
|
package/dist/protocol.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Wire protocol shared with the hosted flow's bridge
|
|
2
|
+
// (frontend/components/verify/sdk-bridge.ts). Keep the source tag and event
|
|
3
|
+
// shapes in lock-step with that file.
|
|
4
|
+
/** Must match TF_BRIDGE_SOURCE in the flow bridge. */
|
|
5
|
+
export const BRIDGE_SOURCE = "thirdfactor";
|
|
6
|
+
/**
|
|
7
|
+
* Validate and narrow an untrusted postMessage payload into a typed event.
|
|
8
|
+
* Returns null for anything that isn't one of our bridge events, so callers can
|
|
9
|
+
* safely ignore unrelated messages (analytics beacons, other embeds, etc.).
|
|
10
|
+
*
|
|
11
|
+
* Origin checking is the CALLER's job and must happen before this — see
|
|
12
|
+
* `messageMatches`.
|
|
13
|
+
*/
|
|
14
|
+
export function parseBridgeMessage(data) {
|
|
15
|
+
if (!data || typeof data !== "object")
|
|
16
|
+
return null;
|
|
17
|
+
const d = data;
|
|
18
|
+
if (d.source !== BRIDGE_SOURCE)
|
|
19
|
+
return null;
|
|
20
|
+
switch (d.type) {
|
|
21
|
+
case "ready":
|
|
22
|
+
return { type: "ready" };
|
|
23
|
+
case "completed":
|
|
24
|
+
return {
|
|
25
|
+
type: "completed",
|
|
26
|
+
status: String(d.status ?? ""),
|
|
27
|
+
sessionId: d.sessionId != null ? String(d.sessionId) : undefined,
|
|
28
|
+
};
|
|
29
|
+
case "error":
|
|
30
|
+
return {
|
|
31
|
+
type: "error",
|
|
32
|
+
code: String(d.code ?? "unknown"),
|
|
33
|
+
message: d.message != null ? String(d.message) : undefined,
|
|
34
|
+
};
|
|
35
|
+
case "close":
|
|
36
|
+
return { type: "close" };
|
|
37
|
+
default:
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Map a raw server session status onto the normalized SDK status.
|
|
43
|
+
* Unknown terminal statuses fall back to `in_review` — the conservative choice,
|
|
44
|
+
* since an unrecognized outcome should get a human look rather than silently
|
|
45
|
+
* pass or fail.
|
|
46
|
+
*/
|
|
47
|
+
export function normalizeStatus(raw) {
|
|
48
|
+
switch (raw) {
|
|
49
|
+
case "approved":
|
|
50
|
+
return "approved";
|
|
51
|
+
case "declined":
|
|
52
|
+
return "declined";
|
|
53
|
+
case "in_review":
|
|
54
|
+
case "review":
|
|
55
|
+
case "manual_review":
|
|
56
|
+
case "pending":
|
|
57
|
+
return "in_review";
|
|
58
|
+
case "abandoned":
|
|
59
|
+
case "cancelled":
|
|
60
|
+
case "canceled":
|
|
61
|
+
return "cancelled";
|
|
62
|
+
default:
|
|
63
|
+
return "in_review";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Origin of a URL string, or "" if it can't be parsed. */
|
|
67
|
+
export function originOf(url) {
|
|
68
|
+
try {
|
|
69
|
+
return new URL(url).origin;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return "";
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Append the embed flag + platform tag the flow's bridge looks for, preserving
|
|
77
|
+
* any existing query/hash on the URL.
|
|
78
|
+
*/
|
|
79
|
+
export function withEmbedParams(url, platform = "web") {
|
|
80
|
+
try {
|
|
81
|
+
const u = new URL(url);
|
|
82
|
+
u.searchParams.set("embed", "1");
|
|
83
|
+
u.searchParams.set("sdk", platform);
|
|
84
|
+
return u.toString();
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Fall back to a naive append if URL() is unavailable/relative.
|
|
88
|
+
const sep = url.includes("?") ? "&" : "?";
|
|
89
|
+
return `${url}${sep}embed=1&sdk=${platform}`;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=protocol.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAAA,qDAAqD;AACrD,4EAA4E;AAC5E,sCAAsC;AAItC,sDAAsD;AACtD,MAAM,CAAC,MAAM,aAAa,GAAG,aAAa,CAAC;AAE3C;;;;;;;GAOG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAa;IAC9C,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACnD,MAAM,CAAC,GAAG,IAA+B,CAAC;IAC1C,IAAI,CAAC,CAAC,MAAM,KAAK,aAAa;QAAE,OAAO,IAAI,CAAC;IAC5C,QAAQ,CAAC,CAAC,IAAI,EAAE,CAAC;QACf,KAAK,OAAO;YACV,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;QAC3B,KAAK,WAAW;YACd,OAAO;gBACL,IAAI,EAAE,WAAW;gBACjB,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,IAAI,EAAE,CAAC;gBAC9B,SAAS,EAAE,CAAC,CAAC,SAAS,IAAI,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS;aACjE,CAAC;QACJ,KAAK,OAAO;YACV,OAAO;gBACL,IAAI,EAAE,OAAO;gBACb,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,IAAI,SAAS,CAAC;gBACjC,OAAO,EAAE,CAAC,CAAC,OAAO,IAAI,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS;aAC3D,CAAC;QACJ,KAAK,OAAO;YACV,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;QAC3B;YACE,OAAO,IAAI,CAAC;IAChB,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,GAAW;IACzC,QAAQ,GAAG,EAAE,CAAC;QACZ,KAAK,UAAU;YACb,OAAO,UAAU,CAAC;QACpB,KAAK,UAAU;YACb,OAAO,UAAU,CAAC;QACpB,KAAK,WAAW,CAAC;QACjB,KAAK,QAAQ,CAAC;QACd,KAAK,eAAe,CAAC;QACrB,KAAK,SAAS;YACZ,OAAO,WAAW,CAAC;QACrB,KAAK,WAAW,CAAC;QACjB,KAAK,WAAW,CAAC;QACjB,KAAK,UAAU;YACb,OAAO,WAAW,CAAC;QACrB;YACE,OAAO,WAAW,CAAC;IACvB,CAAC;AACH,CAAC;AAED,2DAA2D;AAC3D,MAAM,UAAU,QAAQ,CAAC,GAAW;IAClC,IAAI,CAAC;QACH,OAAO,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,GAAW,EAAE,QAAQ,GAAG,KAAK;IAC3D,IAAI,CAAC;QACH,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;QACvB,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;QACjC,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;QACpC,OAAO,CAAC,CAAC,QAAQ,EAAE,CAAC;IACtB,CAAC;IAAC,MAAM,CAAC;QACP,gEAAgE;QAChE,MAAM,GAAG,GAAG,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;QAC1C,OAAO,GAAG,GAAG,GAAG,GAAG,eAAe,QAAQ,EAAE,CAAC;IAC/C,CAAC;AACH,CAAC"}
|
package/dist/runner.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { VerificationResult, VerifyOptions } from "./types.js";
|
|
2
|
+
/** A presentation surface (modal iframe or popup window). */
|
|
3
|
+
export interface Surface {
|
|
4
|
+
/** Show the surface pointed at `url`. Throws on hard failure (e.g. popup
|
|
5
|
+
* blocked) so the runner can resolve an error result. */
|
|
6
|
+
mount(url: string): void;
|
|
7
|
+
/** Tear the surface down. Idempotent. */
|
|
8
|
+
unmount(): void;
|
|
9
|
+
/** Register the user-initiated dismiss handler (× button, Esc, closed tab). */
|
|
10
|
+
onCancel(handler: () => void): void;
|
|
11
|
+
}
|
|
12
|
+
export declare function runSession(embeddedUrl: string, expectedOrigin: string, opts: VerifyOptions, surface: Surface): Promise<VerificationResult>;
|
|
13
|
+
export declare class PopupBlockedError extends Error {
|
|
14
|
+
constructor();
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=runner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEpE,6DAA6D;AAC7D,MAAM,WAAW,OAAO;IACtB;8DAC0D;IAC1D,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,yCAAyC;IACzC,OAAO,IAAI,IAAI,CAAC;IAChB,+EAA+E;IAC/E,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;CACrC;AAED,wBAAgB,UAAU,CACxB,WAAW,EAAE,MAAM,EACnB,cAAc,EAAE,MAAM,EACtB,IAAI,EAAE,aAAa,EACnB,OAAO,EAAE,OAAO,GACf,OAAO,CAAC,kBAAkB,CAAC,CAsE7B;AAED,qBAAa,iBAAkB,SAAQ,KAAK;;CAK3C"}
|
package/dist/runner.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Shared session runner used by both the modal and popup surfaces.
|
|
2
|
+
//
|
|
3
|
+
// It owns the one piece of logic that must be identical across surfaces: the
|
|
4
|
+
// origin-validated message loop and how bridge events resolve into a single
|
|
5
|
+
// VerificationResult. Surfaces only handle presentation + user-cancel.
|
|
6
|
+
import { parseBridgeMessage, normalizeStatus } from "./protocol.js";
|
|
7
|
+
export function runSession(embeddedUrl, expectedOrigin, opts, surface) {
|
|
8
|
+
return new Promise((resolve) => {
|
|
9
|
+
let settled = false;
|
|
10
|
+
// The authoritative result from a `completed`/`error` bridge event, if one
|
|
11
|
+
// arrived. A subsequent `close` (user tapped Done) then dismisses with THIS
|
|
12
|
+
// result rather than treating the dismiss as a cancel.
|
|
13
|
+
let pending = null;
|
|
14
|
+
const finish = (result) => {
|
|
15
|
+
if (settled)
|
|
16
|
+
return;
|
|
17
|
+
settled = true;
|
|
18
|
+
window.removeEventListener("message", onMessage);
|
|
19
|
+
surface.unmount();
|
|
20
|
+
resolve(result);
|
|
21
|
+
};
|
|
22
|
+
const onMessage = (ev) => {
|
|
23
|
+
if (expectedOrigin && ev.origin !== expectedOrigin)
|
|
24
|
+
return;
|
|
25
|
+
const event = parseBridgeMessage(ev.data);
|
|
26
|
+
if (!event)
|
|
27
|
+
return;
|
|
28
|
+
opts.onEvent?.(event);
|
|
29
|
+
switch (event.type) {
|
|
30
|
+
case "ready":
|
|
31
|
+
return;
|
|
32
|
+
case "completed": {
|
|
33
|
+
const result = {
|
|
34
|
+
status: normalizeStatus(event.status),
|
|
35
|
+
sessionId: event.sessionId,
|
|
36
|
+
rawStatus: event.status,
|
|
37
|
+
};
|
|
38
|
+
if (opts.autoClose === false) {
|
|
39
|
+
// Keep the flow's result screen up; dismiss on the later `close`.
|
|
40
|
+
pending = result;
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
finish(result);
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
case "error": {
|
|
48
|
+
finish({
|
|
49
|
+
status: "error",
|
|
50
|
+
error: { code: event.code, message: event.message ?? event.code },
|
|
51
|
+
});
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
case "close": {
|
|
55
|
+
// User acknowledged. If a terminal result is pending, deliver it;
|
|
56
|
+
// otherwise the user dismissed mid-flow → cancelled.
|
|
57
|
+
finish(pending ?? { status: "cancelled" });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
window.addEventListener("message", onMessage);
|
|
63
|
+
surface.onCancel(() => finish(pending ?? { status: "cancelled" }));
|
|
64
|
+
try {
|
|
65
|
+
surface.mount(embeddedUrl);
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
finish({
|
|
69
|
+
status: "error",
|
|
70
|
+
error: {
|
|
71
|
+
code: err instanceof PopupBlockedError ? "popup_blocked" : "load_error",
|
|
72
|
+
message: err instanceof Error ? err.message : String(err),
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
export class PopupBlockedError extends Error {
|
|
79
|
+
constructor() {
|
|
80
|
+
super("The verification popup was blocked. Call verify() from a click handler.");
|
|
81
|
+
this.name = "PopupBlockedError";
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=runner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runner.js","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"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"}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export interface CreateSessionInput {
|
|
2
|
+
/** KYC workflow UUID. Omit to use the tenant's default workflow. */
|
|
3
|
+
workflowId?: string;
|
|
4
|
+
/** Your external user reference; becomes the linked identity's external id. */
|
|
5
|
+
vendorData?: string;
|
|
6
|
+
/** Where the applicant is redirected after finishing (redirect mode). */
|
|
7
|
+
callbackUrl?: string;
|
|
8
|
+
contactEmail?: string;
|
|
9
|
+
contactPhone?: string;
|
|
10
|
+
/** Hosted URL lifetime in hours, 1..720 (default 48). */
|
|
11
|
+
expiresInHours?: number;
|
|
12
|
+
/** UI language, e.g. "en" | "ne". */
|
|
13
|
+
locale?: string;
|
|
14
|
+
/** Applicant fields to pre-fill (only the documented keys are honored). */
|
|
15
|
+
prefill?: Record<string, string>;
|
|
16
|
+
/** Idempotency key — repeat creates return the original session, uncharged. */
|
|
17
|
+
idempotencyKey?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface Session {
|
|
20
|
+
id: string;
|
|
21
|
+
status: string;
|
|
22
|
+
/** Hand this to `ThirdFactor.verify({ url })` (or redirect) in the browser. */
|
|
23
|
+
url: string;
|
|
24
|
+
decision: string;
|
|
25
|
+
vendorData?: string | null;
|
|
26
|
+
expiresAt?: string | null;
|
|
27
|
+
createdAt?: string | null;
|
|
28
|
+
/** The raw response body, in case you need a field not surfaced above. */
|
|
29
|
+
raw: Record<string, unknown>;
|
|
30
|
+
}
|
|
31
|
+
export interface CreateSessionConfig {
|
|
32
|
+
/** Your Obsidian base URL, e.g. "https://kyc.acme.com". No trailing slash. */
|
|
33
|
+
baseUrl: string;
|
|
34
|
+
/** Tenant API key (Console → Settings → API keys). Server-side only. */
|
|
35
|
+
apiKey: string;
|
|
36
|
+
/** Override fetch (tests, custom agents). Defaults to global fetch. */
|
|
37
|
+
fetchImpl?: typeof fetch;
|
|
38
|
+
}
|
|
39
|
+
export declare class ThirdFactorApiError extends Error {
|
|
40
|
+
status: number;
|
|
41
|
+
code: string;
|
|
42
|
+
constructor(status: number, code: string, message: string);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Create a verification session via `POST /v3/session/`.
|
|
46
|
+
*
|
|
47
|
+
* ```ts
|
|
48
|
+
* import { createSession } from "@thirdfactor/web/server";
|
|
49
|
+
* const session = await createSession(
|
|
50
|
+
* { baseUrl: process.env.TF_BASE_URL!, apiKey: process.env.TF_API_KEY! },
|
|
51
|
+
* { vendorData: user.id, callbackUrl: "https://app.acme.com/kyc/done" },
|
|
52
|
+
* );
|
|
53
|
+
* // return { url: session.url, id: session.id } to the browser.
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export declare function createSession(config: CreateSessionConfig, input?: CreateSessionInput): Promise<Session>;
|
|
57
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,kBAAkB;IACjC,oEAAoE;IACpE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,yEAAyE;IACzE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,yDAAyD;IACzD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,qCAAqC;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2EAA2E;IAC3E,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,+EAA+E;IAC/E,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,+EAA+E;IAC/E,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,0EAA0E;IAC1E,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC9B;AAED,MAAM,WAAW,mBAAmB;IAClC,8EAA8E;IAC9E,OAAO,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,MAAM,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAED,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;gBACD,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAM1D;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,mBAAmB,EAC3B,KAAK,GAAE,kBAAuB,GAC7B,OAAO,CAAC,OAAO,CAAC,CAmDlB"}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Optional server helper (`@thirdfactor/web/server`) for creating a KYC session
|
|
2
|
+
// from your Node backend. Runs ONLY on the server — it uses your secret API
|
|
3
|
+
// key, which must never reach the browser. Node 18+ (global fetch) or any
|
|
4
|
+
// runtime with a WHATWG `fetch`.
|
|
5
|
+
export class ThirdFactorApiError extends Error {
|
|
6
|
+
constructor(status, code, message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "ThirdFactorApiError";
|
|
9
|
+
this.status = status;
|
|
10
|
+
this.code = code;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Create a verification session via `POST /v3/session/`.
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* import { createSession } from "@thirdfactor/web/server";
|
|
18
|
+
* const session = await createSession(
|
|
19
|
+
* { baseUrl: process.env.TF_BASE_URL!, apiKey: process.env.TF_API_KEY! },
|
|
20
|
+
* { vendorData: user.id, callbackUrl: "https://app.acme.com/kyc/done" },
|
|
21
|
+
* );
|
|
22
|
+
* // return { url: session.url, id: session.id } to the browser.
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export async function createSession(config, input = {}) {
|
|
26
|
+
const doFetch = config.fetchImpl ?? globalThis.fetch;
|
|
27
|
+
if (typeof doFetch !== "function") {
|
|
28
|
+
throw new ThirdFactorApiError(0, "no_fetch", "No fetch implementation available (Node 18+ or pass fetchImpl).");
|
|
29
|
+
}
|
|
30
|
+
const base = config.baseUrl.replace(/\/+$/, "");
|
|
31
|
+
const body = {};
|
|
32
|
+
if (input.workflowId)
|
|
33
|
+
body.workflow_id = input.workflowId;
|
|
34
|
+
if (input.vendorData)
|
|
35
|
+
body.vendor_data = input.vendorData;
|
|
36
|
+
if (input.callbackUrl)
|
|
37
|
+
body.callback_url = input.callbackUrl;
|
|
38
|
+
if (input.contactEmail)
|
|
39
|
+
body.contact_email = input.contactEmail;
|
|
40
|
+
if (input.contactPhone)
|
|
41
|
+
body.contact_phone = input.contactPhone;
|
|
42
|
+
if (input.expiresInHours != null)
|
|
43
|
+
body.expires_in_hours = input.expiresInHours;
|
|
44
|
+
if (input.locale)
|
|
45
|
+
body.locale = input.locale;
|
|
46
|
+
if (input.prefill)
|
|
47
|
+
body.prefill = input.prefill;
|
|
48
|
+
const headers = {
|
|
49
|
+
"content-type": "application/json",
|
|
50
|
+
"x-api-key": config.apiKey,
|
|
51
|
+
};
|
|
52
|
+
if (input.idempotencyKey)
|
|
53
|
+
headers["Idempotency-Key"] = input.idempotencyKey;
|
|
54
|
+
const res = await doFetch(`${base}/v3/session/`, {
|
|
55
|
+
method: "POST",
|
|
56
|
+
headers,
|
|
57
|
+
body: JSON.stringify(body),
|
|
58
|
+
});
|
|
59
|
+
const text = await res.text();
|
|
60
|
+
let json = {};
|
|
61
|
+
try {
|
|
62
|
+
json = text ? JSON.parse(text) : {};
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
/* non-JSON error body */
|
|
66
|
+
}
|
|
67
|
+
if (!res.ok) {
|
|
68
|
+
const code = typeof json.detail === "string" ? json.detail : `http_${res.status}`;
|
|
69
|
+
throw new ThirdFactorApiError(res.status, code, `Session create failed (${res.status}): ${code}`);
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
id: String(json.id ?? ""),
|
|
73
|
+
status: String(json.status ?? ""),
|
|
74
|
+
url: String(json.url ?? ""),
|
|
75
|
+
decision: String(json.decision ?? ""),
|
|
76
|
+
vendorData: json.vendor_data ?? null,
|
|
77
|
+
expiresAt: json.expires_at ?? null,
|
|
78
|
+
createdAt: json.created_at ?? null,
|
|
79
|
+
raw: json,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** Normalized terminal outcome the SDK resolves with. */
|
|
2
|
+
export type VerificationStatus = "approved" | "declined" | "in_review" | "cancelled" | "error";
|
|
3
|
+
export interface VerificationError {
|
|
4
|
+
/** Stable machine code: invalid_session | expired | popup_blocked |
|
|
5
|
+
* load_error | network | unknown. Branch on this, not `message`. */
|
|
6
|
+
code: string;
|
|
7
|
+
message: string;
|
|
8
|
+
}
|
|
9
|
+
export interface VerificationResult {
|
|
10
|
+
status: VerificationStatus;
|
|
11
|
+
/** The verification session id (echoed from the flow when available). */
|
|
12
|
+
sessionId?: string;
|
|
13
|
+
/** Populated only when `status === "error"`. */
|
|
14
|
+
error?: VerificationError;
|
|
15
|
+
/** The raw server status when it differs from the normalized one
|
|
16
|
+
* (e.g. "abandoned" normalizes to "cancelled"). */
|
|
17
|
+
rawStatus?: string;
|
|
18
|
+
}
|
|
19
|
+
/** Lifecycle events emitted while the flow is open. */
|
|
20
|
+
export type VerificationEvent = {
|
|
21
|
+
type: "ready";
|
|
22
|
+
} | {
|
|
23
|
+
type: "completed";
|
|
24
|
+
status: string;
|
|
25
|
+
sessionId?: string;
|
|
26
|
+
} | {
|
|
27
|
+
type: "error";
|
|
28
|
+
code: string;
|
|
29
|
+
message?: string;
|
|
30
|
+
} | {
|
|
31
|
+
type: "close";
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* How the hosted flow is presented.
|
|
35
|
+
* - `modal` — full-screen overlay iframe. Best desktop UX.
|
|
36
|
+
* - `popup` — a new tab/window. Most reliable camera access on mobile.
|
|
37
|
+
* - `auto` — modal on desktop, popup on mobile (default).
|
|
38
|
+
*
|
|
39
|
+
* Full-page `redirect` is a separate call, `ThirdFactor.redirect(url)`, because
|
|
40
|
+
* it navigates away and cannot return a Promise.
|
|
41
|
+
*/
|
|
42
|
+
export type VerifyMode = "modal" | "popup" | "auto";
|
|
43
|
+
export interface VerifyOptions {
|
|
44
|
+
/**
|
|
45
|
+
* The hosted session URL returned by `POST /v3/session/` (the `url` field),
|
|
46
|
+
* e.g. `https://kyc.acme.com/verify/<token>`. Create the session from your
|
|
47
|
+
* backend with your secret API key — never in the browser.
|
|
48
|
+
*/
|
|
49
|
+
url: string;
|
|
50
|
+
/** Presentation mode. Default `auto`. */
|
|
51
|
+
mode?: VerifyMode;
|
|
52
|
+
/** Called for every lifecycle event (ready/completed/error/close). */
|
|
53
|
+
onEvent?: (event: VerificationEvent) => void;
|
|
54
|
+
/** Accessible label for the modal dialog. Default "Identity verification". */
|
|
55
|
+
title?: string;
|
|
56
|
+
/**
|
|
57
|
+
* Origin the SDK will accept postMessage from. Defaults to the origin of
|
|
58
|
+
* `url`. Only override if the flow renders on a different origin than the URL
|
|
59
|
+
* you pass (rare — e.g. a proxy).
|
|
60
|
+
*/
|
|
61
|
+
expectedOrigin?: string;
|
|
62
|
+
/**
|
|
63
|
+
* Auto-dismiss the surface once a terminal result arrives. Default `true`.
|
|
64
|
+
* Set `false` to keep the flow's own result screen visible until the user
|
|
65
|
+
* taps "Done" (which then dismisses).
|
|
66
|
+
*/
|
|
67
|
+
autoClose?: boolean;
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAaA,yDAAyD;AACzD,MAAM,MAAM,kBAAkB,GAC1B,UAAU,GACV,UAAU,GACV,WAAW,GACX,WAAW,GACX,OAAO,CAAC;AAEZ,MAAM,WAAW,iBAAiB;IAChC;yEACqE;IACrE,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,kBAAkB,CAAC;IAC3B,yEAAyE;IACzE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gDAAgD;IAChD,KAAK,CAAC,EAAE,iBAAiB,CAAC;IAC1B;wDACoD;IACpD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,uDAAuD;AACvD,MAAM,MAAM,iBAAiB,GACzB;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GACjB;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GACzD;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GACjD;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC;AAEtB;;;;;;;;GAQG;AACH,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,CAAC;AAEpD,MAAM,WAAW,aAAa;IAC5B;;;;OAIG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ,yCAAyC;IACzC,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,sEAAsE;IACtE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAC7C,8EAA8E;IAC9E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Public type surface for @thirdfactor/web.
|
|
2
|
+
//
|
|
3
|
+
// The result vocabulary is deliberately small and identical across all four
|
|
4
|
+
// ThirdFactor SDKs (web / Flutter / iOS / Android):
|
|
5
|
+
//
|
|
6
|
+
// approved | declined | in_review | cancelled | error
|
|
7
|
+
//
|
|
8
|
+
// The client-reported result is a UX signal, NOT the source of truth. Always
|
|
9
|
+
// confirm the authoritative decision server-side — via the KYC webhook
|
|
10
|
+
// (identity.kyc.session.*) or GET /v3/session/<id>/decision/ — before granting
|
|
11
|
+
// access or moving money. A user can close the tab after approval, or tamper
|
|
12
|
+
// with the browser; the webhook cannot be spoofed (it is HMAC-signed).
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,4CAA4C;AAC5C,EAAE;AACF,4EAA4E;AAC5E,oDAAoD;AACpD,EAAE;AACF,wDAAwD;AACxD,EAAE;AACF,6EAA6E;AAC7E,uEAAuE;AACvE,+EAA+E;AAC/E,6EAA6E;AAC7E,uEAAuE"}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@thirdfactor/web",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "ThirdFactor / Obsidian KYC — web client SDK. Opens the hosted verification flow in a modal, popup, or full-page redirect and returns a typed result.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./server": {
|
|
16
|
+
"types": "./dist/server.d.ts",
|
|
17
|
+
"import": "./dist/server.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": ["dist", "README.md"],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc -p tsconfig.json",
|
|
23
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
24
|
+
"clean": "rm -rf dist"
|
|
25
|
+
},
|
|
26
|
+
"keywords": ["kyc", "identity", "verification", "thirdfactor", "obsidian", "aml"],
|
|
27
|
+
"license": "UNLICENSED",
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"typescript": "^5.4.0"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
}
|
|
34
|
+
}
|