@soliton-id/web-sdk 1.0.1 → 1.1.1
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/CHANGELOG.md +42 -0
- package/README.md +232 -54
- package/dist/hostedUrl.d.ts +2 -2
- package/dist/index.cjs +42 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +42 -3
- package/dist/server.cjs +119 -0
- package/dist/server.d.ts +34 -0
- package/dist/server.mjs +94 -0
- package/dist/trustedOrigin.d.ts +2 -0
- package/dist/types.d.ts +20 -4
- package/package.json +10 -4
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,48 @@ All notable changes to `@soliton-id/web-sdk`. The `soliton-v1` postMessage
|
|
|
4
4
|
protocol is frozen: changes are additive only, so older SDK versions keep
|
|
5
5
|
working against the hosted flow.
|
|
6
6
|
|
|
7
|
+
## 1.1.1 — 2026-10-29
|
|
8
|
+
|
|
9
|
+
Package metadata only; no code or API changes. The repository and issue links
|
|
10
|
+
point to `georges-soliton/soliton-front-end`.
|
|
11
|
+
|
|
12
|
+
## 1.1.0 — 2026-10-29
|
|
13
|
+
|
|
14
|
+
Additive only. `open()` and the `soliton-v1` protocol are unchanged, so
|
|
15
|
+
existing integrations need no code change.
|
|
16
|
+
|
|
17
|
+
- Add the isolated `/server` export and downloadable `soliton-server.mjs` for
|
|
18
|
+
Node.js 18+: combined v2 session creation and link issuance, with a pre-link
|
|
19
|
+
persistence callback, bounded requests, explicit link recovery and
|
|
20
|
+
structured partial-failure errors.
|
|
21
|
+
- Add the optional `verificationOrigin` option for customer-hosted deployments.
|
|
22
|
+
- Bound initial iframe readiness to 30 seconds; report `load_timeout` and clean
|
|
23
|
+
up stalled overlays without emitting a verification result. A frame that
|
|
24
|
+
stays blank until then usually means the embedding page's origin is not
|
|
25
|
+
registered for the link's environment.
|
|
26
|
+
- Document the v2 session flow: completion callbacks keep the UUID
|
|
27
|
+
`verificationId`, and results are read server-side from the saved session.
|
|
28
|
+
- Document server-side `liveness_tier` selection for Standard, Medium and High
|
|
29
|
+
in sandbox and production; the returned URL selects the capture.
|
|
30
|
+
- Document that registered embedding origins apply on the next page load, with
|
|
31
|
+
no Soliton deployment.
|
|
32
|
+
|
|
33
|
+
## 1.0.2 — 2026-08-06
|
|
34
|
+
|
|
35
|
+
Documentation only. No runtime, API, or bundle changes; `soliton-v1` is
|
|
36
|
+
unchanged and `1.0.0`/`1.0.1` remain fully compatible.
|
|
37
|
+
|
|
38
|
+
- Reframed `sk_test_` as the **sandbox environment** rather than a
|
|
39
|
+
fixtures-only mode. A sandbox credential now runs real captures; the
|
|
40
|
+
deterministic `pass` / `fail` / `partial` fixtures remain available and
|
|
41
|
+
remain sandbox-only. `sk_live_` still cannot request a fixture.
|
|
42
|
+
- Documented that redirect domains, iframe origins, and webhook endpoints are
|
|
43
|
+
registered per environment, so a staging origin can embed sandbox links
|
|
44
|
+
without being able to embed production ones, and each environment's webhook
|
|
45
|
+
has its own signing secret.
|
|
46
|
+
- Documented the additive `environment` field on the create response, every
|
|
47
|
+
status read, and every webhook payload.
|
|
48
|
+
|
|
7
49
|
## 1.0.1 — 2026-08-02
|
|
8
50
|
|
|
9
51
|
Documentation only. No runtime, API, or bundle changes; `soliton-v1` is
|
package/README.md
CHANGED
|
@@ -1,3 +1,87 @@
|
|
|
1
|
+
## Customer-hosted appliances
|
|
2
|
+
|
|
3
|
+
Set `verificationOrigin` to your trusted customer-hosted HTTPS origin when calling
|
|
4
|
+
`open({ url, verificationOrigin, onComplete })`. The issued URL must match that
|
|
5
|
+
origin exactly. Omit the option for the existing cloud allowlist. The iframe
|
|
6
|
+
protocol, completion callbacks and server-side result/webhook contracts are
|
|
7
|
+
unchanged. See [the appliance guide](../docs/on-premise.md) for runtime origins,
|
|
8
|
+
local assets, packaging, iframe permissions and top-level mobile WebViews.
|
|
9
|
+
|
|
10
|
+
## Console API v2
|
|
11
|
+
|
|
12
|
+
The SDK opens `https://soliton.id/v/{token}` links from
|
|
13
|
+
`POST /api/v2/sessions/{id}/link`. Create the session first with your console
|
|
14
|
+
API key using `POST /api/v2/sessions`; resolve `browser_path` against the
|
|
15
|
+
verification origin and pass only the resulting URL to `open()`.
|
|
16
|
+
Both redirect and embedded methods use the same API key and v2 endpoints.
|
|
17
|
+
Register exact embedding origins per account/environment; they apply on the next
|
|
18
|
+
page load with no frontend deploy. There is no wildcard or implicit
|
|
19
|
+
same-company-domain grant.
|
|
20
|
+
|
|
21
|
+
`onComplete({ verificationId })` is unchanged: it carries the browser UUID alias,
|
|
22
|
+
not a verdict. Your backend must fetch `GET /api/v2/sessions/{savedSessionId}`
|
|
23
|
+
using the `ses_…` ID bound to the authenticated user at creation. Never trust the
|
|
24
|
+
callback ID as the authority for a lookup. The `/sdk/v1/` path and `soliton-v1`
|
|
25
|
+
message marker are the SDK protocol, independent of REST API version.
|
|
26
|
+
|
|
27
|
+
## Optional v2 server helper (Node.js 18+)
|
|
28
|
+
|
|
29
|
+
The helper keeps v2 intact while combining session creation and link issuance
|
|
30
|
+
into one server-side function. Download `/sdk/v1/soliton-server.mjs` from your
|
|
31
|
+
Soliton deployment into your backend project, review it, and commit the file.
|
|
32
|
+
Do not use a browser script tag or dynamically download it on every request.
|
|
33
|
+
The source package also exports `@soliton-id/web-sdk/server` (ESM/CJS + types);
|
|
34
|
+
use the downloaded module until a package containing that export is published.
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
import { createVerificationClient, VerificationStartError } from './soliton-server.mjs';
|
|
38
|
+
const soliton = createVerificationClient({
|
|
39
|
+
apiOrigin: process.env.API_ORIGIN,
|
|
40
|
+
verificationOrigin: process.env.VERIFICATION_ORIGIN,
|
|
41
|
+
apiKey: process.env.API_KEY,
|
|
42
|
+
timeoutMs: 20000, // Per request, including its response body.
|
|
43
|
+
});
|
|
44
|
+
// user/attempt come from your authenticated server endpoint.
|
|
45
|
+
// saveSession/saveLink are your application database operations.
|
|
46
|
+
const { sessionId, url } = await soliton.startVerification({
|
|
47
|
+
userReference: user.id,
|
|
48
|
+
tier: 'high',
|
|
49
|
+
idempotencyKey: attempt.id,
|
|
50
|
+
onSessionCreated: async id => { await saveSession(user.id, attempt.id, id); },
|
|
51
|
+
});
|
|
52
|
+
await saveLink(attempt.id, url);
|
|
53
|
+
// Return only { url } to the browser. Keep sessionId on your server.
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
This still makes two HTTP requests, with no automatic retries. Only session
|
|
57
|
+
creation is idempotent; the combined helper is **not** end-to-end idempotent.
|
|
58
|
+
Persist a stable key for each logical attempt, serialize duplicate requests,
|
|
59
|
+
and return a securely stored URL for an attempt that already succeeded.
|
|
60
|
+
Neither successful URLs nor service credentials belong in logs.
|
|
61
|
+
|
|
62
|
+
`VerificationStartError` includes `stage` (`create`, `persist`, `link`),
|
|
63
|
+
`sessionId` when known, `status` when a response was available, and `retryAfter`
|
|
64
|
+
(the raw HTTP Retry-After value, if supplied). It omits arbitrary response bodies
|
|
65
|
+
and transport errors so secrets cannot leak through its message.
|
|
66
|
+
|
|
67
|
+
- `create`: resolve the failure and retry with the same idempotency key and body;
|
|
68
|
+
a lost response may conceal a successful creation. Respect 429/Retry-After;
|
|
69
|
+
fix authentication or conflicting inputs rather than retrying unchanged.
|
|
70
|
+
- `persist`: your `onSessionCreated` callback failed. No link was requested.
|
|
71
|
+
Save the known session ID before proceeding.
|
|
72
|
+
- `link`: the session exists; retain the ID. A timeout or malformed response may
|
|
73
|
+
conceal successful issuance. Make an explicit recovery decision, then call
|
|
74
|
+
`soliton.issueVerificationLink(savedSessionId)` to get `{ sessionId, url }`.
|
|
75
|
+
This replaces the previous link; do not rotate a link already in use.
|
|
76
|
+
|
|
77
|
+
Origins must be HTTPS (HTTP only for loopback development), without credentials,
|
|
78
|
+
paths, queries or fragments. Never derive them from browser input. The helper
|
|
79
|
+
refuses API redirects, validates returned hosted paths and rejects browser use.
|
|
80
|
+
It does not store state, authenticate your user, or make access decisions.
|
|
81
|
+
|
|
82
|
+
See `/docs/integration/sdk` for the full v2 SDK guide and `/docs/integration/api`
|
|
83
|
+
for raw requests. Existing v1 instructions below remain compatible.
|
|
84
|
+
|
|
1
85
|
# @soliton-id/web-sdk
|
|
2
86
|
|
|
3
87
|
Soliton identity verification for the browser. `open()` overlays the Soliton
|
|
@@ -35,9 +119,19 @@ bundles the npm package does not need that remote script source.
|
|
|
35
119
|
|
|
36
120
|
## Quickstart
|
|
37
121
|
|
|
38
|
-
Your backend creates a verification with your
|
|
39
|
-
|
|
40
|
-
|
|
122
|
+
Your backend chooses the liveness tier and creates a verification with your
|
|
123
|
+
secret API key (never do this from the browser):
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
# Use sk_live_... for production; sk_test_... for sandbox.
|
|
127
|
+
curl -X POST https://api.soliton.id/api/v1/verifications \
|
|
128
|
+
-H "Authorization: Bearer sk_test_..." \
|
|
129
|
+
-H "Content-Type: application/json" \
|
|
130
|
+
-d '{"liveness_tier":"standard","client_reference_id":"user-8412"}'
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Store the returned `verification_id`, requested tier and `environment` against
|
|
134
|
+
your authenticated user on your backend. Hand the returned `url` to your page:
|
|
41
135
|
|
|
42
136
|
```ts
|
|
43
137
|
import { open } from "@soliton-id/web-sdk";
|
|
@@ -56,30 +150,82 @@ const handle = open({
|
|
|
56
150
|
},
|
|
57
151
|
onError: ({ code, message }) => {
|
|
58
152
|
// link_expired | link_invalid | origin_not_allowed |
|
|
59
|
-
// storage_unavailable | internal
|
|
153
|
+
// storage_unavailable | internal | load_timeout
|
|
60
154
|
},
|
|
61
155
|
onClose: () => {
|
|
62
|
-
// The
|
|
156
|
+
// The flow closed before completion (dismissal or initial load timeout).
|
|
63
157
|
},
|
|
64
158
|
});
|
|
65
159
|
|
|
66
160
|
// handle.close() dismisses the overlay programmatically.
|
|
67
161
|
```
|
|
68
162
|
|
|
69
|
-
The overlay is always full-viewport
|
|
70
|
-
|
|
163
|
+
The overlay is always full-viewport for every tier. High uses the whole screen
|
|
164
|
+
for its light challenge; Standard and Medium keep that layout without flashes.
|
|
165
|
+
Do not clip the overlay into a widget.
|
|
166
|
+
|
|
167
|
+
## Choose liveness (sandbox and production)
|
|
168
|
+
|
|
169
|
+
| Server-side `liveness_tier` | Capture | Browser permissions |
|
|
170
|
+
| --- | --- | --- |
|
|
171
|
+
| `standard` | Camera only, no sound or flashes | Camera |
|
|
172
|
+
| `medium` | Camera and sound, no flashes | Camera and microphone |
|
|
173
|
+
| `high` (default) | Camera, sound and light | Camera and microphone |
|
|
174
|
+
|
|
175
|
+
All three use the same `POST /api/v1/verifications` endpoint and work with both
|
|
176
|
+
`sk_test_` and `sk_live_` keys. Omitting the field preserves High for existing
|
|
177
|
+
integrations. Choose the tier using your server's policy; do not let untrusted
|
|
178
|
+
browser input override that policy.
|
|
179
|
+
|
|
180
|
+
The URL binds the chosen tier for capture and every retake. Pass it unchanged
|
|
181
|
+
to `open()` or use it as a redirect: **there is no tier option in `open()` and
|
|
182
|
+
no tier query parameter to add**. Existing SDK versions already support every
|
|
183
|
+
tier; no package upgrade or separate bundle is needed. To change tiers, create
|
|
184
|
+
a new verification.
|
|
185
|
+
|
|
186
|
+
On completion, require the authenticated status response's `liveness_tier`
|
|
187
|
+
and `environment` to match the values stored for your user before accepting
|
|
188
|
+
the verdict. The callback still carries only `verificationId`.
|
|
71
189
|
|
|
72
190
|
## Callback semantics
|
|
73
191
|
|
|
192
|
+
An integration that provides its own completion screen can opt out of the
|
|
193
|
+
hosted result summary by adding `completion=parent` to the hosted URL:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
const embeddedUrl = new URL(url);
|
|
197
|
+
embeddedUrl.searchParams.set("completion", "parent");
|
|
198
|
+
const handle = open({
|
|
199
|
+
url: embeddedUrl.toString(),
|
|
200
|
+
onComplete: () => {
|
|
201
|
+
handle.close();
|
|
202
|
+
// Show your own final screen and have your backend fetch the saved result.
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
The preference is honored only inside an iframe after its parent origin passes
|
|
208
|
+
the usual registration check. It hands control back immediately at a terminal
|
|
209
|
+
state, without the default three-second result summary. Retryable failures and
|
|
210
|
+
errors remain in the hosted flow. Top-level redirects and embeds without this
|
|
211
|
+
opt-in retain their existing behavior. Existing SDK bundles preserve this URL
|
|
212
|
+
parameter; the hosted frontend must support it. The callback still means the
|
|
213
|
+
attempt ended, not that verification passed; fetch the verdict server-to-server.
|
|
214
|
+
|
|
74
215
|
- `onComplete({ verificationId })`: fired once, when the user reaches a
|
|
75
216
|
terminal state. It carries ONLY the verification id, never a verdict.
|
|
76
217
|
- `onError({ code, message })`: flow-level errors (`link_expired`,
|
|
77
218
|
`link_invalid`, `origin_not_allowed`, `storage_unavailable` when the user's
|
|
78
219
|
browser blocks the session storage the flow needs, `internal`). The overlay
|
|
79
220
|
stays open so the hosted page can show its own error card; call
|
|
80
|
-
`handle.close()` if you want it gone.
|
|
81
|
-
|
|
82
|
-
|
|
221
|
+
`handle.close()` if you want it gone. If the frame has not become ready or
|
|
222
|
+
reported an error within 30 seconds, `load_timeout` is emitted instead and
|
|
223
|
+
the overlay closes automatically, followed by `onClose`. This is a loading
|
|
224
|
+
failure, never a biometric verdict. A frame that stayed blank before it
|
|
225
|
+
almost always means your page's exact origin is not registered for the
|
|
226
|
+
link's environment: the browser console shows a `frame-ancestors` error. Once ready, there is no SDK capture deadline.
|
|
227
|
+
- `onClose()`: the flow closed before completion (the X button, hosted Cancel,
|
|
228
|
+
`handle.close()`, or initial load timeout). Not fired after `onComplete`.
|
|
83
229
|
|
|
84
230
|
## Getting the result (server-to-server only)
|
|
85
231
|
|
|
@@ -87,12 +233,12 @@ that needs the whole screen, so it cannot be clipped into a widget.
|
|
|
87
233
|
BACKEND with your API key:
|
|
88
234
|
|
|
89
235
|
1. `GET /api/v1/verifications/{id}`: the verdict (`verified`,
|
|
90
|
-
`partial_verification`, plus `portrait_available`).
|
|
236
|
+
`partial_verification`, `liveness_tier`, plus `portrait_available`).
|
|
91
237
|
2. `GET /api/v1/verifications/{id}/portrait`: a JPEG portrait of the verified
|
|
92
238
|
user, selected server-side from the capture under neutral lighting.
|
|
93
239
|
Returns 404 until the verification is complete or when no acceptable
|
|
94
240
|
portrait frame existed. Right after completion, `portrait_available` can
|
|
95
|
-
briefly read `false` while extraction finishes; once status is `
|
|
241
|
+
briefly read `false` while extraction finishes; once status is `completed`
|
|
96
242
|
just fetch the portrait and treat a 404 after one short retry as final.
|
|
97
243
|
|
|
98
244
|
Your page and the SDK never see the verdict or the portrait: callbacks and
|
|
@@ -118,26 +264,32 @@ creation and check it on every status read to make this auditable.
|
|
|
118
264
|
|
|
119
265
|
## Frame-origin registration
|
|
120
266
|
|
|
121
|
-
Embedding is off by default
|
|
267
|
+
Embedding is off by default. One origin list per environment controls two checks:
|
|
122
268
|
|
|
123
269
|
1. Your Soliton account must have the exact origin of each page that runs the
|
|
124
270
|
SDK and directly contains its Soliton iframe registered (scheme + host +
|
|
125
|
-
port, e.g. `https://app.example.com`)
|
|
126
|
-
have to be the browser's
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
271
|
+
port, e.g. `https://app.example.com`), **for the environment that will open
|
|
272
|
+
the link**. This direct-parent page does not have to be the browser's
|
|
273
|
+
top-level page. A link is checked against the origins registered for the
|
|
274
|
+
environment of the key that created it, so `https://dev.example.com` can be
|
|
275
|
+
a sandbox embedder without becoming a production one.
|
|
276
|
+
2. The same list feeds the hosted page's `frame-ancestors` policy, read on
|
|
277
|
+
every page load, so a registered origin applies with no deployment. If the
|
|
278
|
+
SDK page is itself framed, every origin above it in the frame chain must be
|
|
279
|
+
registered for the same environment too, because the browser checks
|
|
280
|
+
`frame-ancestors` against the whole chain.
|
|
281
|
+
|
|
282
|
+
Opened inside an iframe on any other origin, the browser blocks the frame: it
|
|
283
|
+
stays blank and the SDK reports `load_timeout` after 30 seconds.
|
|
284
|
+
`origin_not_allowed` means the hosted page loaded but this particular link may
|
|
285
|
+
not be embedded there, for example a console test link.
|
|
286
|
+
|
|
287
|
+
During the invite-only pilot, Soliton registers your origins. Send your
|
|
136
288
|
Soliton contact every exact production and local direct-parent origin before
|
|
137
289
|
testing. Include a staging origin only when Soliton has supplied a working
|
|
138
290
|
staging flow URL. For a nested integration, also send every other ancestor
|
|
139
|
-
origin. Wildcards are not accepted
|
|
140
|
-
|
|
291
|
+
origin. Wildcards are not accepted. A registered origin applies on the next
|
|
292
|
+
page load; no Soliton deployment is involved.
|
|
141
293
|
|
|
142
294
|
Register the direct-parent page's origin, not a URL path. For example,
|
|
143
295
|
`https://app.example.com` and `https://app.example.com:8443` are different
|
|
@@ -174,8 +326,9 @@ Permissions-Policy: camera=(self "https://soliton.id"), microphone=(self "https:
|
|
|
174
326
|
bundles the npm package.
|
|
175
327
|
- If any document above the Soliton iframe explicitly restricts camera or
|
|
176
328
|
microphone with `Permissions-Policy`, every level in a nested frame chain
|
|
177
|
-
must allow and delegate
|
|
178
|
-
|
|
329
|
+
must allow and delegate the features used by the selected tier: camera for
|
|
330
|
+
Standard; camera and microphone for Medium and High. The examples allow all
|
|
331
|
+
tiers. An upstream denial cannot be overridden lower in the chain. The SDK already
|
|
179
332
|
sets the matching `allow="camera; microphone"` attribute on the iframe it
|
|
180
333
|
creates.
|
|
181
334
|
|
|
@@ -190,38 +343,57 @@ soliton.id. Do not add Soliton to your page's `frame-ancestors`: that directive
|
|
|
190
343
|
controls who may frame your page, while `frame-src` controls what your page may
|
|
191
344
|
frame.
|
|
192
345
|
|
|
193
|
-
##
|
|
346
|
+
## Sandbox
|
|
347
|
+
|
|
348
|
+
Your account has two environments and the API key prefix selects one:
|
|
349
|
+
`sk_live_` is **production**, `sk_test_` is **sandbox**. Each has its own
|
|
350
|
+
credentials, verifications, redirect domains, iframe origins, and webhook
|
|
351
|
+
endpoint with its own signing secret.
|
|
194
352
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
353
|
+
A sandbox verification opens in the overlay exactly like a production one and
|
|
354
|
+
runs the same real capture — but it is checked against the iframe origins
|
|
355
|
+
registered for **sandbox**, so your staging page can embed sandbox links
|
|
356
|
+
without being able to embed production ones. Register each origin for the
|
|
357
|
+
environment that will use it.
|
|
358
|
+
|
|
359
|
+
Sandbox is additionally the only place that can request a fixed outcome, which
|
|
360
|
+
is the practical way to exercise your `partial` branch:
|
|
198
361
|
|
|
199
362
|
```jsonc
|
|
200
363
|
// POST /api/v1/verifications with an sk_test_ key
|
|
201
|
-
{ "client_reference_id": "user-8412", "test_fixture_scenario": "partial" }
|
|
364
|
+
{ "liveness_tier": "medium", "client_reference_id": "user-8412", "test_fixture_scenario": "partial" }
|
|
202
365
|
```
|
|
203
366
|
|
|
204
|
-
| `test_fixture_scenario` | `verified` | `partial_verification` |
|
|
205
|
-
|
|
|
206
|
-
| `pass`
|
|
207
|
-
| `partial`
|
|
208
|
-
| `fail`
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
367
|
+
| `test_fixture_scenario` | `verified` | Standard `partial_verification` | Medium / High `partial_verification` |
|
|
368
|
+
| --- | --- | --- | --- |
|
|
369
|
+
| `pass` | `true` | `false` | `false` |
|
|
370
|
+
| `partial` | `true` | `false` | `true` |
|
|
371
|
+
| `fail` | `false` | `false` | `false` |
|
|
372
|
+
|
|
373
|
+
A fixture's URL opens in the overlay and fires `onComplete` the same way, but
|
|
374
|
+
never requests camera permission and never records anything. The hosted page
|
|
375
|
+
marks itself as synthetic on screen.
|
|
376
|
+
|
|
377
|
+
Fixtures do not exercise liveness, anti-spoof, or anti-deepfake logic, and
|
|
378
|
+
simulate no errors or timeouts — only the three terminal verdicts. Real
|
|
379
|
+
sandbox captures do run the full pipeline; a production `sk_live_` key cannot
|
|
380
|
+
request fixtures at all.
|
|
381
|
+
|
|
382
|
+
The two environments are strictly isolated: neither credential can read the
|
|
383
|
+
other's verifications, even within one account. Every create response, status
|
|
384
|
+
read, and webhook carries `environment` (`sandbox` or `production`) and
|
|
385
|
+
`result_source` (`biometric` or `synthetic_test_fixture`) — assert both
|
|
386
|
+
anywhere a real production verdict matters.
|
|
387
|
+
|
|
388
|
+
Keep the same explicit `liveness_tier` when moving from sandbox to production;
|
|
389
|
+
switch keys and use origins registered for production. The completed status
|
|
390
|
+
response and signed webhook include `liveness_tier`: require it to match your
|
|
391
|
+
stored choice, and reject missing or mismatched tiers. Fetch the verdict using
|
|
392
|
+
the verification id bound to your user's session, as in the quickstart above.
|
|
393
|
+
|
|
394
|
+
Standard has only verified and not-verified outcomes: completed results always
|
|
395
|
+
set `partial_verification: false`. Medium and High can still return partial
|
|
396
|
+
verification. A Standard `partial` test fixture resolves to a verified result.
|
|
225
397
|
|
|
226
398
|
## Completion webhooks
|
|
227
399
|
|
|
@@ -233,6 +405,12 @@ deduplicate on `X-Soliton-Event-Id`, and respond `2xx` within 5 seconds. Full
|
|
|
233
405
|
setup, the signature recipe, and retry semantics are in the
|
|
234
406
|
[integration guide](https://soliton.id/docs/integration).
|
|
235
407
|
|
|
408
|
+
Webhooks are configured per environment: one endpoint for sandbox and one for
|
|
409
|
+
production, each with its own signing secret. A sandbox completion is only ever
|
|
410
|
+
delivered to the sandbox endpoint, so a staging receiver cannot validate — or
|
|
411
|
+
forge — a production event. The payload's `environment` field tells you which
|
|
412
|
+
is which if you point both at one URL.
|
|
413
|
+
|
|
236
414
|
## How it works
|
|
237
415
|
|
|
238
416
|
`open()` accepts hosted URLs from `https://soliton.id`. When Soliton has
|
package/dist/hostedUrl.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Validation of the hosted verification URL handed to SolitonVerify.open().
|
|
3
3
|
*
|
|
4
4
|
* The SDK only ever frames URLs that look exactly like the ones minted by
|
|
5
|
-
*
|
|
5
|
+
* the v1 verification API or v2 session link API: an approved Soliton origin (http strictly for
|
|
6
6
|
* localhost dev) and a bare /v/{32 hex} path. Everything else throws
|
|
7
7
|
* synchronously — a bad URL is an integration bug, not a runtime flow event,
|
|
8
8
|
* so it must fail loudly at the call site instead of rendering an
|
|
@@ -14,4 +14,4 @@ export interface ParsedHostedUrl {
|
|
|
14
14
|
/** Origin the SDK will trust for postMessage events. */
|
|
15
15
|
hostedOrigin: string;
|
|
16
16
|
}
|
|
17
|
-
export declare function parseHostedUrl(raw: string): ParsedHostedUrl;
|
|
17
|
+
export declare function parseHostedUrl(raw: string, verificationOrigin?: string): ParsedHostedUrl;
|
package/dist/index.cjs
CHANGED
|
@@ -27,13 +27,24 @@ __export(index_exports, {
|
|
|
27
27
|
});
|
|
28
28
|
module.exports = __toCommonJS(index_exports);
|
|
29
29
|
|
|
30
|
+
// sdk/src/trustedOrigin.ts
|
|
31
|
+
function trustedOrigin(value) {
|
|
32
|
+
const url = new URL(value);
|
|
33
|
+
const local = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
34
|
+
if (url.origin !== value || url.username || url.password || value.includes("*") || url.protocol !== "https:" && !(url.protocol === "http:" && local)) {
|
|
35
|
+
throw new Error("Use an exact HTTPS origin (HTTP loopback is allowed for development)");
|
|
36
|
+
}
|
|
37
|
+
return url.origin;
|
|
38
|
+
}
|
|
39
|
+
|
|
30
40
|
// sdk/src/hostedUrl.ts
|
|
31
41
|
var HOSTED_PATH_RE = /^\/v\/[0-9a-f]{32}$/;
|
|
32
42
|
var ALLOWED_HTTPS_ORIGINS = /* @__PURE__ */ new Set([
|
|
33
43
|
"https://soliton.id",
|
|
34
44
|
"https://staging.soliton.id"
|
|
35
45
|
]);
|
|
36
|
-
function parseHostedUrl(raw) {
|
|
46
|
+
function parseHostedUrl(raw, verificationOrigin) {
|
|
47
|
+
const configuredOrigin = verificationOrigin === void 0 ? void 0 : trustedOrigin(verificationOrigin);
|
|
37
48
|
let url;
|
|
38
49
|
try {
|
|
39
50
|
url = new URL(raw);
|
|
@@ -41,7 +52,10 @@ function parseHostedUrl(raw) {
|
|
|
41
52
|
throw new Error(`SolitonVerify: "url" is not a valid absolute URL`);
|
|
42
53
|
}
|
|
43
54
|
const isLocalHttp = url.protocol === "http:" && (url.hostname === "localhost" || url.hostname === "127.0.0.1");
|
|
44
|
-
if (
|
|
55
|
+
if (configuredOrigin !== void 0 && url.origin !== configuredOrigin) {
|
|
56
|
+
throw new Error('SolitonVerify: "url" does not match the configured verificationOrigin');
|
|
57
|
+
}
|
|
58
|
+
if (configuredOrigin === void 0 && !ALLOWED_HTTPS_ORIGINS.has(url.origin) && !isLocalHttp) {
|
|
45
59
|
throw new Error(
|
|
46
60
|
'SolitonVerify: "url" must be hosted on https://soliton.id or https://staging.soliton.id (http is allowed for localhost/127.0.0.1 development only)'
|
|
47
61
|
);
|
|
@@ -166,6 +180,7 @@ var EMBED_PROTOCOL = "soliton-v1";
|
|
|
166
180
|
var AUTO_CLOSE_DELAY_MS = 250;
|
|
167
181
|
var HELLO_INTERVAL_MS = 250;
|
|
168
182
|
var HELLO_MAX_PINGS = 40;
|
|
183
|
+
var READY_TIMEOUT_MS = 3e4;
|
|
169
184
|
function open(options) {
|
|
170
185
|
if (!options || typeof options.url !== "string") {
|
|
171
186
|
throw new Error('SolitonVerify: open() requires an options object with a "url"');
|
|
@@ -173,14 +188,16 @@ function open(options) {
|
|
|
173
188
|
if (typeof options.onComplete !== "function") {
|
|
174
189
|
throw new Error("SolitonVerify: open() requires an onComplete callback");
|
|
175
190
|
}
|
|
176
|
-
const { url, hostedOrigin } = parseHostedUrl(options.url);
|
|
191
|
+
const { url, hostedOrigin } = parseHostedUrl(options.url, options.verificationOrigin);
|
|
177
192
|
const src = new URL(url.toString());
|
|
178
193
|
src.searchParams.set("mode", "embed");
|
|
179
194
|
src.searchParams.set("sdk", "1");
|
|
180
195
|
if (options.locale) src.searchParams.set("locale", options.locale);
|
|
181
196
|
let closed = false;
|
|
182
197
|
let terminalDelivered = false;
|
|
198
|
+
let loadTimedOut = false;
|
|
183
199
|
let autoCloseTimer = null;
|
|
200
|
+
let readyTimer = null;
|
|
184
201
|
const overlay = createOverlay(src.toString(), () => close());
|
|
185
202
|
let helloPings = 0;
|
|
186
203
|
const postHello = () => {
|
|
@@ -205,8 +222,13 @@ function open(options) {
|
|
|
205
222
|
clearInterval(helloTimer);
|
|
206
223
|
overlay.iframe.removeEventListener("load", onIframeLoad);
|
|
207
224
|
}
|
|
225
|
+
function stopReadyWait() {
|
|
226
|
+
if (readyTimer !== null) clearTimeout(readyTimer);
|
|
227
|
+
readyTimer = null;
|
|
228
|
+
}
|
|
208
229
|
const onMessage = (event) => {
|
|
209
230
|
var _a;
|
|
231
|
+
if (closed || loadTimedOut || terminalDelivered) return;
|
|
210
232
|
if (event.origin !== hostedOrigin) return;
|
|
211
233
|
if (!overlay.iframe.contentWindow || event.source !== overlay.iframe.contentWindow) {
|
|
212
234
|
return;
|
|
@@ -218,10 +240,12 @@ function open(options) {
|
|
|
218
240
|
stopHello();
|
|
219
241
|
switch (data.type) {
|
|
220
242
|
case "soliton:ready":
|
|
243
|
+
stopReadyWait();
|
|
221
244
|
overlay.markReady();
|
|
222
245
|
break;
|
|
223
246
|
case "soliton:complete": {
|
|
224
247
|
if (terminalDelivered) break;
|
|
248
|
+
stopReadyWait();
|
|
225
249
|
terminalDelivered = true;
|
|
226
250
|
autoCloseTimer = setTimeout(() => close(), AUTO_CLOSE_DELAY_MS);
|
|
227
251
|
options.onComplete({
|
|
@@ -230,6 +254,7 @@ function open(options) {
|
|
|
230
254
|
break;
|
|
231
255
|
}
|
|
232
256
|
case "soliton:error":
|
|
257
|
+
stopReadyWait();
|
|
233
258
|
(_a = options.onError) == null ? void 0 : _a.call(options, {
|
|
234
259
|
code: typeof data.code === "string" ? data.code : "internal",
|
|
235
260
|
message: typeof data.message === "string" ? data.message : ""
|
|
@@ -243,11 +268,25 @@ function open(options) {
|
|
|
243
268
|
}
|
|
244
269
|
};
|
|
245
270
|
window.addEventListener("message", onMessage);
|
|
271
|
+
readyTimer = setTimeout(() => {
|
|
272
|
+
var _a;
|
|
273
|
+
if (readyTimer === null || closed || terminalDelivered) return;
|
|
274
|
+
loadTimedOut = true;
|
|
275
|
+
try {
|
|
276
|
+
(_a = options.onError) == null ? void 0 : _a.call(options, {
|
|
277
|
+
code: "load_timeout",
|
|
278
|
+
message: "Verification did not load. Please try again."
|
|
279
|
+
});
|
|
280
|
+
} finally {
|
|
281
|
+
close();
|
|
282
|
+
}
|
|
283
|
+
}, READY_TIMEOUT_MS);
|
|
246
284
|
function close() {
|
|
247
285
|
var _a;
|
|
248
286
|
if (closed) return;
|
|
249
287
|
closed = true;
|
|
250
288
|
if (autoCloseTimer !== null) clearTimeout(autoCloseTimer);
|
|
289
|
+
stopReadyWait();
|
|
251
290
|
stopHello();
|
|
252
291
|
window.removeEventListener("message", onMessage);
|
|
253
292
|
overlay.destroy();
|
package/dist/index.d.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Soliton Web SDK v1: embedded verification widget (Stripe/Sumsub wrapper
|
|
3
3
|
* pattern). open() frames the EXISTING hosted verification page in a
|
|
4
4
|
* full-viewport overlay on the customer's site and relays its lifecycle via
|
|
5
|
-
* callbacks; the capture engine
|
|
6
|
-
* iframe, so anti-injection defenses update with
|
|
5
|
+
* callbacks; the capture engine runs on the trusted verification origin inside
|
|
6
|
+
* the iframe, so anti-injection defenses update with each frontend release and the
|
|
7
7
|
* verdict never touches the customer page (only the verification id does).
|
|
8
8
|
*
|
|
9
9
|
* Built artifacts (npm run build:sdk):
|
package/dist/index.mjs
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
/* Soliton Web SDK v1. Built from sdk/src via `npm run build:sdk`; do not edit by hand. */
|
|
2
2
|
|
|
3
|
+
// sdk/src/trustedOrigin.ts
|
|
4
|
+
function trustedOrigin(value) {
|
|
5
|
+
const url = new URL(value);
|
|
6
|
+
const local = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
7
|
+
if (url.origin !== value || url.username || url.password || value.includes("*") || url.protocol !== "https:" && !(url.protocol === "http:" && local)) {
|
|
8
|
+
throw new Error("Use an exact HTTPS origin (HTTP loopback is allowed for development)");
|
|
9
|
+
}
|
|
10
|
+
return url.origin;
|
|
11
|
+
}
|
|
12
|
+
|
|
3
13
|
// sdk/src/hostedUrl.ts
|
|
4
14
|
var HOSTED_PATH_RE = /^\/v\/[0-9a-f]{32}$/;
|
|
5
15
|
var ALLOWED_HTTPS_ORIGINS = /* @__PURE__ */ new Set([
|
|
6
16
|
"https://soliton.id",
|
|
7
17
|
"https://staging.soliton.id"
|
|
8
18
|
]);
|
|
9
|
-
function parseHostedUrl(raw) {
|
|
19
|
+
function parseHostedUrl(raw, verificationOrigin) {
|
|
20
|
+
const configuredOrigin = verificationOrigin === void 0 ? void 0 : trustedOrigin(verificationOrigin);
|
|
10
21
|
let url;
|
|
11
22
|
try {
|
|
12
23
|
url = new URL(raw);
|
|
@@ -14,7 +25,10 @@ function parseHostedUrl(raw) {
|
|
|
14
25
|
throw new Error(`SolitonVerify: "url" is not a valid absolute URL`);
|
|
15
26
|
}
|
|
16
27
|
const isLocalHttp = url.protocol === "http:" && (url.hostname === "localhost" || url.hostname === "127.0.0.1");
|
|
17
|
-
if (
|
|
28
|
+
if (configuredOrigin !== void 0 && url.origin !== configuredOrigin) {
|
|
29
|
+
throw new Error('SolitonVerify: "url" does not match the configured verificationOrigin');
|
|
30
|
+
}
|
|
31
|
+
if (configuredOrigin === void 0 && !ALLOWED_HTTPS_ORIGINS.has(url.origin) && !isLocalHttp) {
|
|
18
32
|
throw new Error(
|
|
19
33
|
'SolitonVerify: "url" must be hosted on https://soliton.id or https://staging.soliton.id (http is allowed for localhost/127.0.0.1 development only)'
|
|
20
34
|
);
|
|
@@ -139,6 +153,7 @@ var EMBED_PROTOCOL = "soliton-v1";
|
|
|
139
153
|
var AUTO_CLOSE_DELAY_MS = 250;
|
|
140
154
|
var HELLO_INTERVAL_MS = 250;
|
|
141
155
|
var HELLO_MAX_PINGS = 40;
|
|
156
|
+
var READY_TIMEOUT_MS = 3e4;
|
|
142
157
|
function open(options) {
|
|
143
158
|
if (!options || typeof options.url !== "string") {
|
|
144
159
|
throw new Error('SolitonVerify: open() requires an options object with a "url"');
|
|
@@ -146,14 +161,16 @@ function open(options) {
|
|
|
146
161
|
if (typeof options.onComplete !== "function") {
|
|
147
162
|
throw new Error("SolitonVerify: open() requires an onComplete callback");
|
|
148
163
|
}
|
|
149
|
-
const { url, hostedOrigin } = parseHostedUrl(options.url);
|
|
164
|
+
const { url, hostedOrigin } = parseHostedUrl(options.url, options.verificationOrigin);
|
|
150
165
|
const src = new URL(url.toString());
|
|
151
166
|
src.searchParams.set("mode", "embed");
|
|
152
167
|
src.searchParams.set("sdk", "1");
|
|
153
168
|
if (options.locale) src.searchParams.set("locale", options.locale);
|
|
154
169
|
let closed = false;
|
|
155
170
|
let terminalDelivered = false;
|
|
171
|
+
let loadTimedOut = false;
|
|
156
172
|
let autoCloseTimer = null;
|
|
173
|
+
let readyTimer = null;
|
|
157
174
|
const overlay = createOverlay(src.toString(), () => close());
|
|
158
175
|
let helloPings = 0;
|
|
159
176
|
const postHello = () => {
|
|
@@ -178,8 +195,13 @@ function open(options) {
|
|
|
178
195
|
clearInterval(helloTimer);
|
|
179
196
|
overlay.iframe.removeEventListener("load", onIframeLoad);
|
|
180
197
|
}
|
|
198
|
+
function stopReadyWait() {
|
|
199
|
+
if (readyTimer !== null) clearTimeout(readyTimer);
|
|
200
|
+
readyTimer = null;
|
|
201
|
+
}
|
|
181
202
|
const onMessage = (event) => {
|
|
182
203
|
var _a;
|
|
204
|
+
if (closed || loadTimedOut || terminalDelivered) return;
|
|
183
205
|
if (event.origin !== hostedOrigin) return;
|
|
184
206
|
if (!overlay.iframe.contentWindow || event.source !== overlay.iframe.contentWindow) {
|
|
185
207
|
return;
|
|
@@ -191,10 +213,12 @@ function open(options) {
|
|
|
191
213
|
stopHello();
|
|
192
214
|
switch (data.type) {
|
|
193
215
|
case "soliton:ready":
|
|
216
|
+
stopReadyWait();
|
|
194
217
|
overlay.markReady();
|
|
195
218
|
break;
|
|
196
219
|
case "soliton:complete": {
|
|
197
220
|
if (terminalDelivered) break;
|
|
221
|
+
stopReadyWait();
|
|
198
222
|
terminalDelivered = true;
|
|
199
223
|
autoCloseTimer = setTimeout(() => close(), AUTO_CLOSE_DELAY_MS);
|
|
200
224
|
options.onComplete({
|
|
@@ -203,6 +227,7 @@ function open(options) {
|
|
|
203
227
|
break;
|
|
204
228
|
}
|
|
205
229
|
case "soliton:error":
|
|
230
|
+
stopReadyWait();
|
|
206
231
|
(_a = options.onError) == null ? void 0 : _a.call(options, {
|
|
207
232
|
code: typeof data.code === "string" ? data.code : "internal",
|
|
208
233
|
message: typeof data.message === "string" ? data.message : ""
|
|
@@ -216,11 +241,25 @@ function open(options) {
|
|
|
216
241
|
}
|
|
217
242
|
};
|
|
218
243
|
window.addEventListener("message", onMessage);
|
|
244
|
+
readyTimer = setTimeout(() => {
|
|
245
|
+
var _a;
|
|
246
|
+
if (readyTimer === null || closed || terminalDelivered) return;
|
|
247
|
+
loadTimedOut = true;
|
|
248
|
+
try {
|
|
249
|
+
(_a = options.onError) == null ? void 0 : _a.call(options, {
|
|
250
|
+
code: "load_timeout",
|
|
251
|
+
message: "Verification did not load. Please try again."
|
|
252
|
+
});
|
|
253
|
+
} finally {
|
|
254
|
+
close();
|
|
255
|
+
}
|
|
256
|
+
}, READY_TIMEOUT_MS);
|
|
219
257
|
function close() {
|
|
220
258
|
var _a;
|
|
221
259
|
if (closed) return;
|
|
222
260
|
closed = true;
|
|
223
261
|
if (autoCloseTimer !== null) clearTimeout(autoCloseTimer);
|
|
262
|
+
stopReadyWait();
|
|
224
263
|
stopHello();
|
|
225
264
|
window.removeEventListener("message", onMessage);
|
|
226
265
|
overlay.destroy();
|
package/dist/server.cjs
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/* Soliton Web SDK v1. Built from sdk/src via `npm run build:sdk`; do not edit by hand. */
|
|
2
|
+
"use strict";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// sdk/src/server.ts
|
|
22
|
+
var server_exports = {};
|
|
23
|
+
__export(server_exports, {
|
|
24
|
+
VerificationStartError: () => VerificationStartError,
|
|
25
|
+
createVerificationClient: () => createVerificationClient
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(server_exports);
|
|
28
|
+
var VerificationStartError = class extends Error {
|
|
29
|
+
constructor(stage, sessionId, status, retryAfter) {
|
|
30
|
+
super(`Verification ${stage} failed${status ? ` (HTTP ${status})` : ""}. No automatic retry was attempted.`);
|
|
31
|
+
this.stage = stage;
|
|
32
|
+
this.sessionId = sessionId;
|
|
33
|
+
this.status = status;
|
|
34
|
+
this.retryAfter = retryAfter;
|
|
35
|
+
this.name = "VerificationStartError";
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
function origin(value) {
|
|
39
|
+
const url = new URL(value);
|
|
40
|
+
const local = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
41
|
+
if (url.protocol !== "https:" && !(local && url.protocol === "http:") || url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
|
|
42
|
+
throw new TypeError("Use an HTTPS origin without credentials, path, query or fragment; HTTP is allowed only on localhost.");
|
|
43
|
+
}
|
|
44
|
+
return url.origin;
|
|
45
|
+
}
|
|
46
|
+
function validSessionId(value) {
|
|
47
|
+
return typeof value === "string" && /^ses_[a-zA-Z0-9]+$/.test(value);
|
|
48
|
+
}
|
|
49
|
+
function record(value) {
|
|
50
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
51
|
+
}
|
|
52
|
+
function createVerificationClient(options) {
|
|
53
|
+
if (typeof window !== "undefined") throw new Error("The verification client is server-only. Keep API keys on your server.");
|
|
54
|
+
const apiOrigin = origin(options.apiOrigin);
|
|
55
|
+
const verificationOrigin = origin(options.verificationOrigin);
|
|
56
|
+
if (!options.apiKey || /[\s\x00-\x1f\x7f]/.test(options.apiKey)) throw new TypeError("A server API key is required.");
|
|
57
|
+
const apiKey = options.apiKey;
|
|
58
|
+
const timeoutMs = options.timeoutMs ?? 2e4;
|
|
59
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > 12e4) throw new TypeError("timeoutMs must be between 1 and 120000.");
|
|
60
|
+
async function post(path, stage, sessionId, body, idempotencyKey) {
|
|
61
|
+
const controller = new AbortController();
|
|
62
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
63
|
+
let status;
|
|
64
|
+
try {
|
|
65
|
+
const response = await fetch(apiOrigin + path, {
|
|
66
|
+
method: "POST",
|
|
67
|
+
redirect: "error",
|
|
68
|
+
// Never forward a secret to an API redirect target.
|
|
69
|
+
signal: controller.signal,
|
|
70
|
+
headers: {
|
|
71
|
+
Authorization: `Bearer ${apiKey}`,
|
|
72
|
+
...body ? { "Content-Type": "application/json" } : {},
|
|
73
|
+
...idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}
|
|
74
|
+
},
|
|
75
|
+
...body ? { body: JSON.stringify(body) } : {}
|
|
76
|
+
});
|
|
77
|
+
status = response.status;
|
|
78
|
+
if (!response.ok) throw new VerificationStartError(stage, sessionId, status, response.headers.get("Retry-After") ?? void 0);
|
|
79
|
+
return record(await response.json());
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (error instanceof VerificationStartError) throw error;
|
|
82
|
+
throw new VerificationStartError(stage, sessionId, status);
|
|
83
|
+
} finally {
|
|
84
|
+
clearTimeout(timer);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async function issueVerificationLink(sessionId) {
|
|
88
|
+
if (!validSessionId(sessionId)) throw new TypeError("A saved ses_ session ID is required.");
|
|
89
|
+
const issued = await post(`/api/v2/sessions/${sessionId}/link`, "link", sessionId);
|
|
90
|
+
if (typeof issued.browser_path !== "string" || !/^\/v\/[a-f0-9]{32}$/.test(issued.browser_path)) {
|
|
91
|
+
throw new VerificationStartError("link", sessionId);
|
|
92
|
+
}
|
|
93
|
+
return { sessionId, url: new URL(issued.browser_path, verificationOrigin).href };
|
|
94
|
+
}
|
|
95
|
+
async function startVerification(input) {
|
|
96
|
+
if (!["standard", "medium", "high"].includes(input.tier)) throw new TypeError("Choose standard, medium or high.");
|
|
97
|
+
if (typeof input.userReference !== "string" || !input.userReference.trim()) throw new TypeError("An authenticated user reference is required.");
|
|
98
|
+
if (typeof input.idempotencyKey !== "string" || !/^[\x21-\x7e]{1,196}$/.test(input.idempotencyKey)) {
|
|
99
|
+
throw new TypeError("Use a stable 1\u2013196 character idempotency key for this attempt.");
|
|
100
|
+
}
|
|
101
|
+
const session = await post("/api/v2/sessions", "create", void 0, {
|
|
102
|
+
client_reference_id: input.userReference,
|
|
103
|
+
checks: [{ type: "presence", configuration: { liveness_tier: input.tier } }]
|
|
104
|
+
}, input.idempotencyKey);
|
|
105
|
+
if (!validSessionId(session.id)) throw new VerificationStartError("create");
|
|
106
|
+
try {
|
|
107
|
+
await input.onSessionCreated?.(session.id);
|
|
108
|
+
} catch {
|
|
109
|
+
throw new VerificationStartError("persist", session.id);
|
|
110
|
+
}
|
|
111
|
+
return issueVerificationLink(session.id);
|
|
112
|
+
}
|
|
113
|
+
return { startVerification, issueVerificationLink };
|
|
114
|
+
}
|
|
115
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
116
|
+
0 && (module.exports = {
|
|
117
|
+
VerificationStartError,
|
|
118
|
+
createVerificationClient
|
|
119
|
+
});
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Server-only v2 convenience API. Never import this module into a browser bundle. */
|
|
2
|
+
export type VerificationTier = "standard" | "medium" | "high";
|
|
3
|
+
export type VerificationStage = "create" | "persist" | "link";
|
|
4
|
+
export declare class VerificationStartError extends Error {
|
|
5
|
+
readonly stage: VerificationStage;
|
|
6
|
+
readonly sessionId?: string | undefined;
|
|
7
|
+
readonly status?: number | undefined;
|
|
8
|
+
readonly retryAfter?: string | undefined;
|
|
9
|
+
constructor(stage: VerificationStage, sessionId?: string | undefined, status?: number | undefined, retryAfter?: string | undefined);
|
|
10
|
+
}
|
|
11
|
+
export interface VerificationClientOptions {
|
|
12
|
+
apiOrigin: string;
|
|
13
|
+
verificationOrigin: string;
|
|
14
|
+
apiKey: string;
|
|
15
|
+
/** Per-request timeout, including response body consumption. Default 20 seconds. */
|
|
16
|
+
timeoutMs?: number;
|
|
17
|
+
}
|
|
18
|
+
export interface StartVerificationOptions {
|
|
19
|
+
userReference: string;
|
|
20
|
+
tier: VerificationTier;
|
|
21
|
+
/** Persist this per logical attempt; reuse only with the identical request. */
|
|
22
|
+
idempotencyKey: string;
|
|
23
|
+
/** Awaited before link issuance. Persist the ID against the authenticated user here. */
|
|
24
|
+
onSessionCreated?: (sessionId: string) => void | Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
export interface StartedVerification {
|
|
27
|
+
sessionId: string;
|
|
28
|
+
url: string;
|
|
29
|
+
}
|
|
30
|
+
/** Node 18+ (native fetch). No retries or in-memory caching; persistence belongs to your backend. */
|
|
31
|
+
export declare function createVerificationClient(options: VerificationClientOptions): {
|
|
32
|
+
startVerification: (input: StartVerificationOptions) => Promise<StartedVerification>;
|
|
33
|
+
issueVerificationLink: (sessionId: string) => Promise<StartedVerification>;
|
|
34
|
+
};
|
package/dist/server.mjs
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/* Soliton Web SDK v1. Built from sdk/src via `npm run build:sdk`; do not edit by hand. */
|
|
2
|
+
|
|
3
|
+
// sdk/src/server.ts
|
|
4
|
+
var VerificationStartError = class extends Error {
|
|
5
|
+
constructor(stage, sessionId, status, retryAfter) {
|
|
6
|
+
super(`Verification ${stage} failed${status ? ` (HTTP ${status})` : ""}. No automatic retry was attempted.`);
|
|
7
|
+
this.stage = stage;
|
|
8
|
+
this.sessionId = sessionId;
|
|
9
|
+
this.status = status;
|
|
10
|
+
this.retryAfter = retryAfter;
|
|
11
|
+
this.name = "VerificationStartError";
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
function origin(value) {
|
|
15
|
+
const url = new URL(value);
|
|
16
|
+
const local = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
17
|
+
if (url.protocol !== "https:" && !(local && url.protocol === "http:") || url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
|
|
18
|
+
throw new TypeError("Use an HTTPS origin without credentials, path, query or fragment; HTTP is allowed only on localhost.");
|
|
19
|
+
}
|
|
20
|
+
return url.origin;
|
|
21
|
+
}
|
|
22
|
+
function validSessionId(value) {
|
|
23
|
+
return typeof value === "string" && /^ses_[a-zA-Z0-9]+$/.test(value);
|
|
24
|
+
}
|
|
25
|
+
function record(value) {
|
|
26
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
27
|
+
}
|
|
28
|
+
function createVerificationClient(options) {
|
|
29
|
+
if (typeof window !== "undefined") throw new Error("The verification client is server-only. Keep API keys on your server.");
|
|
30
|
+
const apiOrigin = origin(options.apiOrigin);
|
|
31
|
+
const verificationOrigin = origin(options.verificationOrigin);
|
|
32
|
+
if (!options.apiKey || /[\s\x00-\x1f\x7f]/.test(options.apiKey)) throw new TypeError("A server API key is required.");
|
|
33
|
+
const apiKey = options.apiKey;
|
|
34
|
+
const timeoutMs = options.timeoutMs ?? 2e4;
|
|
35
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > 12e4) throw new TypeError("timeoutMs must be between 1 and 120000.");
|
|
36
|
+
async function post(path, stage, sessionId, body, idempotencyKey) {
|
|
37
|
+
const controller = new AbortController();
|
|
38
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
39
|
+
let status;
|
|
40
|
+
try {
|
|
41
|
+
const response = await fetch(apiOrigin + path, {
|
|
42
|
+
method: "POST",
|
|
43
|
+
redirect: "error",
|
|
44
|
+
// Never forward a secret to an API redirect target.
|
|
45
|
+
signal: controller.signal,
|
|
46
|
+
headers: {
|
|
47
|
+
Authorization: `Bearer ${apiKey}`,
|
|
48
|
+
...body ? { "Content-Type": "application/json" } : {},
|
|
49
|
+
...idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}
|
|
50
|
+
},
|
|
51
|
+
...body ? { body: JSON.stringify(body) } : {}
|
|
52
|
+
});
|
|
53
|
+
status = response.status;
|
|
54
|
+
if (!response.ok) throw new VerificationStartError(stage, sessionId, status, response.headers.get("Retry-After") ?? void 0);
|
|
55
|
+
return record(await response.json());
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (error instanceof VerificationStartError) throw error;
|
|
58
|
+
throw new VerificationStartError(stage, sessionId, status);
|
|
59
|
+
} finally {
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async function issueVerificationLink(sessionId) {
|
|
64
|
+
if (!validSessionId(sessionId)) throw new TypeError("A saved ses_ session ID is required.");
|
|
65
|
+
const issued = await post(`/api/v2/sessions/${sessionId}/link`, "link", sessionId);
|
|
66
|
+
if (typeof issued.browser_path !== "string" || !/^\/v\/[a-f0-9]{32}$/.test(issued.browser_path)) {
|
|
67
|
+
throw new VerificationStartError("link", sessionId);
|
|
68
|
+
}
|
|
69
|
+
return { sessionId, url: new URL(issued.browser_path, verificationOrigin).href };
|
|
70
|
+
}
|
|
71
|
+
async function startVerification(input) {
|
|
72
|
+
if (!["standard", "medium", "high"].includes(input.tier)) throw new TypeError("Choose standard, medium or high.");
|
|
73
|
+
if (typeof input.userReference !== "string" || !input.userReference.trim()) throw new TypeError("An authenticated user reference is required.");
|
|
74
|
+
if (typeof input.idempotencyKey !== "string" || !/^[\x21-\x7e]{1,196}$/.test(input.idempotencyKey)) {
|
|
75
|
+
throw new TypeError("Use a stable 1\u2013196 character idempotency key for this attempt.");
|
|
76
|
+
}
|
|
77
|
+
const session = await post("/api/v2/sessions", "create", void 0, {
|
|
78
|
+
client_reference_id: input.userReference,
|
|
79
|
+
checks: [{ type: "presence", configuration: { liveness_tier: input.tier } }]
|
|
80
|
+
}, input.idempotencyKey);
|
|
81
|
+
if (!validSessionId(session.id)) throw new VerificationStartError("create");
|
|
82
|
+
try {
|
|
83
|
+
await input.onSessionCreated?.(session.id);
|
|
84
|
+
} catch {
|
|
85
|
+
throw new VerificationStartError("persist", session.id);
|
|
86
|
+
}
|
|
87
|
+
return issueVerificationLink(session.id);
|
|
88
|
+
}
|
|
89
|
+
return { startVerification, issueVerificationLink };
|
|
90
|
+
}
|
|
91
|
+
export {
|
|
92
|
+
VerificationStartError,
|
|
93
|
+
createVerificationClient
|
|
94
|
+
};
|
package/dist/types.d.ts
CHANGED
|
@@ -12,22 +12,38 @@
|
|
|
12
12
|
* add payload fields the wrapper is required to understand.
|
|
13
13
|
*/
|
|
14
14
|
export interface SolitonVerifyOptions {
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Hosted URL from the v1 verification API or v2 session link API. Choose
|
|
17
|
+
* liveness_tier (standard, medium, high) in that server-side request;
|
|
18
|
+
* the URL binds the tier in both sandbox and production. open() does not
|
|
19
|
+
* select or override it, and existing SDK versions support every tier.
|
|
20
|
+
*/
|
|
16
21
|
url: string;
|
|
22
|
+
/**
|
|
23
|
+
* Explicit trusted origin for a customer-hosted deployment. Exact HTTPS origin
|
|
24
|
+
* (HTTP loopback for development); must come from your integration configuration,
|
|
25
|
+
* never from an untrusted link. When set, only this origin is accepted.
|
|
26
|
+
* Omit to retain the existing Soliton cloud origin allowlist.
|
|
27
|
+
*/
|
|
28
|
+
verificationOrigin?: string;
|
|
17
29
|
/**
|
|
18
30
|
* Terminal state reached (pass OR final fail; deliberately not
|
|
19
31
|
* distinguished here). Carries only the verification id: confirm the
|
|
20
|
-
* verdict server-side
|
|
32
|
+
* verdict server-side using the ID saved at creation. For v2, fetch
|
|
33
|
+
* GET /api/v2/sessions/{savedSessionId}; verificationId remains the
|
|
34
|
+
* browser UUID alias for compatibility, not the prefixed ses_ ID.
|
|
21
35
|
*/
|
|
22
36
|
onComplete: (e: {
|
|
23
37
|
verificationId: string;
|
|
24
38
|
}) => void;
|
|
25
|
-
/** Flow-level errors: link_expired, link_invalid, origin_not_allowed,
|
|
39
|
+
/** Flow-level errors: link_expired, link_invalid, origin_not_allowed,
|
|
40
|
+
* storage_unavailable, internal, load_timeout. A load_timeout closes the
|
|
41
|
+
* overlay after this callback and invokes onClose, never onComplete. */
|
|
26
42
|
onError?: (e: {
|
|
27
43
|
code: string;
|
|
28
44
|
message: string;
|
|
29
45
|
}) => void;
|
|
30
|
-
/**
|
|
46
|
+
/** Flow closed before completion, including an initial load timeout. */
|
|
31
47
|
onClose?: () => void;
|
|
32
48
|
/** Reserved; passed through to the hosted page as a query param. */
|
|
33
49
|
locale?: string;
|
package/package.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
|
-
"//": "Publishable manifest for the npm Web SDK. No devDependencies and no own node_modules by design: every build runs from the
|
|
2
|
+
"//": "Publishable manifest for the npm Web SDK. No devDependencies and no own node_modules by design: every build runs from the front-end repository root with its existing toolchain (npm run build:sdk), so the lockfile never grows for packaging. dist/ is rebuilt automatically by the prepack hook and is not committed.",
|
|
3
3
|
"name": "@soliton-id/web-sdk",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.1.1",
|
|
5
5
|
"description": "Soliton identity verification for the browser: opens the hosted verification flow in a full-viewport overlay iframe on your page and reports lifecycle events. Verdicts and portraits are fetched by your backend, never exposed in the browser.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Soliton",
|
|
8
8
|
"homepage": "https://soliton.id/docs/integration/sdk",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
|
-
"url": "git+https://github.com/
|
|
11
|
+
"url": "git+https://github.com/georges-soliton/soliton-front-end.git",
|
|
12
12
|
"directory": "sdk"
|
|
13
13
|
},
|
|
14
14
|
"bugs": {
|
|
15
|
-
"url": "https://github.com/
|
|
15
|
+
"url": "https://github.com/georges-soliton/soliton-front-end/issues"
|
|
16
16
|
},
|
|
17
17
|
"publishConfig": {
|
|
18
18
|
"access": "public",
|
|
@@ -27,6 +27,12 @@
|
|
|
27
27
|
"types": "./dist/index.d.ts",
|
|
28
28
|
"import": "./dist/index.mjs",
|
|
29
29
|
"require": "./dist/index.cjs"
|
|
30
|
+
},
|
|
31
|
+
"./server": {
|
|
32
|
+
"types": "./dist/server.d.ts",
|
|
33
|
+
"browser": null,
|
|
34
|
+
"import": "./dist/server.mjs",
|
|
35
|
+
"require": "./dist/server.cjs"
|
|
30
36
|
}
|
|
31
37
|
},
|
|
32
38
|
"files": [
|