@soliton-id/web-sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,20 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@soliton-id/web-sdk`. The `soliton-v1` postMessage
4
+ protocol is frozen: changes are additive only, so older SDK versions keep
5
+ working against the hosted flow.
6
+
7
+ ## 1.0.0 — 2026-08-01
8
+
9
+ First public release.
10
+
11
+ - `open({ url, onComplete, onError, onClose, locale })` opens a verification
12
+ (minted server-side via `POST /api/v1/verifications`) in a full-viewport
13
+ overlay iframe and returns a handle with `close()`.
14
+ - Ships ESM + CJS + TypeScript declarations; also available from the hosted
15
+ classic and cross-origin ESM bundles under `https://soliton.id/sdk/v1/`.
16
+ - Refuses to frame attacker-controlled hosts: verification URLs are pinned to
17
+ the production/staging Soliton origins, with explicit HTTP localhost
18
+ exceptions for development.
19
+ - Callbacks carry lifecycle only; verdicts are only ever released
20
+ server-to-server against your API key.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Soliton
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,180 @@
1
+ # @soliton-id/web-sdk
2
+
3
+ Soliton identity verification for the browser. `open()` overlays the Soliton
4
+ hosted verification flow on your page in a full-viewport iframe, so your user
5
+ never leaves your site, and reports lifecycle events back to your code. The
6
+ capture engine itself always runs on soliton.id inside the iframe, which is
7
+ why anti-injection and anti-deepfake defenses update continuously without you
8
+ shipping anything.
9
+
10
+ Web SDK guide (this package): <https://soliton.id/docs/integration/sdk>
11
+ Server API guide (create, verdicts, portraits):
12
+ <https://soliton.id/docs/integration/api>
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ npm install @soliton-id/web-sdk
18
+ ```
19
+
20
+ No runtime dependencies, no framework requirements, ~7 KB unminified.
21
+
22
+ Prefer a script tag? The same API ships as a hosted bundle that updates with
23
+ every Soliton deploy:
24
+
25
+ ```html
26
+ <script src="https://soliton.id/sdk/v1/soliton-web-sdk.js"></script>
27
+ <!-- exposes the global SolitonVerify; SolitonVerify.open({...}) -->
28
+ ```
29
+
30
+ A cross-origin-enabled hosted ESM build is also available for direct browser
31
+ imports at
32
+ `https://soliton.id/sdk/v1/soliton-web-sdk.esm.js`. If you use either hosted
33
+ build, allow `https://soliton.id` in your page's `script-src`; an app that
34
+ bundles the npm package does not need that remote script source.
35
+
36
+ ## Quickstart
37
+
38
+ Your backend creates a verification with your secret API key (never do this
39
+ from the browser), hands the returned `url` to your page, and your page opens
40
+ it:
41
+
42
+ ```ts
43
+ import { open } from "@soliton-id/web-sdk";
44
+ // also available: import SolitonVerify from "@soliton-id/web-sdk";
45
+ // SolitonVerify.open({...})
46
+
47
+ // Fetch this from YOUR backend, which called POST /api/v1/verifications.
48
+ const { url } = await fetch("/your-api/start-verification").then(r => r.json());
49
+
50
+ const handle = open({
51
+ url,
52
+ onComplete: ({ verificationId }) => {
53
+ // The flow reached a terminal state (pass OR final fail; deliberately
54
+ // not disclosed here). Treat this purely as a "check now" signal and
55
+ // have YOUR BACKEND fetch the result. See "Getting the result" below.
56
+ },
57
+ onError: ({ code, message }) => {
58
+ // link_expired | link_invalid | origin_not_allowed |
59
+ // storage_unavailable | internal
60
+ },
61
+ onClose: () => {
62
+ // The user dismissed the flow before completion.
63
+ },
64
+ });
65
+
66
+ // handle.close() dismisses the overlay programmatically.
67
+ ```
68
+
69
+ The overlay is always full-viewport: the flow runs a screen-light challenge
70
+ that needs the whole screen, so it cannot be clipped into a widget.
71
+
72
+ ## Callback semantics
73
+
74
+ - `onComplete({ verificationId })`: fired once, when the user reaches a
75
+ terminal state. It carries ONLY the verification id, never a verdict.
76
+ - `onError({ code, message })`: flow-level errors (`link_expired`,
77
+ `link_invalid`, `origin_not_allowed`, `storage_unavailable` when the user's
78
+ browser blocks the session storage the flow needs, `internal`). The overlay
79
+ stays open so the hosted page can show its own error card; call
80
+ `handle.close()` if you want it gone.
81
+ - `onClose()`: the user dismissed the flow (the X button, the hosted Cancel,
82
+ or `handle.close()`) before completion. Not fired after `onComplete`.
83
+
84
+ ## Getting the result (server-to-server only)
85
+
86
+ `onComplete` gives you an id. Everything sensitive is then fetched by YOUR
87
+ BACKEND with your API key:
88
+
89
+ 1. `GET /api/v1/verifications/{id}`: the verdict (`verified`,
90
+ `partial_verification`, plus `portrait_available`).
91
+ 2. `GET /api/v1/verifications/{id}/portrait`: a JPEG portrait of the verified
92
+ user, selected server-side from the capture under neutral lighting.
93
+ Returns 404 until the verification is complete or when no acceptable
94
+ portrait frame existed. Right after completion, `portrait_available` can
95
+ briefly read `false` while extraction finishes; once status is `complete`
96
+ just fetch the portrait and treat a 404 after one short retry as final.
97
+
98
+ Your page and the SDK never see the verdict or the portrait: callbacks and
99
+ postMessage payloads carry only the verification id, and both results are
100
+ released exclusively to your backend against your API key. The hosted screen
101
+ does tell the end user how their own attempt went, with no detail about which
102
+ checks ran or why, but that screen is for the user, not for your integration:
103
+ the browser side (your user, your page, their extensions) is untrusted by
104
+ design, and any machine-readable pass/fail surfaced to your page would hand
105
+ fraudsters an oracle for tuning attacks. Your backend is the only place the
106
+ result exists for you, which is also what makes it trustworthy.
107
+
108
+ ### Bind the verification to your user
109
+
110
+ You already know the `verificationId` when you create the verification: store
111
+ it server-side against your authenticated user or session at that moment.
112
+ When `onComplete` fires, have your backend fetch the id it stored, never the
113
+ id from the callback. An end user controls everything in their browser,
114
+ including that callback value, and could substitute a different id, such as
115
+ one that previously passed. Comparing the callback id against your stored one
116
+ is fine; using it as the lookup key is not. Set `client_reference_id` at
117
+ creation and check it on every status read to make this auditable.
118
+
119
+ ## Frame-origin registration
120
+
121
+ Embedding is off by default and gated on BOTH layers:
122
+
123
+ 1. Your API key must have the exact origins of the pages that will embed the
124
+ flow registered (scheme + host + port, e.g. `https://app.example.com`).
125
+ Ask us to register yours.
126
+ 2. The same origins must be present in the soliton.id deploy-time frame
127
+ ancestors allowlist (we handle this when we register your key).
128
+
129
+ A verification link opened inside an iframe on any other origin refuses to
130
+ start and reports `origin_not_allowed`.
131
+
132
+ During the invite-only pilot, Soliton manually manages both layers. Send your
133
+ Soliton contact every exact production, staging, and local origin before
134
+ testing. Wildcards are not accepted, and adding or changing an origin can
135
+ require a Soliton deployment.
136
+
137
+ ## Customer security headers
138
+
139
+ If your embedding page sends restrictive response headers, merge these
140
+ sources into its existing policies (do not replace the rest of your policy
141
+ with this example):
142
+
143
+ ```text
144
+ Content-Security-Policy: frame-src https://soliton.id https://staging.soliton.id; script-src https://soliton.id https://staging.soliton.id
145
+ Permissions-Policy: camera=(self "https://soliton.id" "https://staging.soliton.id"), microphone=(self "https://soliton.id" "https://staging.soliton.id")
146
+ ```
147
+
148
+ - `frame-src` must include the Soliton origin you use because the capture flow
149
+ runs in a Soliton iframe.
150
+ - `script-src` must include the Soliton origin you use only for the classic
151
+ script or hosted ESM distribution. It is not needed when your application
152
+ bundles the npm package.
153
+ - If your top-level `Permissions-Policy` explicitly restricts camera or
154
+ microphone, delegate both features to the production and/or staging Soliton
155
+ origin you use. The SDK already sets the matching
156
+ `allow="camera; microphone"` attribute on its iframe.
157
+
158
+ For local development, add the exact localhost origin (including its port) to
159
+ the corresponding directives as well.
160
+
161
+ ## Pilot test keys
162
+
163
+ `sk_test_` identifies a pilot credential; it is not an isolated or simulated
164
+ sandbox. Test keys use the same hosted capture, biometric processing, and data
165
+ handling as live keys. The prefix does not create synthetic results or trigger
166
+ automatic deletion. Use only informed, consenting pilot participants, give
167
+ them your applicable privacy notice, and follow your Soliton pilot terms.
168
+
169
+ ## How it works
170
+
171
+ `open()` accepts hosted URLs only from `https://soliton.id` or
172
+ `https://staging.soliton.id` (plus HTTP localhost/127.0.0.1 development URLs),
173
+ requires a `/v/{token}` path, and preserves query parameters. It renders a
174
+ full-viewport overlay with an iframe pointing at that validated origin and
175
+ listens for the hosted page's lifecycle messages. Every message is checked
176
+ three ways (origin, source window, protocol marker) before it reaches your
177
+ callbacks, and unknown message types are ignored, so wrapper versions stay
178
+ compatible as the hosted flow evolves. Redirect-style integration (no SDK,
179
+ we redirect the user back to you) remains fully supported; the same
180
+ verification URL works in both modes.
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Validation of the hosted verification URL handed to SolitonVerify.open().
3
+ *
4
+ * The SDK only ever frames URLs that look exactly like the ones minted by
5
+ * POST /api/v1/verifications: an approved Soliton origin (http strictly for
6
+ * localhost dev) and a bare /v/{32 hex} path. Everything else throws
7
+ * synchronously — a bad URL is an integration bug, not a runtime flow event,
8
+ * so it must fail loudly at the call site instead of rendering an
9
+ * attacker-chosen page in our overlay.
10
+ */
11
+ export interface ParsedHostedUrl {
12
+ /** The validated URL, normalized by the URL parser. */
13
+ url: URL;
14
+ /** Origin the SDK will trust for postMessage events. */
15
+ hostedOrigin: string;
16
+ }
17
+ export declare function parseHostedUrl(raw: string): ParsedHostedUrl;
package/dist/index.cjs ADDED
@@ -0,0 +1,259 @@
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/index.ts
22
+ var index_exports = {};
23
+ __export(index_exports, {
24
+ SolitonVerify: () => SolitonVerify,
25
+ default: () => index_default,
26
+ open: () => open
27
+ });
28
+ module.exports = __toCommonJS(index_exports);
29
+
30
+ // sdk/src/hostedUrl.ts
31
+ var HOSTED_PATH_RE = /^\/v\/[0-9a-f]{32}$/;
32
+ var ALLOWED_HTTPS_ORIGINS = /* @__PURE__ */ new Set([
33
+ "https://soliton.id",
34
+ "https://staging.soliton.id"
35
+ ]);
36
+ function parseHostedUrl(raw) {
37
+ let url;
38
+ try {
39
+ url = new URL(raw);
40
+ } catch (e) {
41
+ throw new Error(`SolitonVerify: "url" is not a valid absolute URL`);
42
+ }
43
+ const isLocalHttp = url.protocol === "http:" && (url.hostname === "localhost" || url.hostname === "127.0.0.1");
44
+ if (!ALLOWED_HTTPS_ORIGINS.has(url.origin) && !isLocalHttp) {
45
+ throw new Error(
46
+ '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
+ );
48
+ }
49
+ if (url.username || url.password) {
50
+ throw new Error('SolitonVerify: "url" must not embed credentials');
51
+ }
52
+ if (!HOSTED_PATH_RE.test(url.pathname)) {
53
+ throw new Error(
54
+ 'SolitonVerify: "url" does not look like a hosted verification URL (/v/{token})'
55
+ );
56
+ }
57
+ return { url, hostedOrigin: url.origin };
58
+ }
59
+
60
+ // sdk/src/overlay.ts
61
+ var MAX_Z_INDEX = "2147483647";
62
+ var openOverlayCount = 0;
63
+ var savedBodyOverflow = "";
64
+ function createOverlay(iframeSrc, onCloseClick) {
65
+ const doc = document;
66
+ const container = doc.createElement("div");
67
+ container.setAttribute("data-soliton-sdk", "overlay");
68
+ container.style.cssText = [
69
+ "position:fixed",
70
+ "inset:0",
71
+ `z-index:${MAX_Z_INDEX}`,
72
+ "background:#0f110b",
73
+ "display:block"
74
+ ].join(";");
75
+ const style = doc.createElement("style");
76
+ style.textContent = "@keyframes soliton-sdk-spin{to{transform:rotate(360deg)}}";
77
+ container.appendChild(style);
78
+ const loading = doc.createElement("div");
79
+ loading.setAttribute("data-soliton-sdk", "loading");
80
+ loading.style.cssText = [
81
+ "position:absolute",
82
+ "inset:0",
83
+ "display:flex",
84
+ "flex-direction:column",
85
+ "align-items:center",
86
+ "justify-content:center",
87
+ "gap:14px",
88
+ "color:rgba(255,255,255,0.75)",
89
+ "font:14px/1.4 -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif"
90
+ ].join(";");
91
+ const spinner = doc.createElement("div");
92
+ spinner.style.cssText = [
93
+ "width:22px",
94
+ "height:22px",
95
+ "border-radius:999px",
96
+ "border:2px solid rgba(255,255,255,0.25)",
97
+ "border-top-color:rgba(255,255,255,0.9)",
98
+ "animation:soliton-sdk-spin 0.8s linear infinite"
99
+ ].join(";");
100
+ const loadingText = doc.createElement("div");
101
+ loadingText.textContent = "Loading secure verification...";
102
+ loading.appendChild(spinner);
103
+ loading.appendChild(loadingText);
104
+ container.appendChild(loading);
105
+ const iframe = doc.createElement("iframe");
106
+ iframe.setAttribute("allow", "camera; microphone; fullscreen; autoplay");
107
+ iframe.setAttribute("title", "Soliton identity verification");
108
+ iframe.style.cssText = [
109
+ "position:absolute",
110
+ "inset:0",
111
+ "width:100%",
112
+ "height:100%",
113
+ "border:0",
114
+ "display:block"
115
+ ].join(";");
116
+ iframe.src = iframeSrc;
117
+ container.appendChild(iframe);
118
+ const closeButton = doc.createElement("button");
119
+ closeButton.type = "button";
120
+ closeButton.setAttribute("aria-label", "Close verification");
121
+ closeButton.textContent = "\xD7";
122
+ closeButton.style.cssText = [
123
+ "position:absolute",
124
+ "top:14px",
125
+ "right:14px",
126
+ "width:36px",
127
+ "height:36px",
128
+ "border-radius:999px",
129
+ "border:1px solid rgba(255,255,255,0.3)",
130
+ "background:rgba(0,0,0,0.45)",
131
+ "color:rgba(255,255,255,0.85)",
132
+ "font:20px/1 -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif",
133
+ "cursor:pointer",
134
+ "padding:0"
135
+ ].join(";");
136
+ closeButton.addEventListener("click", onCloseClick);
137
+ container.appendChild(closeButton);
138
+ if (openOverlayCount === 0) {
139
+ savedBodyOverflow = doc.body.style.overflow;
140
+ }
141
+ openOverlayCount += 1;
142
+ doc.body.style.overflow = "hidden";
143
+ doc.body.appendChild(container);
144
+ let destroyed = false;
145
+ return {
146
+ iframe,
147
+ markReady() {
148
+ loading.style.display = "none";
149
+ },
150
+ destroy() {
151
+ if (destroyed) return;
152
+ destroyed = true;
153
+ openOverlayCount -= 1;
154
+ if (openOverlayCount === 0) {
155
+ doc.body.style.overflow = savedBodyOverflow;
156
+ }
157
+ if (container.parentNode) container.parentNode.removeChild(container);
158
+ }
159
+ };
160
+ }
161
+
162
+ // sdk/src/types.ts
163
+ var EMBED_PROTOCOL = "soliton-v1";
164
+
165
+ // sdk/src/index.ts
166
+ var AUTO_CLOSE_DELAY_MS = 250;
167
+ var HELLO_INTERVAL_MS = 250;
168
+ var HELLO_MAX_PINGS = 40;
169
+ function open(options) {
170
+ if (!options || typeof options.url !== "string") {
171
+ throw new Error('SolitonVerify: open() requires an options object with a "url"');
172
+ }
173
+ if (typeof options.onComplete !== "function") {
174
+ throw new Error("SolitonVerify: open() requires an onComplete callback");
175
+ }
176
+ const { url, hostedOrigin } = parseHostedUrl(options.url);
177
+ const src = new URL(url.toString());
178
+ src.searchParams.set("mode", "embed");
179
+ src.searchParams.set("sdk", "1");
180
+ if (options.locale) src.searchParams.set("locale", options.locale);
181
+ let closed = false;
182
+ let terminalDelivered = false;
183
+ let autoCloseTimer = null;
184
+ const overlay = createOverlay(src.toString(), () => close());
185
+ let helloPings = 0;
186
+ const postHello = () => {
187
+ var _a;
188
+ helloPings += 1;
189
+ if (helloPings > HELLO_MAX_PINGS) {
190
+ stopHello();
191
+ return;
192
+ }
193
+ try {
194
+ (_a = overlay.iframe.contentWindow) == null ? void 0 : _a.postMessage(
195
+ { protocol: EMBED_PROTOCOL, type: "soliton:hello" },
196
+ hostedOrigin
197
+ );
198
+ } catch (e) {
199
+ }
200
+ };
201
+ const helloTimer = setInterval(postHello, HELLO_INTERVAL_MS);
202
+ const onIframeLoad = () => postHello();
203
+ overlay.iframe.addEventListener("load", onIframeLoad);
204
+ function stopHello() {
205
+ clearInterval(helloTimer);
206
+ overlay.iframe.removeEventListener("load", onIframeLoad);
207
+ }
208
+ const onMessage = (event) => {
209
+ var _a;
210
+ if (event.origin !== hostedOrigin) return;
211
+ if (!overlay.iframe.contentWindow || event.source !== overlay.iframe.contentWindow) {
212
+ return;
213
+ }
214
+ const data = event.data;
215
+ if (!data || typeof data !== "object" || data.protocol !== EMBED_PROTOCOL) {
216
+ return;
217
+ }
218
+ stopHello();
219
+ switch (data.type) {
220
+ case "soliton:ready":
221
+ overlay.markReady();
222
+ break;
223
+ case "soliton:complete": {
224
+ if (terminalDelivered) break;
225
+ terminalDelivered = true;
226
+ autoCloseTimer = setTimeout(() => close(), AUTO_CLOSE_DELAY_MS);
227
+ options.onComplete({
228
+ verificationId: typeof data.verificationId === "string" ? data.verificationId : ""
229
+ });
230
+ break;
231
+ }
232
+ case "soliton:error":
233
+ (_a = options.onError) == null ? void 0 : _a.call(options, {
234
+ code: typeof data.code === "string" ? data.code : "internal",
235
+ message: typeof data.message === "string" ? data.message : ""
236
+ });
237
+ break;
238
+ case "soliton:close":
239
+ close();
240
+ break;
241
+ default:
242
+ break;
243
+ }
244
+ };
245
+ window.addEventListener("message", onMessage);
246
+ function close() {
247
+ var _a;
248
+ if (closed) return;
249
+ closed = true;
250
+ if (autoCloseTimer !== null) clearTimeout(autoCloseTimer);
251
+ stopHello();
252
+ window.removeEventListener("message", onMessage);
253
+ overlay.destroy();
254
+ if (!terminalDelivered) (_a = options.onClose) == null ? void 0 : _a.call(options);
255
+ }
256
+ return { close };
257
+ }
258
+ var SolitonVerify = { open };
259
+ var index_default = SolitonVerify;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Soliton Web SDK v1: embedded verification widget (Stripe/Sumsub wrapper
3
+ * pattern). open() frames the EXISTING hosted verification page in a
4
+ * full-viewport overlay on the customer's site and relays its lifecycle via
5
+ * callbacks; the capture engine itself always runs on soliton.id inside the
6
+ * iframe, so anti-injection defenses update with every deploy and the
7
+ * verdict never touches the customer page (only the verification id does).
8
+ *
9
+ * Built artifacts (npm run build:sdk):
10
+ * public/sdk/v1/soliton-web-sdk.js IIFE, global `SolitonVerify`
11
+ * public/sdk/v1/soliton-web-sdk.esm.js ESM, `import { open } ...`
12
+ * sdk/dist/index.mjs + index.cjs npm package `@soliton-id/web-sdk`
13
+ */
14
+ import type { SolitonVerifyHandle, SolitonVerifyOptions } from "./types";
15
+ export type { SolitonVerifyHandle, SolitonVerifyOptions };
16
+ export declare function open(options: SolitonVerifyOptions): SolitonVerifyHandle;
17
+ export declare const SolitonVerify: {
18
+ open: typeof open;
19
+ };
20
+ export default SolitonVerify;
package/dist/index.mjs ADDED
@@ -0,0 +1,237 @@
1
+ /* Soliton Web SDK v1. Built from sdk/src via `npm run build:sdk`; do not edit by hand. */
2
+
3
+ // sdk/src/hostedUrl.ts
4
+ var HOSTED_PATH_RE = /^\/v\/[0-9a-f]{32}$/;
5
+ var ALLOWED_HTTPS_ORIGINS = /* @__PURE__ */ new Set([
6
+ "https://soliton.id",
7
+ "https://staging.soliton.id"
8
+ ]);
9
+ function parseHostedUrl(raw) {
10
+ let url;
11
+ try {
12
+ url = new URL(raw);
13
+ } catch (e) {
14
+ throw new Error(`SolitonVerify: "url" is not a valid absolute URL`);
15
+ }
16
+ const isLocalHttp = url.protocol === "http:" && (url.hostname === "localhost" || url.hostname === "127.0.0.1");
17
+ if (!ALLOWED_HTTPS_ORIGINS.has(url.origin) && !isLocalHttp) {
18
+ throw new Error(
19
+ '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
+ );
21
+ }
22
+ if (url.username || url.password) {
23
+ throw new Error('SolitonVerify: "url" must not embed credentials');
24
+ }
25
+ if (!HOSTED_PATH_RE.test(url.pathname)) {
26
+ throw new Error(
27
+ 'SolitonVerify: "url" does not look like a hosted verification URL (/v/{token})'
28
+ );
29
+ }
30
+ return { url, hostedOrigin: url.origin };
31
+ }
32
+
33
+ // sdk/src/overlay.ts
34
+ var MAX_Z_INDEX = "2147483647";
35
+ var openOverlayCount = 0;
36
+ var savedBodyOverflow = "";
37
+ function createOverlay(iframeSrc, onCloseClick) {
38
+ const doc = document;
39
+ const container = doc.createElement("div");
40
+ container.setAttribute("data-soliton-sdk", "overlay");
41
+ container.style.cssText = [
42
+ "position:fixed",
43
+ "inset:0",
44
+ `z-index:${MAX_Z_INDEX}`,
45
+ "background:#0f110b",
46
+ "display:block"
47
+ ].join(";");
48
+ const style = doc.createElement("style");
49
+ style.textContent = "@keyframes soliton-sdk-spin{to{transform:rotate(360deg)}}";
50
+ container.appendChild(style);
51
+ const loading = doc.createElement("div");
52
+ loading.setAttribute("data-soliton-sdk", "loading");
53
+ loading.style.cssText = [
54
+ "position:absolute",
55
+ "inset:0",
56
+ "display:flex",
57
+ "flex-direction:column",
58
+ "align-items:center",
59
+ "justify-content:center",
60
+ "gap:14px",
61
+ "color:rgba(255,255,255,0.75)",
62
+ "font:14px/1.4 -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif"
63
+ ].join(";");
64
+ const spinner = doc.createElement("div");
65
+ spinner.style.cssText = [
66
+ "width:22px",
67
+ "height:22px",
68
+ "border-radius:999px",
69
+ "border:2px solid rgba(255,255,255,0.25)",
70
+ "border-top-color:rgba(255,255,255,0.9)",
71
+ "animation:soliton-sdk-spin 0.8s linear infinite"
72
+ ].join(";");
73
+ const loadingText = doc.createElement("div");
74
+ loadingText.textContent = "Loading secure verification...";
75
+ loading.appendChild(spinner);
76
+ loading.appendChild(loadingText);
77
+ container.appendChild(loading);
78
+ const iframe = doc.createElement("iframe");
79
+ iframe.setAttribute("allow", "camera; microphone; fullscreen; autoplay");
80
+ iframe.setAttribute("title", "Soliton identity verification");
81
+ iframe.style.cssText = [
82
+ "position:absolute",
83
+ "inset:0",
84
+ "width:100%",
85
+ "height:100%",
86
+ "border:0",
87
+ "display:block"
88
+ ].join(";");
89
+ iframe.src = iframeSrc;
90
+ container.appendChild(iframe);
91
+ const closeButton = doc.createElement("button");
92
+ closeButton.type = "button";
93
+ closeButton.setAttribute("aria-label", "Close verification");
94
+ closeButton.textContent = "\xD7";
95
+ closeButton.style.cssText = [
96
+ "position:absolute",
97
+ "top:14px",
98
+ "right:14px",
99
+ "width:36px",
100
+ "height:36px",
101
+ "border-radius:999px",
102
+ "border:1px solid rgba(255,255,255,0.3)",
103
+ "background:rgba(0,0,0,0.45)",
104
+ "color:rgba(255,255,255,0.85)",
105
+ "font:20px/1 -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif",
106
+ "cursor:pointer",
107
+ "padding:0"
108
+ ].join(";");
109
+ closeButton.addEventListener("click", onCloseClick);
110
+ container.appendChild(closeButton);
111
+ if (openOverlayCount === 0) {
112
+ savedBodyOverflow = doc.body.style.overflow;
113
+ }
114
+ openOverlayCount += 1;
115
+ doc.body.style.overflow = "hidden";
116
+ doc.body.appendChild(container);
117
+ let destroyed = false;
118
+ return {
119
+ iframe,
120
+ markReady() {
121
+ loading.style.display = "none";
122
+ },
123
+ destroy() {
124
+ if (destroyed) return;
125
+ destroyed = true;
126
+ openOverlayCount -= 1;
127
+ if (openOverlayCount === 0) {
128
+ doc.body.style.overflow = savedBodyOverflow;
129
+ }
130
+ if (container.parentNode) container.parentNode.removeChild(container);
131
+ }
132
+ };
133
+ }
134
+
135
+ // sdk/src/types.ts
136
+ var EMBED_PROTOCOL = "soliton-v1";
137
+
138
+ // sdk/src/index.ts
139
+ var AUTO_CLOSE_DELAY_MS = 250;
140
+ var HELLO_INTERVAL_MS = 250;
141
+ var HELLO_MAX_PINGS = 40;
142
+ function open(options) {
143
+ if (!options || typeof options.url !== "string") {
144
+ throw new Error('SolitonVerify: open() requires an options object with a "url"');
145
+ }
146
+ if (typeof options.onComplete !== "function") {
147
+ throw new Error("SolitonVerify: open() requires an onComplete callback");
148
+ }
149
+ const { url, hostedOrigin } = parseHostedUrl(options.url);
150
+ const src = new URL(url.toString());
151
+ src.searchParams.set("mode", "embed");
152
+ src.searchParams.set("sdk", "1");
153
+ if (options.locale) src.searchParams.set("locale", options.locale);
154
+ let closed = false;
155
+ let terminalDelivered = false;
156
+ let autoCloseTimer = null;
157
+ const overlay = createOverlay(src.toString(), () => close());
158
+ let helloPings = 0;
159
+ const postHello = () => {
160
+ var _a;
161
+ helloPings += 1;
162
+ if (helloPings > HELLO_MAX_PINGS) {
163
+ stopHello();
164
+ return;
165
+ }
166
+ try {
167
+ (_a = overlay.iframe.contentWindow) == null ? void 0 : _a.postMessage(
168
+ { protocol: EMBED_PROTOCOL, type: "soliton:hello" },
169
+ hostedOrigin
170
+ );
171
+ } catch (e) {
172
+ }
173
+ };
174
+ const helloTimer = setInterval(postHello, HELLO_INTERVAL_MS);
175
+ const onIframeLoad = () => postHello();
176
+ overlay.iframe.addEventListener("load", onIframeLoad);
177
+ function stopHello() {
178
+ clearInterval(helloTimer);
179
+ overlay.iframe.removeEventListener("load", onIframeLoad);
180
+ }
181
+ const onMessage = (event) => {
182
+ var _a;
183
+ if (event.origin !== hostedOrigin) return;
184
+ if (!overlay.iframe.contentWindow || event.source !== overlay.iframe.contentWindow) {
185
+ return;
186
+ }
187
+ const data = event.data;
188
+ if (!data || typeof data !== "object" || data.protocol !== EMBED_PROTOCOL) {
189
+ return;
190
+ }
191
+ stopHello();
192
+ switch (data.type) {
193
+ case "soliton:ready":
194
+ overlay.markReady();
195
+ break;
196
+ case "soliton:complete": {
197
+ if (terminalDelivered) break;
198
+ terminalDelivered = true;
199
+ autoCloseTimer = setTimeout(() => close(), AUTO_CLOSE_DELAY_MS);
200
+ options.onComplete({
201
+ verificationId: typeof data.verificationId === "string" ? data.verificationId : ""
202
+ });
203
+ break;
204
+ }
205
+ case "soliton:error":
206
+ (_a = options.onError) == null ? void 0 : _a.call(options, {
207
+ code: typeof data.code === "string" ? data.code : "internal",
208
+ message: typeof data.message === "string" ? data.message : ""
209
+ });
210
+ break;
211
+ case "soliton:close":
212
+ close();
213
+ break;
214
+ default:
215
+ break;
216
+ }
217
+ };
218
+ window.addEventListener("message", onMessage);
219
+ function close() {
220
+ var _a;
221
+ if (closed) return;
222
+ closed = true;
223
+ if (autoCloseTimer !== null) clearTimeout(autoCloseTimer);
224
+ stopHello();
225
+ window.removeEventListener("message", onMessage);
226
+ overlay.destroy();
227
+ if (!terminalDelivered) (_a = options.onClose) == null ? void 0 : _a.call(options);
228
+ }
229
+ return { close };
230
+ }
231
+ var SolitonVerify = { open };
232
+ var index_default = SolitonVerify;
233
+ export {
234
+ SolitonVerify,
235
+ index_default as default,
236
+ open
237
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Full-viewport overlay hosting the verification iframe.
3
+ *
4
+ * The overlay is deliberately full-screen and opaque: the capture flow runs
5
+ * a screen-light challenge, and a small inline widget would both break that
6
+ * physics and shrink the camera view. Everything is plain DOM with inline
7
+ * styles so the SDK needs no stylesheet and cannot clash with host CSS
8
+ * (spinner keyframes ride in a scoped <style> tag inside the overlay).
9
+ */
10
+ export interface Overlay {
11
+ iframe: HTMLIFrameElement;
12
+ /** Hide the loading state once the hosted page reports soliton:ready. */
13
+ markReady(): void;
14
+ /** Remove the overlay from the DOM and restore body scroll. Idempotent. */
15
+ destroy(): void;
16
+ }
17
+ export declare function createOverlay(iframeSrc: string, onCloseClick: () => void): Overlay;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Public types for the Soliton Web SDK, plus the child -> parent postMessage
3
+ * protocol shared with the hosted page (app/utils/embedMessaging.ts is the
4
+ * child-side counterpart; keep the two in sync).
5
+ *
6
+ * COMPATIBILITY CONTRACT (frozen since the SDK ships on npm): wrapper
7
+ * versions live inside customer bundles for months, so the soliton-v1
8
+ * message shapes below can never change or be removed. The hosted page MAY
9
+ * add new message types in the future; old wrappers silently ignore unknown
10
+ * types (the switch in index.ts must keep its no-op default), so additions
11
+ * are safe. Never rename a type, never remove or retype a field, and never
12
+ * add payload fields the wrapper is required to understand.
13
+ */
14
+ export interface SolitonVerifyOptions {
15
+ /** Hosted verification URL from POST /api/v1/verifications. */
16
+ url: string;
17
+ /**
18
+ * Terminal state reached (pass OR final fail; deliberately not
19
+ * distinguished here). Carries only the verification id: confirm the
20
+ * verdict server-side via GET /api/v1/verifications/{id}.
21
+ */
22
+ onComplete: (e: {
23
+ verificationId: string;
24
+ }) => void;
25
+ /** Flow-level errors: link_expired, link_invalid, origin_not_allowed, storage_unavailable, internal. */
26
+ onError?: (e: {
27
+ code: string;
28
+ message: string;
29
+ }) => void;
30
+ /** User dismissed the flow before completion. */
31
+ onClose?: () => void;
32
+ /** Reserved; passed through to the hosted page as a query param. */
33
+ locale?: string;
34
+ }
35
+ export interface SolitonVerifyHandle {
36
+ close(): void;
37
+ }
38
+ /** Every hosted-page message is tagged with this protocol marker. */
39
+ export declare const EMBED_PROTOCOL = "soliton-v1";
40
+ export type EmbedMessage = {
41
+ protocol: typeof EMBED_PROTOCOL;
42
+ type: "soliton:ready";
43
+ } | {
44
+ protocol: typeof EMBED_PROTOCOL;
45
+ type: "soliton:complete";
46
+ verificationId: string;
47
+ } | {
48
+ protocol: typeof EMBED_PROTOCOL;
49
+ type: "soliton:error";
50
+ code: string;
51
+ message: string;
52
+ } | {
53
+ protocol: typeof EMBED_PROTOCOL;
54
+ type: "soliton:close";
55
+ };
56
+ /**
57
+ * Parent -> child (the only message in that direction): the wrapper pings
58
+ * this at the hosted iframe so the hosted page can read the embedding origin
59
+ * from MessageEvent.origin, which the browser sets and nothing can spoof.
60
+ * Firefox has no location.ancestorOrigins, and Referrer-Policy: no-referrer
61
+ * on the customer page blanks the referrer fallback; without this ping such
62
+ * embeds would always fail the frame-origin gate. Added before the first npm
63
+ * publish, so it is part of the frozen soliton-v1 surface. Deliberately
64
+ * carries nothing beyond the protocol marker.
65
+ */
66
+ export type ParentHelloMessage = {
67
+ protocol: typeof EMBED_PROTOCOL;
68
+ type: "soliton:hello";
69
+ };
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "//": "Publishable manifest for the npm Web SDK. No devDependencies and no own node_modules by design: every build runs from the next-app 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
+ "name": "@soliton-id/web-sdk",
4
+ "version": "1.0.0",
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
+ "license": "MIT",
7
+ "author": "Soliton",
8
+ "homepage": "https://soliton.id/docs/integration/sdk",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/georgesabr/next-app.git",
12
+ "directory": "sdk"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/georgesabr/next-app/issues"
16
+ },
17
+ "publishConfig": {
18
+ "access": "public",
19
+ "registry": "https://registry.npmjs.org/"
20
+ },
21
+ "sideEffects": false,
22
+ "main": "./dist/index.cjs",
23
+ "module": "./dist/index.mjs",
24
+ "types": "./dist/index.d.ts",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "import": "./dist/index.mjs",
29
+ "require": "./dist/index.cjs"
30
+ }
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "README.md",
35
+ "LICENSE",
36
+ "CHANGELOG.md"
37
+ ],
38
+ "scripts": {
39
+ "prepack": "node build.mjs"
40
+ },
41
+ "engines": {
42
+ "node": ">=16"
43
+ },
44
+ "keywords": [
45
+ "identity",
46
+ "verification",
47
+ "liveness",
48
+ "kyc",
49
+ "soliton"
50
+ ]
51
+ }