@checktiv/sdk-web 1.0.0 → 1.2.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.
Files changed (50) hide show
  1. package/README.md +172 -96
  2. package/dist/agents/AGENTS.md +20 -25
  3. package/dist/agents/manifest.json +15 -4
  4. package/dist/{types-CnlkFS7V.d.ts → api-DyExBfAQ.d.ts} +161 -97
  5. package/dist/capture-ui.css +1 -1
  6. package/dist/capture-ui.d.ts +5 -4
  7. package/dist/capture-ui.js +4 -2
  8. package/dist/capture.d.ts +3 -3
  9. package/dist/capture.js +3 -1
  10. package/dist/challenge.d.ts +18 -0
  11. package/dist/challenge.js +9 -0
  12. package/dist/chunk-5AQCMTZV.js +6 -0
  13. package/dist/chunk-5MNNRYD5.js +7 -0
  14. package/dist/{chunk-TRTAZKYV.js → chunk-6U53T32A.js} +4 -3
  15. package/dist/chunk-BEZGAQ74.js +8 -0
  16. package/dist/chunk-GFHNOZVO.js +3 -0
  17. package/dist/chunk-IQKHRPJR.js +3 -0
  18. package/dist/chunk-ITLSDZHK.js +5 -0
  19. package/dist/chunk-LTMLIUPC.js +5 -0
  20. package/dist/chunk-QVDWM4KK.js +6 -0
  21. package/dist/chunk-UBQTPS5J.js +3 -0
  22. package/dist/{controller-C4vJ47JI.d.ts → controller-CFL3mA5e.d.ts} +2 -2
  23. package/dist/copy-DCeQ4JKx.d.ts +19 -0
  24. package/dist/{copy-DeUly5vr.d.ts → copy-Dfmk11S_.d.ts} +2 -2
  25. package/dist/cross-device.d.ts +19 -1
  26. package/dist/custom-form.css +1 -1
  27. package/dist/custom-form.d.ts +4 -3
  28. package/dist/custom-form.js +6 -3
  29. package/dist/fraud.d.ts +4 -3
  30. package/dist/fraud.js +6 -4
  31. package/dist/idv/cross-device.d.ts +41 -6
  32. package/dist/idv/cross-device.js +7 -3
  33. package/dist/idv.css +1 -1
  34. package/dist/idv.d.ts +4 -3
  35. package/dist/idv.js +10 -6
  36. package/dist/index.d.ts +19 -6
  37. package/dist/index.js +9 -7
  38. package/dist/{machine--zVZIM4F.d.ts → machine-CuZalPFX.d.ts} +15 -3
  39. package/dist/react.d.ts +6 -4
  40. package/dist/react.js +7 -5
  41. package/dist/workspace.d.ts +60 -0
  42. package/dist/workspace.js +7 -0
  43. package/package.json +15 -5
  44. package/dist/chunk-BYTKOKAD.js +0 -7
  45. package/dist/chunk-C7H2ZVQT.js +0 -7
  46. package/dist/chunk-CXNPD67Z.js +0 -5
  47. package/dist/chunk-FGWZZNAN.js +0 -5
  48. package/dist/chunk-WQCYRLWP.js +0 -3
  49. package/dist/fp.esm.min-QD5CJD52.js +0 -5
  50. package/dist/index-BtJbU9MC.d.ts +0 -43
package/README.md CHANGED
@@ -1,39 +1,41 @@
1
1
  # @checktiv/sdk-web
2
2
 
3
- Customer-embed Web SDK for the Checktiv identity verification platform. Published on
4
- npm as `@checktiv/sdk-web`; the internal workspace name is `@rf/sdk-web`.
3
+ The customer-embed Web SDK for the Checktiv identity verification platform. Embed
4
+ identity verification, fraud-signal collection, and custom intake forms directly in
5
+ your own web app, with full control over look, feel, and journey.
5
6
 
6
- Two surfaces ship from this package:
7
+ Two ways to use it ship from one package:
7
8
 
8
- - **ESM npm package** (`@checktiv/sdk-web` and subpaths) - tree-shakeable ESM
9
- modules for bundler-based apps. React peer dependency is optional; `react`/`react-dom`
10
- are not bundled.
11
- - **CDN IIFE** (`window.Checktiv`) - the self-contained bundle served by `sdk-cdn`
12
- at `https://sdk.<region>.<apex>/v1/sdk.js`. Includes all modules eagerly so a single
13
- `<script>` tag is sufficient. A legacy `window.AutohostKyc` alias is kept for
14
- existing customer snippets; auto-discover attributes `[data-checktiv]` (preferred)
15
- and `[data-autohost-kyc]` (back-compat) work on both the CDN and npm paths.
9
+ - **ESM npm package** (`@checktiv/sdk-web` and its subpaths) - tree-shakeable ESM
10
+ modules for bundler-based apps (Vite, webpack, Rollup, Next.js, and friends). The
11
+ React wrapper is an optional subpath; `react` / `react-dom` are never bundled.
12
+ - **CDN `<script>` tag** (`window.Checktiv`) - a self-contained bundle. A single
13
+ script tag is enough: every module is included, so no build step is required.
16
14
 
17
- ## Install (npm)
15
+ ## Install
18
16
 
19
17
  ```bash
20
18
  npm install @checktiv/sdk-web
21
19
  # or: pnpm add @checktiv/sdk-web
20
+ # or: yarn add @checktiv/sdk-web
22
21
  ```
23
22
 
24
- `react` and `react-dom` are **optional peer dependencies**. Install them only when
25
- using the `./react` subpath.
23
+ `react` and `react-dom` are **optional peer dependencies**. Install them only if you
24
+ use the `./react` subpath. Everything else works without React.
26
25
 
27
- ## CDN snippet
26
+ ## CDN usage
27
+
28
+ Load the moving pointer for the latest release:
28
29
 
29
30
  ```html
30
- <!-- Moving pointer - always the latest release. No SRI: content changes on every release. -->
31
+ <!-- Moving pointer - always the latest release. -->
31
32
  <script src="https://sdk.us.checktiv.com/v1/sdk.js" crossorigin="anonymous"></script>
32
33
  ```
33
34
 
34
- This loads the latest release. SRI is not usable on the moving `/v1/sdk.js` pointer
35
- because its content changes on every release. For production, pin to the immutable
36
- versioned URL and include a Subresource Integrity hash from the release notes:
35
+ The moving `/v1/sdk.js` pointer always serves the newest release, so its bytes change
36
+ over time. That means a Subresource Integrity (SRI) hash cannot be used on it. For
37
+ production, pin to the immutable versioned URL and add the SRI hash published in the
38
+ release notes, so the exact bytes your page loads are locked and verified:
37
39
 
38
40
  ```html
39
41
  <script
@@ -43,71 +45,145 @@ versioned URL and include a Subresource Integrity hash from the release notes:
43
45
  ></script>
44
46
  ```
45
47
 
46
- ## The two-line surface
48
+ Use the region host that matches your account (for example `sdk.us.checktiv.com` or
49
+ `sdk.eu.checktiv.com`). The CDN bundle exposes the same API as the npm package on the
50
+ `window.Checktiv` global.
51
+
52
+ ## Quickstart: the two-line surface
47
53
 
48
54
  ```js
49
55
  import { init } from '@checktiv/sdk-web';
50
- import '@checktiv/sdk-web/idv'; // self-registers the IDV module
56
+ import '@checktiv/sdk-web/idv'; // self-registers the managed IDV module
51
57
 
52
58
  const client = init({
53
59
  publishableKey: 'ah_pk_us_test_...',
54
60
  getSessionToken: async (ctx) => {
61
+ // Your server exchanges its secret key for a short-lived browser token.
55
62
  const res = await fetch('/api/checktiv/token', {
56
63
  method: 'POST',
57
- body: JSON.stringify({ reason: ctx.reason }),
58
64
  headers: { 'Content-Type': 'application/json' },
65
+ body: JSON.stringify({ reason: ctx.reason }),
59
66
  });
60
67
  return (await res.json()).token;
61
68
  },
62
69
  });
63
70
 
64
71
  // Server-driven: renders whatever modules the session declares.
65
- client.mountProvisioned({
72
+ const handle = client.mountProvisioned({
66
73
  target: document.getElementById('checktiv-container'),
67
74
  onEvent: (event) => console.log(event),
68
75
  });
76
+
77
+ // Later, when navigating away:
78
+ // handle.destroy();
69
79
  ```
70
80
 
71
- `init()` never makes a network request. `mountProvisioned()` reads the
72
- server-declared modules from the session (`GET /sdk/v1/sessions/me`) and renders
73
- each one. Your secret key never appears in client code.
81
+ Key properties of this design:
82
+
83
+ - `init()` makes **no network call**. It only sets up the client and the token
84
+ callback.
85
+ - Your **secret key never touches client code**. `getSessionToken` returns a
86
+ short-lived browser token that your server mints; the SDK only ever holds that
87
+ token.
88
+ - **The server drives composition.** `mountProvisioned()` reads the modules the
89
+ session declares and renders each one, so you can change the journey server-side
90
+ without shipping new client code.
74
91
 
75
92
  ## Published subpaths
76
93
 
77
- All subpaths below are part of the published API
78
- (`publishConfig.exports` in `package.json`):
79
-
80
- | Subpath | Purpose |
81
- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
82
- | `.` / `@checktiv/sdk-web` | `Checktiv.init(opts)` substrate. Returns a `ChecktivClient`. No Zod at runtime; safe for tree-shaking. |
83
- | `./idv` | Managed IDV module. Import for its self-registration side effect so `mountProvisioned` / `mount('idv')` can resolve it. |
84
- | `./fraud` | Consent-gated fraud signal collection. Import for its self-registration side effect. |
85
- | `./custom-form` | Custom form module (server-authored field definitions). Import for self-registration. |
86
- | `./custom-form/style.css` | Stylesheet for the custom-form module. `<link>` or `import` this in the host page. |
87
- | `./react` | React wrapper: `ChecktivProvider`, `useChecktiv()`, `ChecktivIdv`. Requires `react`/`react-dom` peer dependencies. |
88
- | `./capture` | Headless host-side IDV capture controller (`createCaptureController`). Tier-1 - no default UI. |
89
- | `./capture-ui` | Batteries-included capture renderer (`mount()`). Tier-2 built on `./capture`. |
90
- | `./cross-device` | Cross-device QR/copy-link handoff overlay. |
91
- | `./idv/cross-device` | IDV-specific cross-device wiring. |
92
- | `./agents` | Machine-readable agent steering manifest (`dist/agents/manifest.json`). |
93
-
94
- The `./agent` subpath is **workspace-only** (not published) - it is omitted from
95
- `publishConfig.exports`.
94
+ Every entry below is part of the published API. Import a module subpath (like
95
+ `./idv`) once anywhere in your app: importing it runs a one-time self-registration
96
+ side effect so `mountProvisioned()` and `mount('idv')` can resolve that module. If a
97
+ session needs a module you did not import, the SDK throws a clear, actionable error
98
+ telling you which subpath to add, instead of failing silently.
99
+
100
+ | Subpath | Purpose |
101
+ | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
102
+ | `.` / `@checktiv/sdk-web` | Core entry: `init(opts)` returns a `ChecktivClient`. Tree-shakeable and lightweight. |
103
+ | `./idv` | Managed identity-verification module. Import for its self-registration side effect so `mountProvisioned` / `mount('idv')` resolve it. |
104
+ | `./fraud` | Consent-gated fraud-signal collection. Import for its self-registration side effect. |
105
+ | `./custom-form` | Custom intake-form module (server-authored field definitions). Import for its self-registration side effect. |
106
+ | `./custom-form/style.css` | Stylesheet for the custom-form module. `<link>` or `import` this into the host page. |
107
+ | `./react` | React wrapper: `ChecktivProvider`, `useChecktiv()`, `ChecktivIdv`. Requires the `react` / `react-dom` peer dependencies. |
108
+ | `./capture` | Advanced: headless host-side capture controller (`createCaptureController`). No default UI. |
109
+ | `./capture-ui` | Advanced: batteries-included capture renderer (`mount()`), built on `./capture`. |
110
+ | `./cross-device` | Cross-device QR / copy-link handoff overlay (`mountCrossDevice`). |
111
+ | `./idv/cross-device` | Cross-device wiring for the managed IDV module. |
112
+ | `./agents` | Machine-readable steering manifest (JSON) for coding-agent tooling. |
113
+
114
+ ## Managed IDV module (`./idv`)
115
+
116
+ The managed IDV module renders the full identity-verification capture experience for
117
+ you. Import it for its registration side effect, then let the provisioned session
118
+ render it, or mount it explicitly:
96
119
 
97
- ## Back-compat surface
120
+ ```js
121
+ import { init } from '@checktiv/sdk-web';
122
+ import '@checktiv/sdk-web/idv';
123
+
124
+ const client = init({ publishableKey: 'ah_pk_us_live_...', getSessionToken });
125
+
126
+ const idv = client.mount('idv', {
127
+ target: '#checktiv-container',
128
+ onEvent: (event) => {
129
+ // Events are named `checktiv.idv.*`, for example `checktiv.idv.ready`.
130
+ console.log(event.type);
131
+ },
132
+ });
133
+
134
+ // idv.destroy() when you are done.
135
+ ```
98
136
 
99
- The following identifiers are kept for existing customer integrations and will not be
100
- removed without a major version bump:
137
+ Events are `checktiv.<module>.<name>`. The event stream is forward-open: new event
138
+ types can appear over time, so handle the ones you care about and ignore the rest -
139
+ do not write an exhaustive switch that assumes a fixed set.
101
140
 
102
- - `window.AutohostKyc` CDN global alias (alongside `window.Checktiv`)
103
- - `[data-autohost-kyc]` auto-discover attribute (alongside `[data-checktiv]`)
104
- - `autohost.kyc.complete` postMessage completion event name
141
+ ## Fraud module (`./fraud`)
105
142
 
106
- ## React usage
143
+ Fraud-signal collection is **consent-gated and default-deny**: it collects nothing
144
+ until your `onConsent` callback resolves truthy.
145
+
146
+ ```js
147
+ import { init } from '@checktiv/sdk-web';
148
+ import '@checktiv/sdk-web/fraud';
149
+
150
+ const client = init({ publishableKey: 'ah_pk_us_live_...', getSessionToken });
151
+
152
+ const fraud = client.mount('fraud', {
153
+ target: '#checktiv-container',
154
+ onConsent: () => userHasConsented(), // boolean or Promise<boolean>
155
+ onEvent: (event) => console.log(event), // `checktiv.fraud.*`
156
+ });
157
+
158
+ // fraud.destroy() when you are done.
159
+ ```
160
+
161
+ ## Custom-form module (`./custom-form`)
162
+
163
+ Renders a server-authored intake form and owns the whole submit. The host resolves
164
+ the form config and copy (from your own localization), then passes them in. Import
165
+ the stylesheet so the field controls are styled in your page:
166
+
167
+ ```js
168
+ import '@checktiv/sdk-web/custom-form';
169
+ import '@checktiv/sdk-web/custom-form/style.css';
170
+ ```
171
+
172
+ The module always renders a continue control (even for a content-only screen) so the
173
+ journey can advance, and emits `checktiv.custom_form.submitted` when the applicant
174
+ completes it. When you use `mountProvisioned`, pass `customFormConfig` and
175
+ `customFormCopy` alongside `target`.
176
+
177
+ ## React integration (`./react`)
178
+
179
+ The React wrapper provides `ChecktivProvider`, the `useChecktiv()` hook, and the
180
+ `ChecktivIdv` component. It is SSR-safe (it renders `null` on the server and only
181
+ initializes in the browser), and `react` / `react-dom` are supplied by your app as
182
+ optional peers, so there is a single React instance.
107
183
 
108
184
  ```tsx
109
185
  import { ChecktivProvider, ChecktivIdv } from '@checktiv/sdk-web/react';
110
- import '@checktiv/sdk-web/idv'; // register the IDV module
186
+ import '@checktiv/sdk-web/idv'; // register the managed IDV module
111
187
 
112
188
  export function App() {
113
189
  return (
@@ -121,61 +197,61 @@ export function App() {
121
197
  }
122
198
  ```
123
199
 
124
- See the React guide in the docs for the full SSR-safe / Next.js pattern.
200
+ `ChecktivIdv` mounts the managed IDV module and tears it down on unmount. Read the
201
+ hosted developer docs for the full SSR-safe integration pattern.
125
202
 
126
- ## Architecture notes
203
+ ## Advanced: host-side capture (`./capture`, `./capture-ui`)
127
204
 
128
- - **No PII, no private API key in the SDK.** The publishable key is operator config,
129
- not PII. Your server mints `bt_*` browser tokens; the SDK only ever holds those.
130
- - **Module-load seam.** The `.` entry does not statically import the IDV or fraud
131
- modules. Each module self-registers via a top-level side effect when its subpath is
132
- imported. Without the import, `mount('idv')` fails with a clear `sdk_load_failed`
133
- error message.
134
- - **Event surface.** Events are `checktiv.<module>.<name>`. The stream is
135
- forward-open: do not exhaustively switch on `event.type` - unknown types may be
136
- added without a major bump.
137
- - **Bundle budget.** The CDN `sdk.js` is gated at 100 KiB gzip by CI.
205
+ Most integrations should use the managed `./idv` module above, which renders the
206
+ capture experience for you. The `./capture` and `./capture-ui` subpaths are lower
207
+ level escape hatches for hosts that need to own the capture surface themselves:
138
208
 
139
- ## Build
209
+ - `./capture` exports `createCaptureController`, a framework-agnostic, headless
210
+ capture controller with no default UI. You inject token and submit functions and
211
+ drive the state machine yourself.
212
+ - `./capture-ui` exports `mount(element, options)`, a batteries-included default
213
+ renderer built on `./capture` that returns a `{ destroy }` handle.
140
214
 
141
- ```bash
142
- pnpm --filter @rf/sdk-web build
143
- # emits dist/ (ESM) and services/sdk-cdn/public/v1/sdk.js (CDN IIFE)
144
- ```
215
+ ## Cross-device handoff (`./cross-device`)
145
216
 
146
- No sourcemaps ship in the npm tarball or the CDN bundle. Maps are CI-only build
147
- artifacts (they contain internal module names).
217
+ `./cross-device` exports `mountCrossDevice`, an overlay that hands off a verification
218
+ to another device via a QR code or copy-link. `./idv/cross-device` wires the same
219
+ handoff into the managed IDV module.
148
220
 
149
- ## Bundle size checks
221
+ ## Back-compat surface
150
222
 
151
- ```bash
152
- pnpm --filter @rf/sdk-web size # CDN sdk.js (100 KiB gate)
153
- pnpm --filter @rf/sdk-web size:host-capture # capture.js
154
- pnpm --filter @rf/sdk-web size:host-capture-ui # capture-ui.js
155
- pnpm --filter @rf/sdk-web size:cross-device # cross-device chunk
156
- pnpm --filter @rf/sdk-web size:npm # per-entry npm tarball sizes
157
- ```
223
+ These identifiers are kept for existing integrations that were built against the
224
+ previous product name, and will not be removed without a major version bump. They are
225
+ literal API identifiers (like a stable route path), so use them verbatim if you are
226
+ migrating an older snippet:
158
227
 
159
- The canonical gate for CI is `pnpm audit:bundle-sizes` (runs all four budgets).
228
+ - `window.AutohostKyc` - CDN global alias (alongside `window.Checktiv`).
229
+ - `[data-autohost-kyc]` - auto-discover attribute (alongside `[data-checktiv]`).
230
+ - `autohost.kyc.complete` - completion event name.
160
231
 
161
- ## Publish validation
232
+ ## TypeScript
162
233
 
163
- ```bash
164
- pnpm --filter @rf/sdk-web validate:publint # publint --strict
165
- pnpm --filter @rf/sdk-web validate:attw # are the types wrong?
166
- pnpm --filter @rf/sdk-web validate:tarball # publish-boundary gate
234
+ The package ships self-contained type declarations. Import types from the root:
235
+
236
+ ```ts
237
+ import type { ChecktivClient, ChecktivEvent, ChecktivIdvEvent } from '@checktiv/sdk-web';
167
238
  ```
168
239
 
169
- The publish-boundary gate (`scripts/audit-sdk-publish-boundary.mjs`) checks for
170
- internal `@rf/` specifier leaks, vendor names, internal settings sentinels, and
171
- dependency hygiene in the packed tarball.
240
+ The `ChecktivEvent` union is forward-open, so a handler typed against it keeps
241
+ compiling as new modules and phases are added.
172
242
 
173
- ## Testing
243
+ ## Versioning and support
174
244
 
175
- ```bash
176
- pnpm --filter @rf/sdk-web test
177
- pnpm --filter @rf/sdk-web typecheck
178
- ```
245
+ The package follows semantic versioning. The back-compat identifiers listed above are
246
+ preserved across minor and patch releases. Pin the CDN URL to an immutable versioned
247
+ path (with its SRI hash) for production, as shown in the CDN section.
248
+
249
+ ## Documentation
250
+
251
+ Full guides, the API reference, and framework-specific integration walkthroughs live
252
+ in the Checktiv developer docs, Web SDK section, at
253
+ [https://docs.checktiv.com](https://docs.checktiv.com).
254
+
255
+ ## License
179
256
 
180
- Tests run under jsdom. Real camera capture and the CDN IIFE are verified against a
181
- deployed dev cell before each release.
257
+ Apache-2.0
@@ -7,49 +7,44 @@ runnable recipes. Read both.
7
7
 
8
8
  ## Rules
9
9
 
10
- 1. **The server mints the session, never the browser.** Your backend calls the public API `POST /v1/sessions` with your secret key (`ah_sk_*`) and receives the session. The secret key is server-only: NEVER put `ah_sk_*` in browser code. To hand control to the frontend, your backend mints a short-lived browser token (`bt_*`) by calling `POST /sdk/v1/sessions/:sessionId/browser-token` with the same `ah_sk_*`; that route returns `{ data: { browserToken, expiresAt, modules, scopes } }`. Only the `bt_*` string crosses to the browser.
10
+ 1. **Prefer the zero-lifecycle Checktiv.mount entry.** The simplest integration is two pieces of code. (1) On YOUR backend, call `sessions.create({ templateId, applicant }, { idempotencyKey })` - a thin wrapper over `POST /v1/sessions` with your `ah_sk_*` secret key - and return ONLY the resulting `clientToken` to the browser. (2) In the browser, call `Checktiv.mount(target, { fetchToken, onComplete })`, where `fetchToken` returns that `clientToken`. `fetchToken` runs ONCE for the cold / new-device start, NOT on every expiry: the SDK exchanges the durable `clientToken` for a short-lived working token and refreshes it internally, so you write NO token-refresh endpoint, NO expiration handler, and NO resume endpoint. Key `sessions.create` idempotently per user or reservation so retries and new-device re-entry never double-bill. The legacy `Checktiv.init({ ... }).mountProvisioned({ target })` with a `getSessionToken` browser-token callback stays fully supported for existing integrations.
11
11
 
12
- 2. **Use mountProvisioned as the default path.** Call `Checktiv.init({ ... }).mountProvisioned({ target })`. It reads the server-declared `session.modules` from `GET /sdk/v1/sessions/me` and renders exactly what the server composed, so the server drives composition. `mount('idv')` / `mount('fraud')` is the explicit override / escape hatch, not the first choice.
12
+ 2. **The server mints the session, never the browser.** Your backend calls the public API `POST /v1/sessions` with your secret key (`ah_sk_*`) and receives the session. The secret key is server-only: NEVER put `ah_sk_*` in browser code. To hand control to the frontend, your backend mints a short-lived browser token (`bt_*`) by calling `POST /v1/sessions/{id}/browser_token` with the same `ah_sk_*`; that route returns `{ data: { browser_token, expires_at } }`. Only the `bt_*` string crosses to the browser.
13
13
 
14
- 3. **Completion is not a verdict: trust the signed webhook.** The client event `checktiv.idv.submitted` is terminal-for-capture only; it means the applicant finished the capture step, NOT that they passed. The decision arrives on your server through the signed `kyc.session.*` webhook. Trust the signed webhook as the only outcome anchor; never infer a pass/fail from a client event.
14
+ 3. **Use mountProvisioned as the default path.** Call `Checktiv.init({ ... }).mountProvisioned({ target })`. It reads the server-declared `session.modules` from `GET /sdk/v1/sessions/me` and renders exactly what the server composed, so the server drives composition. `mount('idv')` / `mount('fraud')` is the explicit override / escape hatch, not the first choice.
15
15
 
16
- 4. **Satisfy each applicant-info requirement once, through any one path.** When a session declares applicant-info requirements (name, date of birth, address, and similar fields), each requirement is satisfied by ANY ONE of the supported collection paths, not by all of them. Supply a field through one path and the SDK treats that requirement as met; do not double-collect the same field. Let the server-declared session decide which fields are required rather than hard-coding a form.
16
+ 4. **Completion is not a verdict: trust the signed webhook.** The client event `checktiv.idv.submitted` is terminal-for-capture only; it means the applicant finished the capture step, NOT that they passed. The decision arrives on your server through the signed `kyc.session.*` webhook. Trust the signed webhook as the only outcome anchor; never infer a pass/fail from a client event.
17
17
 
18
- 5. **Develop local-first with the synthetic driver.** Use a **test-mode publishable key** (`ah_pk_<region>_test_*`) PLUS the synthetic opt-in. Test mode is selected by the key itself, not by a `mode` property on `init`. A test-mode key without the synthetic option renders a BLANK embed on a local origin because the capture license is domain-bound, whereas the synthetic path runs the full `checktiv.idv.*` flow locally with no camera, no license, and no deployed environment. Ensure your own session-mint backend and token endpoint are running before the SDK calls `getSessionToken`. Build and verify locally with the synthetic driver, then graduate to a deployed environment for real capture and ship through the standard npm / CDN path.
18
+ 5. **Satisfy each applicant-info requirement once, through any one path.** When a session declares applicant-info requirements (name, date of birth, address, and similar fields), each requirement is satisfied by ANY ONE of the supported collection paths, not by all of them. Supply a field through one path and the SDK treats that requirement as met; do not double-collect the same field. Let the server-declared session decide which fields are required rather than hard-coding a form.
19
19
 
20
- 6. **getSessionToken supplies a fresh bt_* on demand.** `getSessionToken` is `string | ((ctx: { sessionId; reason: 'initial' | 'expired' | '401' }) => Promise<string>)`. The SDK calls it on mount and again on expiry or a 401, then retries the failed request once. Always return a freshly minted `bt_*` from your backend; never hold a long-lived credential in the browser. If your callback rejects, the SDK emits `token_expired` with `recovery: 'refresh_session'` and halts cleanly.
20
+ 6. **Develop local-first with the synthetic driver.** Use a **test-mode publishable key** (`ah_pk_<region>_test_*`) PLUS the synthetic opt-in. Test mode is selected by the key itself, not by a `mode` property on `init`. A test-mode key without the synthetic option renders a BLANK embed on a local origin because the capture license is domain-bound, whereas the synthetic path runs the full `checktiv.idv.*` flow locally with no camera, no license, and no deployed environment. Ensure your own session-mint backend and token endpoint are running before the SDK calls `getSessionToken`. Build and verify locally with the synthetic driver, then graduate to a deployed environment for real capture and ship through the standard npm / CDN path.
21
21
 
22
- 7. **Add your frontend origin to the publishable key allowlist first.** Each publishable key carries an origin allowlist that you set in the console / dashboard. Your frontend's origin MUST be on that allowlist before the first mount, or EVERY mount fails with `origin_not_allowed` (the `bt_*` data-plane routes reject an off-allowlist `Origin`). Add your origin to the publishable key allowlist before you integrate.
22
+ 7. **getSessionToken supplies a fresh bt_* on demand.** `getSessionToken` is `string | ((ctx: { sessionId; reason: 'initial' | 'expired' | '401' }) => Promise<string>)`. The SDK calls it on mount and again on expiry or a 401, then retries the failed request once. Always return a freshly minted `bt_*` from your backend; never hold a long-lived credential in the browser. If your callback rejects, the SDK emits `token_expired` with `recovery: 'refresh_session'` and halts cleanly.
23
23
 
24
- 8. **The SDK adds X-Publishable-Key itself on the customer path.** Pass `publishableKey` to `Checktiv.init(...)` and stop there. On the customer (pk-present) path the managed transport attaches the `X-Publishable-Key` header to its own `bt_*` data-plane calls internally. Do NOT also hand-add an `X-Publishable-Key` header yourself; double-adding it is wrong. (The first-party hosted path used internally by the verify app carries no publishable key and sends none.)
24
+ 8. **Add your frontend origin to the publishable key allowlist first.** Each publishable key carries an origin allowlist that you set in the console / dashboard. Your frontend's origin MUST be on that allowlist before the first mount, or EVERY mount fails with `origin_not_allowed` (the `bt_*` data-plane routes reject an off-allowlist `Origin`). Add your origin to the publishable key allowlist before you integrate.
25
25
 
26
- 9. **Wire cross-device QR handoff via openCrossDeviceOverlay.** Cross-device handoff is wired via STANDALONE exports from the `@checktiv/sdk-web/idv/cross-device` subpath (NOT via an option on `IdvMountOptions` / `mountProvisioned`). Import: `import { openCrossDeviceOverlay, preloadCrossDeviceChunk } from '@checktiv/sdk-web/idv/cross-device';`
26
+ 9. **The SDK adds X-Publishable-Key itself on the customer path.** Pass `publishableKey` to `Checktiv.init(...)` and stop there. On the customer (pk-present) path the managed transport attaches the `X-Publishable-Key` header to its own `bt_*` data-plane calls internally. Do NOT also hand-add an `X-Publishable-Key` header yourself; double-adding it is wrong. (The first-party hosted path used internally by the verify app carries no publishable key and sends none.)
27
27
 
28
- `openCrossDeviceOverlay(opts: CrossDeviceOverlayOptions)` opens the overlay. Key fields on `CrossDeviceOverlayOptions`:
29
- • `target: HTMLElement`: the module's root container element.
30
- • `onOpenCrossDevice: CrossDeviceHandoffHook`: HOST-supplied mint hook. Type: `() => Promise<{ kind: 'ok'; url: string } | { kind: 'unavailable' }>`. Your host calls its own OTL-mint BFF and returns `{ kind: 'ok', url }` with the journey URL, or `{ kind: 'unavailable' }` if the mint fails. The URL MUST be `https:` (the SDK validates this and collapses `javascript:`, `http:`, and `data:` schemes to `'unavailable'`). The hook is re-callable (the overlay has a "refresh link" affordance); cap re-mints with a per-overlay counter on your host.
31
- • `copy: CrossDeviceCopy`: host-injected copy map; no hardcoded strings.
32
- • `isMobile: boolean`: `true` suppresses the QR (same-device scan is circular).
33
- • `emit: (e: ChecktivIdvEvent) => void`: forward to the host's `onEvent` handler.
34
- • `onClose?: () => void`: called when the applicant closes the overlay or `destroy()` is called.
28
+ 10. **Cross-device QR handoff is built into the managed IDV module.** Cross-device handoff (a desktop applicant finishes on their phone via a QR + copy-link overlay) is OWNED BY THE MANAGED IDV MODULE. Render it with `client.mount('idv', { crossDeviceCopy, onEvent })` and the returned handle exposes `openCrossDevice()`: calling it opens the overlay OVER the still-live capture frame, runs the completion poll, and emits `checktiv.idv.submitted` when the phone finishes so your host reloads. You MUST `import '@checktiv/sdk-web/idv/cross-device';` to enable the overlay (the CDN script-tag bundle already includes it); without that import `openCrossDevice()` is a graceful, actionable no-op, as it also is before the applicant verification resolves or when no `crossDeviceCopy` was supplied. `IdvMountOptions` carries `crossDeviceCopy` (host-injected strings; the `unavailableMessage` key is required so no arm renders blank) and the OPTIONAL `onOpenCrossDevice` mint override; `IdvHandle` exposes `openCrossDevice?()`, so call it as `handle.openCrossDevice?.()`. NOTE: `mountProvisioned` and the zero-lifecycle `Checktiv.mount` entry do NOT expose the overlay: use the explicit `mount('idv', ...)` call when you need `openCrossDevice()`.
35
29
 
36
- Returns `CrossDeviceOverlayHandle` with:
37
- • `setCompleting()`: marks the session completing (phone finished; overlay stays mounted with a waiting label instead of disappearing).
38
- • `destroy()`: tears down the overlay. Idempotent.
30
+ Who mints the one-time handoff link depends on the token plane:
31
+ • Working-token plane: the module mints the link ITSELF (it calls its own handoff endpoint `POST /sdk/v1/sessions/me/handoff`, authenticated by the applicant's short-lived working token, and caps the per-overlay "refresh link" re-mints). Supply `onOpenCrossDevice` ONLY to point the mint at your own backend instead.
32
+ • Browser-token (`bt_*`) plane: the SDK CANNOT self-mint (the handoff route rejects a `bt_*` bearer), so you MUST supply `onOpenCrossDevice` backed by a server-held credential.
39
33
 
40
- `preloadCrossDeviceChunk()`: fire-and-forget preload of the lazy CDN chunk. Call it when the IDV module becomes active to reduce perceived latency on open.
34
+ `onOpenCrossDevice: CrossDeviceHandoffHook` is `() => Promise<{ kind: 'ok'; url: string } | { kind: 'unavailable' }>`. Return `{ kind: 'ok', url }` with the journey URL (which MUST be `https:`: the SDK collapses `javascript:`, `http:`, and `data:` schemes to `'unavailable'`) or `{ kind: 'unavailable' }` when the mint fails. The hook is re-callable for the overlay's "refresh link" affordance; the SDK caps re-mints per overlay.
41
35
 
42
- Events (both are token-free, NEVER contain the url/OTL/short-code):
43
- • `checktiv.idv.cross_device_opened`: hook returned `ok`, overlay shown.
44
- • `checktiv.idv.cross_device_unavailable`: hook returned `unavailable` or URL validation failed.
45
- There is NO `cross_device_completed` event. Completion is the HOST's responsibility: poll your own `/api/idv/cross-device-status` BFF (returns `{ advanced: boolean }`) and call `window.location.reload()` exactly once when `advanced: true`. The SDK module NEVER writes `window.location`. Use `setCompleting()` on the returned handle before reloading so the overlay shows a completing state rather than disappearing abruptly.
36
+ For a FULLY CUSTOM IDV renderer built on `@checktiv/sdk-web/capture` (NOT the managed module), the STANDALONE exports drive the overlay yourself and you always supply the mint hook: `@checktiv/sdk-web/idv/cross-device` (`openCrossDeviceOverlay(opts)` opens the overlay, `preloadCrossDeviceChunk()` warms the lazy chunk) and `@checktiv/sdk-web/cross-device` (`mountCrossDevice(target, props)` renders the bare panel on a screen you fully control). `openCrossDeviceOverlay` takes `{ target, onOpenCrossDevice, copy, isMobile, emit, onClose }` plus an optional `poll` config, and returns a handle with `setCompleting()` (keep the QR mounted with a waiting label while your page reloads) and `destroy()` (idempotent). Set `isMobile: true` to suppress the QR on a phone (a same-device scan is circular). The SDK NEVER writes `window.location`; navigation is always the host's, off the emitted event.
46
37
 
47
- The browser SDK CANNOT self-mint: `bt_*` bearers are rejected at the OTL-mint route. The host must own a server-side BFF that holds a `vt_*` cookie (HttpOnly, SameSite=Lax) and calls the sdk-api OTL-mint endpoint to produce the handoff URL.
38
+ Three token-free overlay events (NONE carries the url / OTL / short-code / expiry hint):
39
+ • `checktiv.idv.cross_device_opened`: the mint returned `ok` and the overlay is shown.
40
+ • `checktiv.idv.cross_device_unavailable`: the mint returned `unavailable` or URL validation failed.
41
+ • `checktiv.idv.cross_device_capped`: the completion poll hit its total time cap without the phone finishing. NOT an error and NOT a verdict: the QR panel stays mounted so the applicant can still finish on their phone.
48
42
 
49
43
  ## Runnable recipes
50
44
 
51
45
  Full, copy-pasteable code for each step is in `manifest.json` under `recipes`:
52
46
 
47
+ - **(e) Zero-lifecycle: sessions.create on the backend, Checktiv.mount on the frontend**
53
48
  - **(a) Backend: mint a session with your secret key**
54
49
  - **(b) Backend: mint a bt_* browser token and hand it to the frontend**
55
50
  - **(c) Frontend: init and mountProvisioned**
@@ -26,10 +26,15 @@
26
26
  }
27
27
  },
28
28
  "rules": [
29
+ {
30
+ "id": "zero-lifecycle-mount-is-preferred",
31
+ "title": "Prefer the zero-lifecycle Checktiv.mount entry",
32
+ "body": "The simplest integration is two pieces of code. (1) On YOUR backend, call `sessions.create({ templateId, applicant }, { idempotencyKey })` - a thin wrapper over `POST /v1/sessions` with your `ah_sk_*` secret key - and return ONLY the resulting `clientToken` to the browser. (2) In the browser, call `Checktiv.mount(target, { fetchToken, onComplete })`, where `fetchToken` returns that `clientToken`. `fetchToken` runs ONCE for the cold / new-device start, NOT on every expiry: the SDK exchanges the durable `clientToken` for a short-lived working token and refreshes it internally, so you write NO token-refresh endpoint, NO expiration handler, and NO resume endpoint. Key `sessions.create` idempotently per user or reservation so retries and new-device re-entry never double-bill. The legacy `Checktiv.init({ ... }).mountProvisioned({ target })` with a `getSessionToken` browser-token callback stays fully supported for existing integrations."
33
+ },
29
34
  {
30
35
  "id": "server-mints-the-session",
31
36
  "title": "The server mints the session, never the browser",
32
- "body": "Your backend calls the public API `POST /v1/sessions` with your secret key (`ah_sk_*`) and receives the session. The secret key is server-only: NEVER put `ah_sk_*` in browser code. To hand control to the frontend, your backend mints a short-lived browser token (`bt_*`) by calling `POST /sdk/v1/sessions/:sessionId/browser-token` with the same `ah_sk_*`; that route returns `{ data: { browserToken, expiresAt, modules, scopes } }`. Only the `bt_*` string crosses to the browser."
37
+ "body": "Your backend calls the public API `POST /v1/sessions` with your secret key (`ah_sk_*`) and receives the session. The secret key is server-only: NEVER put `ah_sk_*` in browser code. To hand control to the frontend, your backend mints a short-lived browser token (`bt_*`) by calling `POST /v1/sessions/{id}/browser_token` with the same `ah_sk_*`; that route returns `{ data: { browser_token, expires_at } }`. Only the `bt_*` string crosses to the browser."
33
38
  },
34
39
  {
35
40
  "id": "mount-provisioned-is-the-default",
@@ -68,11 +73,17 @@
68
73
  },
69
74
  {
70
75
  "id": "cross-device-handoff-hook",
71
- "title": "Wire cross-device QR handoff via openCrossDeviceOverlay",
72
- "body": "Cross-device handoff is wired via STANDALONE exports from the `@checktiv/sdk-web/idv/cross-device` subpath (NOT via an option on `IdvMountOptions` / `mountProvisioned`). Import: `import { openCrossDeviceOverlay, preloadCrossDeviceChunk } from '@checktiv/sdk-web/idv/cross-device';`\n\n`openCrossDeviceOverlay(opts: CrossDeviceOverlayOptions)` opens the overlay. Key fields on `CrossDeviceOverlayOptions`:\n • `target: HTMLElement`: the module's root container element.\n • `onOpenCrossDevice: CrossDeviceHandoffHook`: HOST-supplied mint hook. Type: `() => Promise<{ kind: 'ok'; url: string } | { kind: 'unavailable' }>`. Your host calls its own OTL-mint BFF and returns `{ kind: 'ok', url }` with the journey URL, or `{ kind: 'unavailable' }` if the mint fails. The URL MUST be `https:` (the SDK validates this and collapses `javascript:`, `http:`, and `data:` schemes to `'unavailable'`). The hook is re-callable (the overlay has a \"refresh link\" affordance); cap re-mints with a per-overlay counter on your host.\n • `copy: CrossDeviceCopy`: host-injected copy map; no hardcoded strings.\n • `isMobile: boolean`: `true` suppresses the QR (same-device scan is circular).\n • `emit: (e: ChecktivIdvEvent) => void`: forward to the host's `onEvent` handler.\n • `onClose?: () => void`: called when the applicant closes the overlay or `destroy()` is called.\n\nReturns `CrossDeviceOverlayHandle` with:\n • `setCompleting()`: marks the session completing (phone finished; overlay stays mounted with a waiting label instead of disappearing).\n • `destroy()`: tears down the overlay. Idempotent.\n\n`preloadCrossDeviceChunk()`: fire-and-forget preload of the lazy CDN chunk. Call it when the IDV module becomes active to reduce perceived latency on open.\n\nEvents (both are token-free, NEVER contain the url/OTL/short-code):\n • `checktiv.idv.cross_device_opened`: hook returned `ok`, overlay shown.\n • `checktiv.idv.cross_device_unavailable`: hook returned `unavailable` or URL validation failed.\nThere is NO `cross_device_completed` event. Completion is the HOST's responsibility: poll your own `/api/idv/cross-device-status` BFF (returns `{ advanced: boolean }`) and call `window.location.reload()` exactly once when `advanced: true`. The SDK module NEVER writes `window.location`. Use `setCompleting()` on the returned handle before reloading so the overlay shows a completing state rather than disappearing abruptly.\n\nThe browser SDK CANNOT self-mint: `bt_*` bearers are rejected at the OTL-mint route. The host must own a server-side BFF that holds a `vt_*` cookie (HttpOnly, SameSite=Lax) and calls the sdk-api OTL-mint endpoint to produce the handoff URL."
76
+ "title": "Cross-device QR handoff is built into the managed IDV module",
77
+ "body": "Cross-device handoff (a desktop applicant finishes on their phone via a QR + copy-link overlay) is OWNED BY THE MANAGED IDV MODULE. Render it with `client.mount('idv', { crossDeviceCopy, onEvent })` and the returned handle exposes `openCrossDevice()`: calling it opens the overlay OVER the still-live capture frame, runs the completion poll, and emits `checktiv.idv.submitted` when the phone finishes so your host reloads. You MUST `import '@checktiv/sdk-web/idv/cross-device';` to enable the overlay (the CDN script-tag bundle already includes it); without that import `openCrossDevice()` is a graceful, actionable no-op, as it also is before the applicant verification resolves or when no `crossDeviceCopy` was supplied. `IdvMountOptions` carries `crossDeviceCopy` (host-injected strings; the `unavailableMessage` key is required so no arm renders blank) and the OPTIONAL `onOpenCrossDevice` mint override; `IdvHandle` exposes `openCrossDevice?()`, so call it as `handle.openCrossDevice?.()`. NOTE: `mountProvisioned` and the zero-lifecycle `Checktiv.mount` entry do NOT expose the overlay: use the explicit `mount('idv', ...)` call when you need `openCrossDevice()`.\n\nWho mints the one-time handoff link depends on the token plane:\n • Working-token plane: the module mints the link ITSELF (it calls its own handoff endpoint `POST /sdk/v1/sessions/me/handoff`, authenticated by the applicant's short-lived working token, and caps the per-overlay \"refresh link\" re-mints). Supply `onOpenCrossDevice` ONLY to point the mint at your own backend instead.\n • Browser-token (`bt_*`) plane: the SDK CANNOT self-mint (the handoff route rejects a `bt_*` bearer), so you MUST supply `onOpenCrossDevice` backed by a server-held credential.\n\n`onOpenCrossDevice: CrossDeviceHandoffHook` is `() => Promise<{ kind: 'ok'; url: string } | { kind: 'unavailable' }>`. Return `{ kind: 'ok', url }` with the journey URL (which MUST be `https:`: the SDK collapses `javascript:`, `http:`, and `data:` schemes to `'unavailable'`) or `{ kind: 'unavailable' }` when the mint fails. The hook is re-callable for the overlay's \"refresh link\" affordance; the SDK caps re-mints per overlay.\n\nFor a FULLY CUSTOM IDV renderer built on `@checktiv/sdk-web/capture` (NOT the managed module), the STANDALONE exports drive the overlay yourself and you always supply the mint hook: `@checktiv/sdk-web/idv/cross-device` (`openCrossDeviceOverlay(opts)` opens the overlay, `preloadCrossDeviceChunk()` warms the lazy chunk) and `@checktiv/sdk-web/cross-device` (`mountCrossDevice(target, props)` renders the bare panel on a screen you fully control). `openCrossDeviceOverlay` takes `{ target, onOpenCrossDevice, copy, isMobile, emit, onClose }` plus an optional `poll` config, and returns a handle with `setCompleting()` (keep the QR mounted with a waiting label while your page reloads) and `destroy()` (idempotent). Set `isMobile: true` to suppress the QR on a phone (a same-device scan is circular). The SDK NEVER writes `window.location`; navigation is always the host's, off the emitted event.\n\nThree token-free overlay events (NONE carries the url / OTL / short-code / expiry hint):\n • `checktiv.idv.cross_device_opened`: the mint returned `ok` and the overlay is shown.\n • `checktiv.idv.cross_device_unavailable`: the mint returned `unavailable` or URL validation failed.\n • `checktiv.idv.cross_device_capped`: the completion poll hit its total time cap without the phone finishing. NOT an error and NOT a verdict: the QR panel stays mounted so the applicant can still finish on their phone."
73
78
  }
74
79
  ],
75
80
  "recipes": [
81
+ {
82
+ "id": "zero-lifecycle-mount",
83
+ "title": "(e) Zero-lifecycle: sessions.create on the backend, Checktiv.mount on the frontend",
84
+ "language": "ts",
85
+ "code": "// ---- YOUR BACKEND: the ONE lifecycle call. ah_sk_* stays server-only. ----\nimport { sessions } from '@checktiv/sdk-web';\n\napp.post('/kyc/session', async (req, res) => {\n const session = await sessions.create(\n { templateId: 'wt_...', applicant: { externalId: req.user.id } },\n {\n apiBase: `https://api.${process.env.CHECKTIV_REGION ?? \"us\"}.checktiv.com`,\n secretKey: process.env.CHECKTIV_SECRET_KEY, // ah_sk_* - NEVER in the browser\n idempotencyKey: req.user.id, // same user maps to the same session, no double-mint\n },\n );\n res.json({ clientToken: session.clientToken }); // low-privilege, resume-only\n});\n\n// ---- YOUR FRONTEND: mount and forget. No refresh endpoint, no expiry branch. ----\nimport { mount } from '@checktiv/sdk-web';\nimport '@checktiv/sdk-web/idv'; // self-registers each module mount may render\nimport '@checktiv/sdk-web/fraud';\n\nmount('#checktiv', {\n publishableKey: import.meta.env.PUBLIC_CHECKTIV_PK, // ah_pk_* - safe in the browser\n // Called ONCE for the cold start (and only for a new-device start), NOT on expiry.\n fetchToken: () => fetch('/kyc/session', { method: 'POST' })\n .then((r) => r.json()).then((x) => x.clientToken),\n onComplete: (result) => showThankYou(result), // capture submitted; verdict via webhook\n});"
86
+ },
76
87
  {
77
88
  "id": "backend-session-mint",
78
89
  "title": "(a) Backend: mint a session with your secret key",
@@ -83,7 +94,7 @@
83
94
  "id": "backend-browser-token-mint",
84
95
  "title": "(b) Backend: mint a bt_* browser token and hand it to the frontend",
85
96
  "language": "ts",
86
- "code": "// Your own endpoint that getSessionToken (recipe c) fetches. It mints a\n// fresh, short-lived bt_* for a session you already own server-side and\n// returns ONLY the bt_* string. ah_sk_* never leaves the server.\nimport express from 'express';\nconst app = express();\n\napp.post('/checktiv/browser-token', async (req, res) => {\n // Resolve the session id from YOUR session/auth, not from the client body.\n const sessionId = await resolveSessionIdForUser(req);\n const region = process.env.CHECKTIV_REGION ?? \"us\";\n const minted = await fetch(\n `https://api.${region}.checktiv.com/sdk/v1/sessions/${sessionId}/browser-token`,\n {\n method: 'POST',\n headers: { Authorization: `Bearer ${process.env.CHECKTIV_SECRET_KEY}` },\n },\n );\n const { data } = await minted.json(); // { browserToken, expiresAt, modules, scopes }\n res.json({ browserToken: data.browserToken }); // return only the bt_* string\n});"
97
+ "code": "// Your own endpoint that getSessionToken (recipe c) fetches. It mints a\n// fresh, short-lived bt_* for a session you already own server-side and\n// returns ONLY the bt_* string. ah_sk_* never leaves the server.\nimport express from 'express';\nconst app = express();\n\napp.post('/checktiv/browser-token', async (req, res) => {\n // Resolve the session id from YOUR session/auth, not from the client body.\n const sessionId = await resolveSessionIdForUser(req);\n const region = process.env.CHECKTIV_REGION ?? \"us\";\n const minted = await fetch(\n `https://api.${region}.checktiv.com/v1/sessions/${sessionId}/browser_token`,\n {\n method: 'POST',\n headers: { Authorization: `Bearer ${process.env.CHECKTIV_SECRET_KEY}` },\n },\n );\n const { data } = await minted.json(); // { browser_token, expires_at }\n res.json({ browserToken: data.browser_token }); // return only the bt_* string\n});"
87
98
  },
88
99
  {
89
100
  "id": "frontend-init-mount-provisioned",