@splitin/verification-web 0.1.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE +32 -0
- package/README.md +18 -0
- package/dist/index.cjs +289 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +108 -0
- package/dist/index.d.ts +108 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +271 -0
- package/dist/index.js.map +1 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SplitInTech
|
|
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/NOTICE
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
Verification Adapter SDK
|
|
2
|
+
Copyright (c) 2026 SplitInTech
|
|
3
|
+
|
|
4
|
+
This product is licensed under the MIT License. See LICENSE.
|
|
5
|
+
|
|
6
|
+
Third-party notices
|
|
7
|
+
===================
|
|
8
|
+
|
|
9
|
+
This project redistributes no vendor identity SDKs in its core or server
|
|
10
|
+
packages. Optional peer dependencies used by browser plugins:
|
|
11
|
+
|
|
12
|
+
- `@stripe/stripe-js` — Stripe, Inc. (MIT). Used only by the Stripe Identity
|
|
13
|
+
browser plugin via dynamic import. Stripe is a trademark of Stripe, Inc.
|
|
14
|
+
- `persona` — Persona Identities, Inc. Used only by the Persona browser plugin
|
|
15
|
+
via dynamic import.
|
|
16
|
+
- `react-plaid-link` — Plaid Inc. (MIT). Used only by the Plaid Identity
|
|
17
|
+
Verification browser plugin via dynamic import. Plaid is a trademark of
|
|
18
|
+
Plaid Inc.
|
|
19
|
+
|
|
20
|
+
Server adapters speak HTTPS to provider APIs with an injected Fetch
|
|
21
|
+
implementation. They do not bundle Stripe, Persona, or Plaid Node SDKs.
|
|
22
|
+
|
|
23
|
+
Runtime and tooling (declared in package.json; not vendored):
|
|
24
|
+
|
|
25
|
+
- `ajv` — MIT
|
|
26
|
+
- `jose` — MIT (Plaid webhook JWT verification)
|
|
27
|
+
- `pg` — MIT (optional PostgreSQL executor)
|
|
28
|
+
- TypeScript, tsup, vitest, Changesets — Apache-2.0 / MIT as published by
|
|
29
|
+
their authors
|
|
30
|
+
|
|
31
|
+
This project provides engineering primitives. It is not a legal, KYC, KYB, or
|
|
32
|
+
regulatory compliance certification.
|
package/README.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# `@splitin/verification-web`
|
|
2
|
+
|
|
3
|
+
Framework-neutral browser controller for verification V1 launch and status
|
|
4
|
+
envelopes. Browser completion only requests an authoritative status refresh.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
import { createVerificationWebController } from '@splitin/verification-web';
|
|
8
|
+
|
|
9
|
+
const controller = createVerificationWebController({
|
|
10
|
+
plugins: {
|
|
11
|
+
test_embedded: () => import('./my-plugin.js'),
|
|
12
|
+
},
|
|
13
|
+
refreshStatus: (attemptId) => client.getSession(attemptId),
|
|
14
|
+
});
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Transient launch secrets stay in module memory. They are never written to
|
|
18
|
+
`localStorage` or `sessionStorage`.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/environment.ts
|
|
4
|
+
function isBrowserOnline() {
|
|
5
|
+
if (typeof navigator === "undefined") return true;
|
|
6
|
+
return navigator.onLine !== false;
|
|
7
|
+
}
|
|
8
|
+
function isInstalledPwa() {
|
|
9
|
+
if (typeof window === "undefined") return false;
|
|
10
|
+
const standalone = window.matchMedia?.("(display-mode: standalone)")?.matches;
|
|
11
|
+
const iosStandalone = "standalone" in navigator && Boolean(navigator.standalone);
|
|
12
|
+
return Boolean(standalone || iosStandalone);
|
|
13
|
+
}
|
|
14
|
+
function prefersReducedMotion() {
|
|
15
|
+
if (typeof window === "undefined") return false;
|
|
16
|
+
return Boolean(window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches);
|
|
17
|
+
}
|
|
18
|
+
function clientSurface(width) {
|
|
19
|
+
if (isInstalledPwa()) return "installed-pwa";
|
|
20
|
+
if (typeof window === "undefined") return "desktop";
|
|
21
|
+
const viewport = width ?? window.innerWidth;
|
|
22
|
+
if (viewport <= 640 || Boolean(window.matchMedia?.("(max-width: 640px)")?.matches)) return "mobile-web";
|
|
23
|
+
return "desktop";
|
|
24
|
+
}
|
|
25
|
+
function subscribeOnlineStatus(listener) {
|
|
26
|
+
if (typeof window === "undefined") return () => void 0;
|
|
27
|
+
const onOnline = () => listener(true);
|
|
28
|
+
const onOffline = () => listener(false);
|
|
29
|
+
window.addEventListener("online", onOnline);
|
|
30
|
+
window.addEventListener("offline", onOffline);
|
|
31
|
+
return () => {
|
|
32
|
+
window.removeEventListener("online", onOnline);
|
|
33
|
+
window.removeEventListener("offline", onOffline);
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
async function createQrDataUrl(value, renderer) {
|
|
37
|
+
if (!renderer) return null;
|
|
38
|
+
try {
|
|
39
|
+
return await renderer(value);
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/plugins.ts
|
|
46
|
+
var loaded = /* @__PURE__ */ new Map();
|
|
47
|
+
async function loadBrowserPlugin(launcherKey, registry) {
|
|
48
|
+
const cached = loaded.get(launcherKey);
|
|
49
|
+
if (cached) return cached;
|
|
50
|
+
const entry = registry[launcherKey];
|
|
51
|
+
if (!entry) return null;
|
|
52
|
+
const plugin = typeof entry === "function" ? unwrap(await entry()) : entry;
|
|
53
|
+
loaded.set(launcherKey, plugin);
|
|
54
|
+
return plugin;
|
|
55
|
+
}
|
|
56
|
+
function clearPluginCache() {
|
|
57
|
+
loaded.clear();
|
|
58
|
+
}
|
|
59
|
+
function unwrap(value) {
|
|
60
|
+
return "present" in value ? value : value.default;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/secrets.ts
|
|
64
|
+
var secrets = /* @__PURE__ */ new Map();
|
|
65
|
+
function rememberTransientSecret(attemptId, secret, expiresAt) {
|
|
66
|
+
secrets.set(attemptId, {
|
|
67
|
+
secret,
|
|
68
|
+
expiresAt: expiresAt ? Date.parse(expiresAt) : Date.now() + 5 * 6e4
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function peekTransientSecret(attemptId, now = Date.now()) {
|
|
72
|
+
const row = secrets.get(attemptId);
|
|
73
|
+
if (!row) return void 0;
|
|
74
|
+
if (row.expiresAt <= now) {
|
|
75
|
+
secrets.delete(attemptId);
|
|
76
|
+
return void 0;
|
|
77
|
+
}
|
|
78
|
+
return row.secret;
|
|
79
|
+
}
|
|
80
|
+
function takeTransientSecret(attemptId, now = Date.now()) {
|
|
81
|
+
const secret = peekTransientSecret(attemptId, now);
|
|
82
|
+
secrets.delete(attemptId);
|
|
83
|
+
return secret;
|
|
84
|
+
}
|
|
85
|
+
function forgetTransientSecret(attemptId) {
|
|
86
|
+
secrets.delete(attemptId);
|
|
87
|
+
}
|
|
88
|
+
function forgetAllTransientSecrets() {
|
|
89
|
+
secrets.clear();
|
|
90
|
+
}
|
|
91
|
+
function rememberLaunchSecrets(attemptId, launch) {
|
|
92
|
+
if (!launch?.transientSecret) return;
|
|
93
|
+
rememberTransientSecret(attemptId, launch.transientSecret, launch.transientSecretExpiresAt);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/types.ts
|
|
97
|
+
var STATUS_CONTRACT_VERSION = "1.0.0";
|
|
98
|
+
var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
|
|
99
|
+
"verified",
|
|
100
|
+
"declined",
|
|
101
|
+
"failed",
|
|
102
|
+
"expired",
|
|
103
|
+
"canceled",
|
|
104
|
+
"redacted"
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
// src/controller.ts
|
|
108
|
+
function createVerificationWebController(options) {
|
|
109
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
110
|
+
async function refresh(attemptId) {
|
|
111
|
+
const envelope = await options.refreshStatus(attemptId);
|
|
112
|
+
rememberLaunchSecrets(attemptId, envelope.launch);
|
|
113
|
+
return envelope;
|
|
114
|
+
}
|
|
115
|
+
async function present(input) {
|
|
116
|
+
let envelope = input.envelope;
|
|
117
|
+
rememberLaunchSecrets(envelope.attemptId, envelope.launch);
|
|
118
|
+
const abort = new AbortController();
|
|
119
|
+
let handle = null;
|
|
120
|
+
let continuationUrl = continuationLink(envelope);
|
|
121
|
+
const unsubscribe = subscribeOnlineStatus((online) => {
|
|
122
|
+
if (!online) input.onMessage?.("You are offline. Your place is saved. Reconnect to resume verification.");
|
|
123
|
+
else void refreshAndEmit();
|
|
124
|
+
});
|
|
125
|
+
const emit = (next, message) => {
|
|
126
|
+
envelope = next;
|
|
127
|
+
input.onEnvelopeChange?.(next);
|
|
128
|
+
if (message) input.onMessage?.(message);
|
|
129
|
+
};
|
|
130
|
+
const refreshAndEmit = async () => {
|
|
131
|
+
const next = await refresh(envelope.attemptId);
|
|
132
|
+
const verified = next.status === "verified";
|
|
133
|
+
emit(next, verified ? "Verified. Returning to your saved action." : "Your status is not approved yet. The protected action stays saved.");
|
|
134
|
+
return next;
|
|
135
|
+
};
|
|
136
|
+
const presentPlugin = async () => {
|
|
137
|
+
handle?.destroy();
|
|
138
|
+
handle = null;
|
|
139
|
+
const launch = envelope.launch;
|
|
140
|
+
if (!launch || envelope.presentation === "none" || TERMINAL_STATUSES.has(envelope.status)) return;
|
|
141
|
+
if (envelope.status === "processing" || envelope.status === "manual_review_required") return;
|
|
142
|
+
if (!isBrowserOnline()) return;
|
|
143
|
+
if (launch.transientSecretExpiresAt && Date.parse(launch.transientSecretExpiresAt) <= now().getTime()) {
|
|
144
|
+
forgetTransientSecret(envelope.attemptId);
|
|
145
|
+
input.onMessage?.("The secure launch credential expired. Checking status.");
|
|
146
|
+
await refreshAndEmit();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (envelope.presentation === "hosted" && launch.hostedUrl) {
|
|
150
|
+
handle = presentHosted(input.container, launch.hostedUrl, () => {
|
|
151
|
+
void refreshAndEmit();
|
|
152
|
+
});
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (envelope.presentation === "qr" && continuationUrl) {
|
|
156
|
+
handle = await presentQr(input.container, continuationUrl, options.renderQr);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const plugin = await loadBrowserPlugin(launch.launcherKey, options.plugins);
|
|
160
|
+
if (!plugin) {
|
|
161
|
+
if (launch.hostedUrl) {
|
|
162
|
+
handle = presentHosted(input.container, launch.hostedUrl, () => {
|
|
163
|
+
void refreshAndEmit();
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
handle = await plugin.present({
|
|
169
|
+
container: input.container,
|
|
170
|
+
launch: {
|
|
171
|
+
...launch,
|
|
172
|
+
transientSecret: peekTransientSecret(envelope.attemptId) ?? launch.transientSecret
|
|
173
|
+
},
|
|
174
|
+
signal: abort.signal,
|
|
175
|
+
onComplete: () => {
|
|
176
|
+
input.onMessage?.("Verification was submitted. Checking the server-side result now.");
|
|
177
|
+
void refreshAndEmit();
|
|
178
|
+
},
|
|
179
|
+
onPause: () => {
|
|
180
|
+
void pause();
|
|
181
|
+
},
|
|
182
|
+
onError: (message) => input.onMessage?.(message),
|
|
183
|
+
onOpened: () => void 0
|
|
184
|
+
});
|
|
185
|
+
};
|
|
186
|
+
async function pause() {
|
|
187
|
+
if (!options.pause) return;
|
|
188
|
+
const result = await options.pause(envelope.attemptId);
|
|
189
|
+
const next = "attemptId" in result ? result : { ...envelope, status: result.status, canResume: true };
|
|
190
|
+
emit(next);
|
|
191
|
+
}
|
|
192
|
+
async function resume() {
|
|
193
|
+
if (!options.resume) return;
|
|
194
|
+
const next = await options.resume(envelope.attemptId);
|
|
195
|
+
emit(next);
|
|
196
|
+
continuationUrl = continuationLink(next);
|
|
197
|
+
await presentPlugin();
|
|
198
|
+
}
|
|
199
|
+
await presentPlugin();
|
|
200
|
+
return {
|
|
201
|
+
continuationUrl,
|
|
202
|
+
destroy() {
|
|
203
|
+
abort.abort();
|
|
204
|
+
unsubscribe();
|
|
205
|
+
handle?.destroy();
|
|
206
|
+
forgetTransientSecret(envelope.attemptId);
|
|
207
|
+
},
|
|
208
|
+
open() {
|
|
209
|
+
handle?.open?.();
|
|
210
|
+
},
|
|
211
|
+
refresh: refreshAndEmit,
|
|
212
|
+
pause,
|
|
213
|
+
resume,
|
|
214
|
+
async retry() {
|
|
215
|
+
if (!options.retry || !envelope.canRetry || !isBrowserOnline()) return;
|
|
216
|
+
const next = await options.retry(envelope.attemptId);
|
|
217
|
+
emit(next);
|
|
218
|
+
continuationUrl = continuationLink(next);
|
|
219
|
+
await presentPlugin();
|
|
220
|
+
},
|
|
221
|
+
async cancel() {
|
|
222
|
+
if (!options.cancel) return;
|
|
223
|
+
const result = await options.cancel(envelope.attemptId);
|
|
224
|
+
const next = "attemptId" in result ? result : { ...envelope, status: result.status, canResume: false, canRetry: true };
|
|
225
|
+
emit(next);
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
function continuationLink(envelope) {
|
|
230
|
+
if (!envelope.continuation) return null;
|
|
231
|
+
if (options.createContinuationUrl) return options.createContinuationUrl(envelope.continuation, envelope);
|
|
232
|
+
return envelope.continuation.token;
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
refresh,
|
|
236
|
+
present,
|
|
237
|
+
isOnline: isBrowserOnline,
|
|
238
|
+
isInstalledPwa,
|
|
239
|
+
peekSecret: peekTransientSecret
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function presentHosted(container, url, onOpened) {
|
|
243
|
+
const anchor = document.createElement("a");
|
|
244
|
+
anchor.href = url;
|
|
245
|
+
anchor.target = "_blank";
|
|
246
|
+
anchor.rel = "noopener noreferrer";
|
|
247
|
+
anchor.textContent = "Open hosted verification";
|
|
248
|
+
anchor.addEventListener("click", onOpened);
|
|
249
|
+
container.replaceChildren(anchor);
|
|
250
|
+
return {
|
|
251
|
+
open() {
|
|
252
|
+
anchor.click();
|
|
253
|
+
},
|
|
254
|
+
destroy() {
|
|
255
|
+
container.replaceChildren();
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
async function presentQr(container, url, renderer) {
|
|
260
|
+
const image = document.createElement("img");
|
|
261
|
+
image.alt = "QR code to continue verification on another device";
|
|
262
|
+
const dataUrl = await createQrDataUrl(url, renderer);
|
|
263
|
+
if (dataUrl) image.src = dataUrl;
|
|
264
|
+
else image.dataset.continuation = url;
|
|
265
|
+
container.replaceChildren(image);
|
|
266
|
+
return { destroy() {
|
|
267
|
+
container.replaceChildren();
|
|
268
|
+
} };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
exports.STATUS_CONTRACT_VERSION = STATUS_CONTRACT_VERSION;
|
|
272
|
+
exports.TERMINAL_STATUSES = TERMINAL_STATUSES;
|
|
273
|
+
exports.clearPluginCache = clearPluginCache;
|
|
274
|
+
exports.clientSurface = clientSurface;
|
|
275
|
+
exports.createQrDataUrl = createQrDataUrl;
|
|
276
|
+
exports.createVerificationWebController = createVerificationWebController;
|
|
277
|
+
exports.forgetAllTransientSecrets = forgetAllTransientSecrets;
|
|
278
|
+
exports.forgetTransientSecret = forgetTransientSecret;
|
|
279
|
+
exports.isBrowserOnline = isBrowserOnline;
|
|
280
|
+
exports.isInstalledPwa = isInstalledPwa;
|
|
281
|
+
exports.loadBrowserPlugin = loadBrowserPlugin;
|
|
282
|
+
exports.peekTransientSecret = peekTransientSecret;
|
|
283
|
+
exports.prefersReducedMotion = prefersReducedMotion;
|
|
284
|
+
exports.rememberLaunchSecrets = rememberLaunchSecrets;
|
|
285
|
+
exports.rememberTransientSecret = rememberTransientSecret;
|
|
286
|
+
exports.subscribeOnlineStatus = subscribeOnlineStatus;
|
|
287
|
+
exports.takeTransientSecret = takeTransientSecret;
|
|
288
|
+
//# sourceMappingURL=index.cjs.map
|
|
289
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/environment.ts","../src/plugins.ts","../src/secrets.ts","../src/types.ts","../src/controller.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["export function isBrowserOnline(): boolean {\n if (typeof navigator === 'undefined') return true;\n return navigator.onLine !== false;\n}\n\nexport function isInstalledPwa(): boolean {\n if (typeof window === 'undefined') return false;\n const standalone = window.matchMedia?.('(display-mode: standalone)')?.matches;\n const iosStandalone = 'standalone' in navigator && Boolean((navigator as Navigator & { standalone?: boolean }).standalone);\n return Boolean(standalone || iosStandalone);\n}\n\nexport function prefersReducedMotion(): boolean {\n if (typeof window === 'undefined') return false;\n return Boolean(window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches);\n}\n\nexport type VerificationClientSurface = 'desktop' | 'mobile-web' | 'installed-pwa';\n\nexport function clientSurface(width?: number): VerificationClientSurface {\n if (isInstalledPwa()) return 'installed-pwa';\n if (typeof window === 'undefined') return 'desktop';\n const viewport = width ?? window.innerWidth;\n if (viewport <= 640 || Boolean(window.matchMedia?.('(max-width: 640px)')?.matches)) return 'mobile-web';\n return 'desktop';\n}\n\nexport function subscribeOnlineStatus(listener: (online: boolean) => void): () => void {\n if (typeof window === 'undefined') return () => undefined;\n const onOnline = () => listener(true);\n const onOffline = () => listener(false);\n window.addEventListener('online', onOnline);\n window.addEventListener('offline', onOffline);\n return () => {\n window.removeEventListener('online', onOnline);\n window.removeEventListener('offline', onOffline);\n };\n}\n\nexport async function createQrDataUrl(\n value: string,\n renderer?: (text: string) => Promise<string>,\n): Promise<string | null> {\n if (!renderer) return null;\n try {\n return await renderer(value);\n } catch {\n return null;\n }\n}\n","import type { BrowserPlugin, BrowserPluginLoader } from './types.ts';\n\nconst loaded = new Map<string, BrowserPlugin>();\n\nexport async function loadBrowserPlugin(\n launcherKey: string,\n registry: Record<string, BrowserPlugin | BrowserPluginLoader>,\n): Promise<BrowserPlugin | null> {\n const cached = loaded.get(launcherKey);\n if (cached) return cached;\n const entry = registry[launcherKey];\n if (!entry) return null;\n const plugin = typeof entry === 'function' ? unwrap(await entry()) : entry;\n loaded.set(launcherKey, plugin);\n return plugin;\n}\n\nexport function clearPluginCache(): void {\n loaded.clear();\n}\n\nfunction unwrap(value: BrowserPlugin | { default: BrowserPlugin }): BrowserPlugin {\n return 'present' in value ? value : value.default;\n}\n","const secrets = new Map<string, { secret: string; expiresAt: number }>();\n\nfunction storageBlocked(): void {\n // Transient secrets must never enter Web Storage. This function exists so\n // tests can assert the controller never writes launch credentials.\n}\n\nexport function rememberTransientSecret(attemptId: string, secret: string, expiresAt?: string | null): void {\n storageBlocked();\n secrets.set(attemptId, {\n secret,\n expiresAt: expiresAt ? Date.parse(expiresAt) : Date.now() + 5 * 60_000,\n });\n}\n\nexport function peekTransientSecret(attemptId: string, now = Date.now()): string | undefined {\n const row = secrets.get(attemptId);\n if (!row) return undefined;\n if (row.expiresAt <= now) {\n secrets.delete(attemptId);\n return undefined;\n }\n return row.secret;\n}\n\nexport function takeTransientSecret(attemptId: string, now = Date.now()): string | undefined {\n const secret = peekTransientSecret(attemptId, now);\n secrets.delete(attemptId);\n return secret;\n}\n\nexport function forgetTransientSecret(attemptId: string): void {\n secrets.delete(attemptId);\n}\n\nexport function forgetAllTransientSecrets(): void {\n secrets.clear();\n}\n\nexport function rememberLaunchSecrets(attemptId: string, launch: { transientSecret?: string; transientSecretExpiresAt?: string } | null): void {\n if (!launch?.transientSecret) return;\n rememberTransientSecret(attemptId, launch.transientSecret, launch.transientSecretExpiresAt);\n}\n","import type {\n ProviderLaunchEnvelope,\n VerificationCanonicalStatus,\n VerificationLaunchPresentation,\n VerificationPackageCode,\n} from '@splitin/verification-adapter-sdk';\n\nexport const STATUS_CONTRACT_VERSION = '1.0.0' as const;\n\nexport interface StatusContinuation {\n key: string;\n token: string;\n expiresAt: string;\n}\n\nexport interface VerificationStatusEnvelope {\n contractVersion?: typeof STATUS_CONTRACT_VERSION | string;\n attemptId: string;\n packageCode: VerificationPackageCode;\n status: VerificationCanonicalStatus;\n presentation: VerificationLaunchPresentation;\n launch: ProviderLaunchEnvelope | null;\n launcherKey: string | null;\n providerDisclosure: string | null;\n safeErrorCode: string | null;\n retryAfter: string | null;\n supportPath: string | null;\n expiresAt: string | null;\n canResume: boolean;\n canRetry: boolean;\n continuation: StatusContinuation | null;\n}\n\nexport interface BrowserPluginHandle {\n open?(): void;\n destroy(): void;\n}\n\nexport interface BrowserPluginPresentInput {\n container: HTMLElement;\n launch: ProviderLaunchEnvelope;\n onComplete: () => void;\n onPause: () => void;\n onError: (message: string) => void;\n onOpened: () => void;\n signal?: AbortSignal;\n}\n\nexport interface BrowserPlugin {\n launcherKey: string;\n present(input: BrowserPluginPresentInput): Promise<BrowserPluginHandle>;\n}\n\nexport type BrowserPluginLoader = () => Promise<BrowserPlugin | { default: BrowserPlugin }>;\n\nexport interface VerificationWebControllerOptions {\n plugins: Record<string, BrowserPlugin | BrowserPluginLoader>;\n refreshStatus: (attemptId: string) => Promise<VerificationStatusEnvelope>;\n pause?: (attemptId: string) => Promise<VerificationStatusEnvelope | { status: VerificationCanonicalStatus }>;\n resume?: (attemptId: string) => Promise<VerificationStatusEnvelope>;\n retry?: (attemptId: string) => Promise<VerificationStatusEnvelope>;\n cancel?: (attemptId: string) => Promise<VerificationStatusEnvelope | { status: VerificationCanonicalStatus }>;\n createContinuationUrl?: (continuation: StatusContinuation, envelope: VerificationStatusEnvelope) => string;\n renderQr?: (value: string) => Promise<string>;\n now?: () => Date;\n}\n\nexport interface PresentSessionInput {\n envelope: VerificationStatusEnvelope;\n container: HTMLElement;\n onEnvelopeChange?: (envelope: VerificationStatusEnvelope) => void;\n onMessage?: (message: string) => void;\n}\n\nexport interface PresentedSession {\n destroy(): void;\n open(): void;\n refresh(): Promise<VerificationStatusEnvelope>;\n pause(): Promise<void>;\n resume(): Promise<void>;\n retry(): Promise<void>;\n cancel(): Promise<void>;\n continuationUrl: string | null;\n}\n\nexport const TERMINAL_STATUSES = new Set<VerificationCanonicalStatus>([\n 'verified',\n 'declined',\n 'failed',\n 'expired',\n 'canceled',\n 'redacted',\n]);\n","import { createQrDataUrl, isBrowserOnline, isInstalledPwa, subscribeOnlineStatus } from './environment.ts';\nimport { loadBrowserPlugin } from './plugins.ts';\nimport { forgetTransientSecret, peekTransientSecret, rememberLaunchSecrets } from './secrets.ts';\nimport {\n TERMINAL_STATUSES,\n type BrowserPluginHandle,\n type PresentedSession,\n type PresentSessionInput,\n type VerificationStatusEnvelope,\n type VerificationWebControllerOptions,\n} from './types.ts';\n\nexport function createVerificationWebController(options: VerificationWebControllerOptions) {\n const now = options.now ?? (() => new Date());\n\n async function refresh(attemptId: string): Promise<VerificationStatusEnvelope> {\n const envelope = await options.refreshStatus(attemptId);\n rememberLaunchSecrets(attemptId, envelope.launch);\n return envelope;\n }\n\n async function present(input: PresentSessionInput): Promise<PresentedSession> {\n let envelope = input.envelope;\n rememberLaunchSecrets(envelope.attemptId, envelope.launch);\n const abort = new AbortController();\n let handle: BrowserPluginHandle | null = null;\n let continuationUrl = continuationLink(envelope);\n const unsubscribe = subscribeOnlineStatus((online) => {\n if (!online) input.onMessage?.('You are offline. Your place is saved. Reconnect to resume verification.');\n else void refreshAndEmit();\n });\n\n const emit = (next: VerificationStatusEnvelope, message?: string) => {\n envelope = next;\n input.onEnvelopeChange?.(next);\n if (message) input.onMessage?.(message);\n };\n\n const refreshAndEmit = async (): Promise<VerificationStatusEnvelope> => {\n const next = await refresh(envelope.attemptId);\n const verified = next.status === 'verified';\n emit(next, verified\n ? 'Verified. Returning to your saved action.'\n : 'Your status is not approved yet. The protected action stays saved.');\n return next;\n };\n\n const presentPlugin = async (): Promise<void> => {\n handle?.destroy();\n handle = null;\n const launch = envelope.launch;\n if (!launch || envelope.presentation === 'none' || TERMINAL_STATUSES.has(envelope.status)) return;\n if (envelope.status === 'processing' || envelope.status === 'manual_review_required') return;\n if (!isBrowserOnline()) return;\n if (launch.transientSecretExpiresAt && Date.parse(launch.transientSecretExpiresAt) <= now().getTime()) {\n forgetTransientSecret(envelope.attemptId);\n input.onMessage?.('The secure launch credential expired. Checking status.');\n await refreshAndEmit();\n return;\n }\n if (envelope.presentation === 'hosted' && launch.hostedUrl) {\n handle = presentHosted(input.container, launch.hostedUrl, () => { void refreshAndEmit(); });\n return;\n }\n if (envelope.presentation === 'qr' && continuationUrl) {\n handle = await presentQr(input.container, continuationUrl, options.renderQr);\n return;\n }\n const plugin = await loadBrowserPlugin(launch.launcherKey, options.plugins);\n if (!plugin) {\n if (launch.hostedUrl) {\n handle = presentHosted(input.container, launch.hostedUrl, () => { void refreshAndEmit(); });\n }\n return;\n }\n handle = await plugin.present({\n container: input.container,\n launch: {\n ...launch,\n transientSecret: peekTransientSecret(envelope.attemptId) ?? launch.transientSecret,\n },\n signal: abort.signal,\n onComplete: () => {\n input.onMessage?.('Verification was submitted. Checking the server-side result now.');\n void refreshAndEmit();\n },\n onPause: () => { void pause(); },\n onError: (message) => input.onMessage?.(message),\n onOpened: () => undefined,\n });\n };\n\n async function pause(): Promise<void> {\n if (!options.pause) return;\n const result = await options.pause(envelope.attemptId);\n const next = 'attemptId' in result ? result : { ...envelope, status: result.status, canResume: true };\n emit(next as VerificationStatusEnvelope);\n }\n\n async function resume(): Promise<void> {\n if (!options.resume) return;\n const next = await options.resume(envelope.attemptId);\n emit(next);\n continuationUrl = continuationLink(next);\n await presentPlugin();\n }\n\n await presentPlugin();\n\n return {\n continuationUrl,\n destroy() {\n abort.abort();\n unsubscribe();\n handle?.destroy();\n forgetTransientSecret(envelope.attemptId);\n },\n open() {\n handle?.open?.();\n },\n refresh: refreshAndEmit,\n pause,\n resume,\n async retry() {\n if (!options.retry || !envelope.canRetry || !isBrowserOnline()) return;\n const next = await options.retry(envelope.attemptId);\n emit(next);\n continuationUrl = continuationLink(next);\n await presentPlugin();\n },\n async cancel() {\n if (!options.cancel) return;\n const result = await options.cancel(envelope.attemptId);\n const next = 'attemptId' in result ? result : { ...envelope, status: result.status, canResume: false, canRetry: true };\n emit(next as VerificationStatusEnvelope);\n },\n };\n }\n\n function continuationLink(envelope: VerificationStatusEnvelope): string | null {\n if (!envelope.continuation) return null;\n if (options.createContinuationUrl) return options.createContinuationUrl(envelope.continuation, envelope);\n return envelope.continuation.token;\n }\n\n return {\n refresh,\n present,\n isOnline: isBrowserOnline,\n isInstalledPwa,\n peekSecret: peekTransientSecret,\n };\n}\n\nfunction presentHosted(container: HTMLElement, url: string, onOpened: () => void): BrowserPluginHandle {\n const anchor = document.createElement('a');\n anchor.href = url;\n anchor.target = '_blank';\n anchor.rel = 'noopener noreferrer';\n anchor.textContent = 'Open hosted verification';\n anchor.addEventListener('click', onOpened);\n container.replaceChildren(anchor);\n return {\n open() { anchor.click(); },\n destroy() { container.replaceChildren(); },\n };\n}\n\nasync function presentQr(\n container: HTMLElement,\n url: string,\n renderer?: (value: string) => Promise<string>,\n): Promise<BrowserPluginHandle> {\n const image = document.createElement('img');\n image.alt = 'QR code to continue verification on another device';\n const dataUrl = await createQrDataUrl(url, renderer);\n if (dataUrl) image.src = dataUrl;\n else image.dataset.continuation = url;\n container.replaceChildren(image);\n return { destroy() { container.replaceChildren(); } };\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { ProviderLaunchEnvelope, VerificationPackageCode, VerificationCanonicalStatus, VerificationLaunchPresentation } from '@splitin/verification-adapter-sdk';
|
|
2
|
+
|
|
3
|
+
declare function isBrowserOnline(): boolean;
|
|
4
|
+
declare function isInstalledPwa(): boolean;
|
|
5
|
+
declare function prefersReducedMotion(): boolean;
|
|
6
|
+
type VerificationClientSurface = 'desktop' | 'mobile-web' | 'installed-pwa';
|
|
7
|
+
declare function clientSurface(width?: number): VerificationClientSurface;
|
|
8
|
+
declare function subscribeOnlineStatus(listener: (online: boolean) => void): () => void;
|
|
9
|
+
declare function createQrDataUrl(value: string, renderer?: (text: string) => Promise<string>): Promise<string | null>;
|
|
10
|
+
|
|
11
|
+
declare function rememberTransientSecret(attemptId: string, secret: string, expiresAt?: string | null): void;
|
|
12
|
+
declare function peekTransientSecret(attemptId: string, now?: number): string | undefined;
|
|
13
|
+
declare function takeTransientSecret(attemptId: string, now?: number): string | undefined;
|
|
14
|
+
declare function forgetTransientSecret(attemptId: string): void;
|
|
15
|
+
declare function forgetAllTransientSecrets(): void;
|
|
16
|
+
declare function rememberLaunchSecrets(attemptId: string, launch: {
|
|
17
|
+
transientSecret?: string;
|
|
18
|
+
transientSecretExpiresAt?: string;
|
|
19
|
+
} | null): void;
|
|
20
|
+
|
|
21
|
+
declare const STATUS_CONTRACT_VERSION: "1.0.0";
|
|
22
|
+
interface StatusContinuation {
|
|
23
|
+
key: string;
|
|
24
|
+
token: string;
|
|
25
|
+
expiresAt: string;
|
|
26
|
+
}
|
|
27
|
+
interface VerificationStatusEnvelope {
|
|
28
|
+
contractVersion?: typeof STATUS_CONTRACT_VERSION | string;
|
|
29
|
+
attemptId: string;
|
|
30
|
+
packageCode: VerificationPackageCode;
|
|
31
|
+
status: VerificationCanonicalStatus;
|
|
32
|
+
presentation: VerificationLaunchPresentation;
|
|
33
|
+
launch: ProviderLaunchEnvelope | null;
|
|
34
|
+
launcherKey: string | null;
|
|
35
|
+
providerDisclosure: string | null;
|
|
36
|
+
safeErrorCode: string | null;
|
|
37
|
+
retryAfter: string | null;
|
|
38
|
+
supportPath: string | null;
|
|
39
|
+
expiresAt: string | null;
|
|
40
|
+
canResume: boolean;
|
|
41
|
+
canRetry: boolean;
|
|
42
|
+
continuation: StatusContinuation | null;
|
|
43
|
+
}
|
|
44
|
+
interface BrowserPluginHandle {
|
|
45
|
+
open?(): void;
|
|
46
|
+
destroy(): void;
|
|
47
|
+
}
|
|
48
|
+
interface BrowserPluginPresentInput {
|
|
49
|
+
container: HTMLElement;
|
|
50
|
+
launch: ProviderLaunchEnvelope;
|
|
51
|
+
onComplete: () => void;
|
|
52
|
+
onPause: () => void;
|
|
53
|
+
onError: (message: string) => void;
|
|
54
|
+
onOpened: () => void;
|
|
55
|
+
signal?: AbortSignal;
|
|
56
|
+
}
|
|
57
|
+
interface BrowserPlugin {
|
|
58
|
+
launcherKey: string;
|
|
59
|
+
present(input: BrowserPluginPresentInput): Promise<BrowserPluginHandle>;
|
|
60
|
+
}
|
|
61
|
+
type BrowserPluginLoader = () => Promise<BrowserPlugin | {
|
|
62
|
+
default: BrowserPlugin;
|
|
63
|
+
}>;
|
|
64
|
+
interface VerificationWebControllerOptions {
|
|
65
|
+
plugins: Record<string, BrowserPlugin | BrowserPluginLoader>;
|
|
66
|
+
refreshStatus: (attemptId: string) => Promise<VerificationStatusEnvelope>;
|
|
67
|
+
pause?: (attemptId: string) => Promise<VerificationStatusEnvelope | {
|
|
68
|
+
status: VerificationCanonicalStatus;
|
|
69
|
+
}>;
|
|
70
|
+
resume?: (attemptId: string) => Promise<VerificationStatusEnvelope>;
|
|
71
|
+
retry?: (attemptId: string) => Promise<VerificationStatusEnvelope>;
|
|
72
|
+
cancel?: (attemptId: string) => Promise<VerificationStatusEnvelope | {
|
|
73
|
+
status: VerificationCanonicalStatus;
|
|
74
|
+
}>;
|
|
75
|
+
createContinuationUrl?: (continuation: StatusContinuation, envelope: VerificationStatusEnvelope) => string;
|
|
76
|
+
renderQr?: (value: string) => Promise<string>;
|
|
77
|
+
now?: () => Date;
|
|
78
|
+
}
|
|
79
|
+
interface PresentSessionInput {
|
|
80
|
+
envelope: VerificationStatusEnvelope;
|
|
81
|
+
container: HTMLElement;
|
|
82
|
+
onEnvelopeChange?: (envelope: VerificationStatusEnvelope) => void;
|
|
83
|
+
onMessage?: (message: string) => void;
|
|
84
|
+
}
|
|
85
|
+
interface PresentedSession {
|
|
86
|
+
destroy(): void;
|
|
87
|
+
open(): void;
|
|
88
|
+
refresh(): Promise<VerificationStatusEnvelope>;
|
|
89
|
+
pause(): Promise<void>;
|
|
90
|
+
resume(): Promise<void>;
|
|
91
|
+
retry(): Promise<void>;
|
|
92
|
+
cancel(): Promise<void>;
|
|
93
|
+
continuationUrl: string | null;
|
|
94
|
+
}
|
|
95
|
+
declare const TERMINAL_STATUSES: Set<"created" | "pending_user_input" | "paused" | "processing" | "manual_review_required" | "verified" | "declined" | "failed" | "expired" | "canceled" | "provider_unavailable" | "redacted">;
|
|
96
|
+
|
|
97
|
+
declare function createVerificationWebController(options: VerificationWebControllerOptions): {
|
|
98
|
+
refresh: (attemptId: string) => Promise<VerificationStatusEnvelope>;
|
|
99
|
+
present: (input: PresentSessionInput) => Promise<PresentedSession>;
|
|
100
|
+
isOnline: typeof isBrowserOnline;
|
|
101
|
+
isInstalledPwa: typeof isInstalledPwa;
|
|
102
|
+
peekSecret: typeof peekTransientSecret;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
declare function loadBrowserPlugin(launcherKey: string, registry: Record<string, BrowserPlugin | BrowserPluginLoader>): Promise<BrowserPlugin | null>;
|
|
106
|
+
declare function clearPluginCache(): void;
|
|
107
|
+
|
|
108
|
+
export { type BrowserPlugin, type BrowserPluginHandle, type BrowserPluginLoader, type BrowserPluginPresentInput, type PresentSessionInput, type PresentedSession, STATUS_CONTRACT_VERSION, type StatusContinuation, TERMINAL_STATUSES, type VerificationClientSurface, type VerificationStatusEnvelope, type VerificationWebControllerOptions, clearPluginCache, clientSurface, createQrDataUrl, createVerificationWebController, forgetAllTransientSecrets, forgetTransientSecret, isBrowserOnline, isInstalledPwa, loadBrowserPlugin, peekTransientSecret, prefersReducedMotion, rememberLaunchSecrets, rememberTransientSecret, subscribeOnlineStatus, takeTransientSecret };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { ProviderLaunchEnvelope, VerificationPackageCode, VerificationCanonicalStatus, VerificationLaunchPresentation } from '@splitin/verification-adapter-sdk';
|
|
2
|
+
|
|
3
|
+
declare function isBrowserOnline(): boolean;
|
|
4
|
+
declare function isInstalledPwa(): boolean;
|
|
5
|
+
declare function prefersReducedMotion(): boolean;
|
|
6
|
+
type VerificationClientSurface = 'desktop' | 'mobile-web' | 'installed-pwa';
|
|
7
|
+
declare function clientSurface(width?: number): VerificationClientSurface;
|
|
8
|
+
declare function subscribeOnlineStatus(listener: (online: boolean) => void): () => void;
|
|
9
|
+
declare function createQrDataUrl(value: string, renderer?: (text: string) => Promise<string>): Promise<string | null>;
|
|
10
|
+
|
|
11
|
+
declare function rememberTransientSecret(attemptId: string, secret: string, expiresAt?: string | null): void;
|
|
12
|
+
declare function peekTransientSecret(attemptId: string, now?: number): string | undefined;
|
|
13
|
+
declare function takeTransientSecret(attemptId: string, now?: number): string | undefined;
|
|
14
|
+
declare function forgetTransientSecret(attemptId: string): void;
|
|
15
|
+
declare function forgetAllTransientSecrets(): void;
|
|
16
|
+
declare function rememberLaunchSecrets(attemptId: string, launch: {
|
|
17
|
+
transientSecret?: string;
|
|
18
|
+
transientSecretExpiresAt?: string;
|
|
19
|
+
} | null): void;
|
|
20
|
+
|
|
21
|
+
declare const STATUS_CONTRACT_VERSION: "1.0.0";
|
|
22
|
+
interface StatusContinuation {
|
|
23
|
+
key: string;
|
|
24
|
+
token: string;
|
|
25
|
+
expiresAt: string;
|
|
26
|
+
}
|
|
27
|
+
interface VerificationStatusEnvelope {
|
|
28
|
+
contractVersion?: typeof STATUS_CONTRACT_VERSION | string;
|
|
29
|
+
attemptId: string;
|
|
30
|
+
packageCode: VerificationPackageCode;
|
|
31
|
+
status: VerificationCanonicalStatus;
|
|
32
|
+
presentation: VerificationLaunchPresentation;
|
|
33
|
+
launch: ProviderLaunchEnvelope | null;
|
|
34
|
+
launcherKey: string | null;
|
|
35
|
+
providerDisclosure: string | null;
|
|
36
|
+
safeErrorCode: string | null;
|
|
37
|
+
retryAfter: string | null;
|
|
38
|
+
supportPath: string | null;
|
|
39
|
+
expiresAt: string | null;
|
|
40
|
+
canResume: boolean;
|
|
41
|
+
canRetry: boolean;
|
|
42
|
+
continuation: StatusContinuation | null;
|
|
43
|
+
}
|
|
44
|
+
interface BrowserPluginHandle {
|
|
45
|
+
open?(): void;
|
|
46
|
+
destroy(): void;
|
|
47
|
+
}
|
|
48
|
+
interface BrowserPluginPresentInput {
|
|
49
|
+
container: HTMLElement;
|
|
50
|
+
launch: ProviderLaunchEnvelope;
|
|
51
|
+
onComplete: () => void;
|
|
52
|
+
onPause: () => void;
|
|
53
|
+
onError: (message: string) => void;
|
|
54
|
+
onOpened: () => void;
|
|
55
|
+
signal?: AbortSignal;
|
|
56
|
+
}
|
|
57
|
+
interface BrowserPlugin {
|
|
58
|
+
launcherKey: string;
|
|
59
|
+
present(input: BrowserPluginPresentInput): Promise<BrowserPluginHandle>;
|
|
60
|
+
}
|
|
61
|
+
type BrowserPluginLoader = () => Promise<BrowserPlugin | {
|
|
62
|
+
default: BrowserPlugin;
|
|
63
|
+
}>;
|
|
64
|
+
interface VerificationWebControllerOptions {
|
|
65
|
+
plugins: Record<string, BrowserPlugin | BrowserPluginLoader>;
|
|
66
|
+
refreshStatus: (attemptId: string) => Promise<VerificationStatusEnvelope>;
|
|
67
|
+
pause?: (attemptId: string) => Promise<VerificationStatusEnvelope | {
|
|
68
|
+
status: VerificationCanonicalStatus;
|
|
69
|
+
}>;
|
|
70
|
+
resume?: (attemptId: string) => Promise<VerificationStatusEnvelope>;
|
|
71
|
+
retry?: (attemptId: string) => Promise<VerificationStatusEnvelope>;
|
|
72
|
+
cancel?: (attemptId: string) => Promise<VerificationStatusEnvelope | {
|
|
73
|
+
status: VerificationCanonicalStatus;
|
|
74
|
+
}>;
|
|
75
|
+
createContinuationUrl?: (continuation: StatusContinuation, envelope: VerificationStatusEnvelope) => string;
|
|
76
|
+
renderQr?: (value: string) => Promise<string>;
|
|
77
|
+
now?: () => Date;
|
|
78
|
+
}
|
|
79
|
+
interface PresentSessionInput {
|
|
80
|
+
envelope: VerificationStatusEnvelope;
|
|
81
|
+
container: HTMLElement;
|
|
82
|
+
onEnvelopeChange?: (envelope: VerificationStatusEnvelope) => void;
|
|
83
|
+
onMessage?: (message: string) => void;
|
|
84
|
+
}
|
|
85
|
+
interface PresentedSession {
|
|
86
|
+
destroy(): void;
|
|
87
|
+
open(): void;
|
|
88
|
+
refresh(): Promise<VerificationStatusEnvelope>;
|
|
89
|
+
pause(): Promise<void>;
|
|
90
|
+
resume(): Promise<void>;
|
|
91
|
+
retry(): Promise<void>;
|
|
92
|
+
cancel(): Promise<void>;
|
|
93
|
+
continuationUrl: string | null;
|
|
94
|
+
}
|
|
95
|
+
declare const TERMINAL_STATUSES: Set<"created" | "pending_user_input" | "paused" | "processing" | "manual_review_required" | "verified" | "declined" | "failed" | "expired" | "canceled" | "provider_unavailable" | "redacted">;
|
|
96
|
+
|
|
97
|
+
declare function createVerificationWebController(options: VerificationWebControllerOptions): {
|
|
98
|
+
refresh: (attemptId: string) => Promise<VerificationStatusEnvelope>;
|
|
99
|
+
present: (input: PresentSessionInput) => Promise<PresentedSession>;
|
|
100
|
+
isOnline: typeof isBrowserOnline;
|
|
101
|
+
isInstalledPwa: typeof isInstalledPwa;
|
|
102
|
+
peekSecret: typeof peekTransientSecret;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
declare function loadBrowserPlugin(launcherKey: string, registry: Record<string, BrowserPlugin | BrowserPluginLoader>): Promise<BrowserPlugin | null>;
|
|
106
|
+
declare function clearPluginCache(): void;
|
|
107
|
+
|
|
108
|
+
export { type BrowserPlugin, type BrowserPluginHandle, type BrowserPluginLoader, type BrowserPluginPresentInput, type PresentSessionInput, type PresentedSession, STATUS_CONTRACT_VERSION, type StatusContinuation, TERMINAL_STATUSES, type VerificationClientSurface, type VerificationStatusEnvelope, type VerificationWebControllerOptions, clearPluginCache, clientSurface, createQrDataUrl, createVerificationWebController, forgetAllTransientSecrets, forgetTransientSecret, isBrowserOnline, isInstalledPwa, loadBrowserPlugin, peekTransientSecret, prefersReducedMotion, rememberLaunchSecrets, rememberTransientSecret, subscribeOnlineStatus, takeTransientSecret };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sources":["index.d.ts"],"names":[],"mappings":"AAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
// src/environment.ts
|
|
2
|
+
function isBrowserOnline() {
|
|
3
|
+
if (typeof navigator === "undefined") return true;
|
|
4
|
+
return navigator.onLine !== false;
|
|
5
|
+
}
|
|
6
|
+
function isInstalledPwa() {
|
|
7
|
+
if (typeof window === "undefined") return false;
|
|
8
|
+
const standalone = window.matchMedia?.("(display-mode: standalone)")?.matches;
|
|
9
|
+
const iosStandalone = "standalone" in navigator && Boolean(navigator.standalone);
|
|
10
|
+
return Boolean(standalone || iosStandalone);
|
|
11
|
+
}
|
|
12
|
+
function prefersReducedMotion() {
|
|
13
|
+
if (typeof window === "undefined") return false;
|
|
14
|
+
return Boolean(window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches);
|
|
15
|
+
}
|
|
16
|
+
function clientSurface(width) {
|
|
17
|
+
if (isInstalledPwa()) return "installed-pwa";
|
|
18
|
+
if (typeof window === "undefined") return "desktop";
|
|
19
|
+
const viewport = width ?? window.innerWidth;
|
|
20
|
+
if (viewport <= 640 || Boolean(window.matchMedia?.("(max-width: 640px)")?.matches)) return "mobile-web";
|
|
21
|
+
return "desktop";
|
|
22
|
+
}
|
|
23
|
+
function subscribeOnlineStatus(listener) {
|
|
24
|
+
if (typeof window === "undefined") return () => void 0;
|
|
25
|
+
const onOnline = () => listener(true);
|
|
26
|
+
const onOffline = () => listener(false);
|
|
27
|
+
window.addEventListener("online", onOnline);
|
|
28
|
+
window.addEventListener("offline", onOffline);
|
|
29
|
+
return () => {
|
|
30
|
+
window.removeEventListener("online", onOnline);
|
|
31
|
+
window.removeEventListener("offline", onOffline);
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
async function createQrDataUrl(value, renderer) {
|
|
35
|
+
if (!renderer) return null;
|
|
36
|
+
try {
|
|
37
|
+
return await renderer(value);
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// src/plugins.ts
|
|
44
|
+
var loaded = /* @__PURE__ */ new Map();
|
|
45
|
+
async function loadBrowserPlugin(launcherKey, registry) {
|
|
46
|
+
const cached = loaded.get(launcherKey);
|
|
47
|
+
if (cached) return cached;
|
|
48
|
+
const entry = registry[launcherKey];
|
|
49
|
+
if (!entry) return null;
|
|
50
|
+
const plugin = typeof entry === "function" ? unwrap(await entry()) : entry;
|
|
51
|
+
loaded.set(launcherKey, plugin);
|
|
52
|
+
return plugin;
|
|
53
|
+
}
|
|
54
|
+
function clearPluginCache() {
|
|
55
|
+
loaded.clear();
|
|
56
|
+
}
|
|
57
|
+
function unwrap(value) {
|
|
58
|
+
return "present" in value ? value : value.default;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/secrets.ts
|
|
62
|
+
var secrets = /* @__PURE__ */ new Map();
|
|
63
|
+
function rememberTransientSecret(attemptId, secret, expiresAt) {
|
|
64
|
+
secrets.set(attemptId, {
|
|
65
|
+
secret,
|
|
66
|
+
expiresAt: expiresAt ? Date.parse(expiresAt) : Date.now() + 5 * 6e4
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
function peekTransientSecret(attemptId, now = Date.now()) {
|
|
70
|
+
const row = secrets.get(attemptId);
|
|
71
|
+
if (!row) return void 0;
|
|
72
|
+
if (row.expiresAt <= now) {
|
|
73
|
+
secrets.delete(attemptId);
|
|
74
|
+
return void 0;
|
|
75
|
+
}
|
|
76
|
+
return row.secret;
|
|
77
|
+
}
|
|
78
|
+
function takeTransientSecret(attemptId, now = Date.now()) {
|
|
79
|
+
const secret = peekTransientSecret(attemptId, now);
|
|
80
|
+
secrets.delete(attemptId);
|
|
81
|
+
return secret;
|
|
82
|
+
}
|
|
83
|
+
function forgetTransientSecret(attemptId) {
|
|
84
|
+
secrets.delete(attemptId);
|
|
85
|
+
}
|
|
86
|
+
function forgetAllTransientSecrets() {
|
|
87
|
+
secrets.clear();
|
|
88
|
+
}
|
|
89
|
+
function rememberLaunchSecrets(attemptId, launch) {
|
|
90
|
+
if (!launch?.transientSecret) return;
|
|
91
|
+
rememberTransientSecret(attemptId, launch.transientSecret, launch.transientSecretExpiresAt);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/types.ts
|
|
95
|
+
var STATUS_CONTRACT_VERSION = "1.0.0";
|
|
96
|
+
var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
|
|
97
|
+
"verified",
|
|
98
|
+
"declined",
|
|
99
|
+
"failed",
|
|
100
|
+
"expired",
|
|
101
|
+
"canceled",
|
|
102
|
+
"redacted"
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
// src/controller.ts
|
|
106
|
+
function createVerificationWebController(options) {
|
|
107
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
108
|
+
async function refresh(attemptId) {
|
|
109
|
+
const envelope = await options.refreshStatus(attemptId);
|
|
110
|
+
rememberLaunchSecrets(attemptId, envelope.launch);
|
|
111
|
+
return envelope;
|
|
112
|
+
}
|
|
113
|
+
async function present(input) {
|
|
114
|
+
let envelope = input.envelope;
|
|
115
|
+
rememberLaunchSecrets(envelope.attemptId, envelope.launch);
|
|
116
|
+
const abort = new AbortController();
|
|
117
|
+
let handle = null;
|
|
118
|
+
let continuationUrl = continuationLink(envelope);
|
|
119
|
+
const unsubscribe = subscribeOnlineStatus((online) => {
|
|
120
|
+
if (!online) input.onMessage?.("You are offline. Your place is saved. Reconnect to resume verification.");
|
|
121
|
+
else void refreshAndEmit();
|
|
122
|
+
});
|
|
123
|
+
const emit = (next, message) => {
|
|
124
|
+
envelope = next;
|
|
125
|
+
input.onEnvelopeChange?.(next);
|
|
126
|
+
if (message) input.onMessage?.(message);
|
|
127
|
+
};
|
|
128
|
+
const refreshAndEmit = async () => {
|
|
129
|
+
const next = await refresh(envelope.attemptId);
|
|
130
|
+
const verified = next.status === "verified";
|
|
131
|
+
emit(next, verified ? "Verified. Returning to your saved action." : "Your status is not approved yet. The protected action stays saved.");
|
|
132
|
+
return next;
|
|
133
|
+
};
|
|
134
|
+
const presentPlugin = async () => {
|
|
135
|
+
handle?.destroy();
|
|
136
|
+
handle = null;
|
|
137
|
+
const launch = envelope.launch;
|
|
138
|
+
if (!launch || envelope.presentation === "none" || TERMINAL_STATUSES.has(envelope.status)) return;
|
|
139
|
+
if (envelope.status === "processing" || envelope.status === "manual_review_required") return;
|
|
140
|
+
if (!isBrowserOnline()) return;
|
|
141
|
+
if (launch.transientSecretExpiresAt && Date.parse(launch.transientSecretExpiresAt) <= now().getTime()) {
|
|
142
|
+
forgetTransientSecret(envelope.attemptId);
|
|
143
|
+
input.onMessage?.("The secure launch credential expired. Checking status.");
|
|
144
|
+
await refreshAndEmit();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (envelope.presentation === "hosted" && launch.hostedUrl) {
|
|
148
|
+
handle = presentHosted(input.container, launch.hostedUrl, () => {
|
|
149
|
+
void refreshAndEmit();
|
|
150
|
+
});
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (envelope.presentation === "qr" && continuationUrl) {
|
|
154
|
+
handle = await presentQr(input.container, continuationUrl, options.renderQr);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const plugin = await loadBrowserPlugin(launch.launcherKey, options.plugins);
|
|
158
|
+
if (!plugin) {
|
|
159
|
+
if (launch.hostedUrl) {
|
|
160
|
+
handle = presentHosted(input.container, launch.hostedUrl, () => {
|
|
161
|
+
void refreshAndEmit();
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
handle = await plugin.present({
|
|
167
|
+
container: input.container,
|
|
168
|
+
launch: {
|
|
169
|
+
...launch,
|
|
170
|
+
transientSecret: peekTransientSecret(envelope.attemptId) ?? launch.transientSecret
|
|
171
|
+
},
|
|
172
|
+
signal: abort.signal,
|
|
173
|
+
onComplete: () => {
|
|
174
|
+
input.onMessage?.("Verification was submitted. Checking the server-side result now.");
|
|
175
|
+
void refreshAndEmit();
|
|
176
|
+
},
|
|
177
|
+
onPause: () => {
|
|
178
|
+
void pause();
|
|
179
|
+
},
|
|
180
|
+
onError: (message) => input.onMessage?.(message),
|
|
181
|
+
onOpened: () => void 0
|
|
182
|
+
});
|
|
183
|
+
};
|
|
184
|
+
async function pause() {
|
|
185
|
+
if (!options.pause) return;
|
|
186
|
+
const result = await options.pause(envelope.attemptId);
|
|
187
|
+
const next = "attemptId" in result ? result : { ...envelope, status: result.status, canResume: true };
|
|
188
|
+
emit(next);
|
|
189
|
+
}
|
|
190
|
+
async function resume() {
|
|
191
|
+
if (!options.resume) return;
|
|
192
|
+
const next = await options.resume(envelope.attemptId);
|
|
193
|
+
emit(next);
|
|
194
|
+
continuationUrl = continuationLink(next);
|
|
195
|
+
await presentPlugin();
|
|
196
|
+
}
|
|
197
|
+
await presentPlugin();
|
|
198
|
+
return {
|
|
199
|
+
continuationUrl,
|
|
200
|
+
destroy() {
|
|
201
|
+
abort.abort();
|
|
202
|
+
unsubscribe();
|
|
203
|
+
handle?.destroy();
|
|
204
|
+
forgetTransientSecret(envelope.attemptId);
|
|
205
|
+
},
|
|
206
|
+
open() {
|
|
207
|
+
handle?.open?.();
|
|
208
|
+
},
|
|
209
|
+
refresh: refreshAndEmit,
|
|
210
|
+
pause,
|
|
211
|
+
resume,
|
|
212
|
+
async retry() {
|
|
213
|
+
if (!options.retry || !envelope.canRetry || !isBrowserOnline()) return;
|
|
214
|
+
const next = await options.retry(envelope.attemptId);
|
|
215
|
+
emit(next);
|
|
216
|
+
continuationUrl = continuationLink(next);
|
|
217
|
+
await presentPlugin();
|
|
218
|
+
},
|
|
219
|
+
async cancel() {
|
|
220
|
+
if (!options.cancel) return;
|
|
221
|
+
const result = await options.cancel(envelope.attemptId);
|
|
222
|
+
const next = "attemptId" in result ? result : { ...envelope, status: result.status, canResume: false, canRetry: true };
|
|
223
|
+
emit(next);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function continuationLink(envelope) {
|
|
228
|
+
if (!envelope.continuation) return null;
|
|
229
|
+
if (options.createContinuationUrl) return options.createContinuationUrl(envelope.continuation, envelope);
|
|
230
|
+
return envelope.continuation.token;
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
refresh,
|
|
234
|
+
present,
|
|
235
|
+
isOnline: isBrowserOnline,
|
|
236
|
+
isInstalledPwa,
|
|
237
|
+
peekSecret: peekTransientSecret
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
function presentHosted(container, url, onOpened) {
|
|
241
|
+
const anchor = document.createElement("a");
|
|
242
|
+
anchor.href = url;
|
|
243
|
+
anchor.target = "_blank";
|
|
244
|
+
anchor.rel = "noopener noreferrer";
|
|
245
|
+
anchor.textContent = "Open hosted verification";
|
|
246
|
+
anchor.addEventListener("click", onOpened);
|
|
247
|
+
container.replaceChildren(anchor);
|
|
248
|
+
return {
|
|
249
|
+
open() {
|
|
250
|
+
anchor.click();
|
|
251
|
+
},
|
|
252
|
+
destroy() {
|
|
253
|
+
container.replaceChildren();
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
async function presentQr(container, url, renderer) {
|
|
258
|
+
const image = document.createElement("img");
|
|
259
|
+
image.alt = "QR code to continue verification on another device";
|
|
260
|
+
const dataUrl = await createQrDataUrl(url, renderer);
|
|
261
|
+
if (dataUrl) image.src = dataUrl;
|
|
262
|
+
else image.dataset.continuation = url;
|
|
263
|
+
container.replaceChildren(image);
|
|
264
|
+
return { destroy() {
|
|
265
|
+
container.replaceChildren();
|
|
266
|
+
} };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export { STATUS_CONTRACT_VERSION, TERMINAL_STATUSES, clearPluginCache, clientSurface, createQrDataUrl, createVerificationWebController, forgetAllTransientSecrets, forgetTransientSecret, isBrowserOnline, isInstalledPwa, loadBrowserPlugin, peekTransientSecret, prefersReducedMotion, rememberLaunchSecrets, rememberTransientSecret, subscribeOnlineStatus, takeTransientSecret };
|
|
270
|
+
//# sourceMappingURL=index.js.map
|
|
271
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/environment.ts","../src/plugins.ts","../src/secrets.ts","../src/types.ts","../src/controller.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["export function isBrowserOnline(): boolean {\n if (typeof navigator === 'undefined') return true;\n return navigator.onLine !== false;\n}\n\nexport function isInstalledPwa(): boolean {\n if (typeof window === 'undefined') return false;\n const standalone = window.matchMedia?.('(display-mode: standalone)')?.matches;\n const iosStandalone = 'standalone' in navigator && Boolean((navigator as Navigator & { standalone?: boolean }).standalone);\n return Boolean(standalone || iosStandalone);\n}\n\nexport function prefersReducedMotion(): boolean {\n if (typeof window === 'undefined') return false;\n return Boolean(window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches);\n}\n\nexport type VerificationClientSurface = 'desktop' | 'mobile-web' | 'installed-pwa';\n\nexport function clientSurface(width?: number): VerificationClientSurface {\n if (isInstalledPwa()) return 'installed-pwa';\n if (typeof window === 'undefined') return 'desktop';\n const viewport = width ?? window.innerWidth;\n if (viewport <= 640 || Boolean(window.matchMedia?.('(max-width: 640px)')?.matches)) return 'mobile-web';\n return 'desktop';\n}\n\nexport function subscribeOnlineStatus(listener: (online: boolean) => void): () => void {\n if (typeof window === 'undefined') return () => undefined;\n const onOnline = () => listener(true);\n const onOffline = () => listener(false);\n window.addEventListener('online', onOnline);\n window.addEventListener('offline', onOffline);\n return () => {\n window.removeEventListener('online', onOnline);\n window.removeEventListener('offline', onOffline);\n };\n}\n\nexport async function createQrDataUrl(\n value: string,\n renderer?: (text: string) => Promise<string>,\n): Promise<string | null> {\n if (!renderer) return null;\n try {\n return await renderer(value);\n } catch {\n return null;\n }\n}\n","import type { BrowserPlugin, BrowserPluginLoader } from './types.ts';\n\nconst loaded = new Map<string, BrowserPlugin>();\n\nexport async function loadBrowserPlugin(\n launcherKey: string,\n registry: Record<string, BrowserPlugin | BrowserPluginLoader>,\n): Promise<BrowserPlugin | null> {\n const cached = loaded.get(launcherKey);\n if (cached) return cached;\n const entry = registry[launcherKey];\n if (!entry) return null;\n const plugin = typeof entry === 'function' ? unwrap(await entry()) : entry;\n loaded.set(launcherKey, plugin);\n return plugin;\n}\n\nexport function clearPluginCache(): void {\n loaded.clear();\n}\n\nfunction unwrap(value: BrowserPlugin | { default: BrowserPlugin }): BrowserPlugin {\n return 'present' in value ? value : value.default;\n}\n","const secrets = new Map<string, { secret: string; expiresAt: number }>();\n\nfunction storageBlocked(): void {\n // Transient secrets must never enter Web Storage. This function exists so\n // tests can assert the controller never writes launch credentials.\n}\n\nexport function rememberTransientSecret(attemptId: string, secret: string, expiresAt?: string | null): void {\n storageBlocked();\n secrets.set(attemptId, {\n secret,\n expiresAt: expiresAt ? Date.parse(expiresAt) : Date.now() + 5 * 60_000,\n });\n}\n\nexport function peekTransientSecret(attemptId: string, now = Date.now()): string | undefined {\n const row = secrets.get(attemptId);\n if (!row) return undefined;\n if (row.expiresAt <= now) {\n secrets.delete(attemptId);\n return undefined;\n }\n return row.secret;\n}\n\nexport function takeTransientSecret(attemptId: string, now = Date.now()): string | undefined {\n const secret = peekTransientSecret(attemptId, now);\n secrets.delete(attemptId);\n return secret;\n}\n\nexport function forgetTransientSecret(attemptId: string): void {\n secrets.delete(attemptId);\n}\n\nexport function forgetAllTransientSecrets(): void {\n secrets.clear();\n}\n\nexport function rememberLaunchSecrets(attemptId: string, launch: { transientSecret?: string; transientSecretExpiresAt?: string } | null): void {\n if (!launch?.transientSecret) return;\n rememberTransientSecret(attemptId, launch.transientSecret, launch.transientSecretExpiresAt);\n}\n","import type {\n ProviderLaunchEnvelope,\n VerificationCanonicalStatus,\n VerificationLaunchPresentation,\n VerificationPackageCode,\n} from '@splitin/verification-adapter-sdk';\n\nexport const STATUS_CONTRACT_VERSION = '1.0.0' as const;\n\nexport interface StatusContinuation {\n key: string;\n token: string;\n expiresAt: string;\n}\n\nexport interface VerificationStatusEnvelope {\n contractVersion?: typeof STATUS_CONTRACT_VERSION | string;\n attemptId: string;\n packageCode: VerificationPackageCode;\n status: VerificationCanonicalStatus;\n presentation: VerificationLaunchPresentation;\n launch: ProviderLaunchEnvelope | null;\n launcherKey: string | null;\n providerDisclosure: string | null;\n safeErrorCode: string | null;\n retryAfter: string | null;\n supportPath: string | null;\n expiresAt: string | null;\n canResume: boolean;\n canRetry: boolean;\n continuation: StatusContinuation | null;\n}\n\nexport interface BrowserPluginHandle {\n open?(): void;\n destroy(): void;\n}\n\nexport interface BrowserPluginPresentInput {\n container: HTMLElement;\n launch: ProviderLaunchEnvelope;\n onComplete: () => void;\n onPause: () => void;\n onError: (message: string) => void;\n onOpened: () => void;\n signal?: AbortSignal;\n}\n\nexport interface BrowserPlugin {\n launcherKey: string;\n present(input: BrowserPluginPresentInput): Promise<BrowserPluginHandle>;\n}\n\nexport type BrowserPluginLoader = () => Promise<BrowserPlugin | { default: BrowserPlugin }>;\n\nexport interface VerificationWebControllerOptions {\n plugins: Record<string, BrowserPlugin | BrowserPluginLoader>;\n refreshStatus: (attemptId: string) => Promise<VerificationStatusEnvelope>;\n pause?: (attemptId: string) => Promise<VerificationStatusEnvelope | { status: VerificationCanonicalStatus }>;\n resume?: (attemptId: string) => Promise<VerificationStatusEnvelope>;\n retry?: (attemptId: string) => Promise<VerificationStatusEnvelope>;\n cancel?: (attemptId: string) => Promise<VerificationStatusEnvelope | { status: VerificationCanonicalStatus }>;\n createContinuationUrl?: (continuation: StatusContinuation, envelope: VerificationStatusEnvelope) => string;\n renderQr?: (value: string) => Promise<string>;\n now?: () => Date;\n}\n\nexport interface PresentSessionInput {\n envelope: VerificationStatusEnvelope;\n container: HTMLElement;\n onEnvelopeChange?: (envelope: VerificationStatusEnvelope) => void;\n onMessage?: (message: string) => void;\n}\n\nexport interface PresentedSession {\n destroy(): void;\n open(): void;\n refresh(): Promise<VerificationStatusEnvelope>;\n pause(): Promise<void>;\n resume(): Promise<void>;\n retry(): Promise<void>;\n cancel(): Promise<void>;\n continuationUrl: string | null;\n}\n\nexport const TERMINAL_STATUSES = new Set<VerificationCanonicalStatus>([\n 'verified',\n 'declined',\n 'failed',\n 'expired',\n 'canceled',\n 'redacted',\n]);\n","import { createQrDataUrl, isBrowserOnline, isInstalledPwa, subscribeOnlineStatus } from './environment.ts';\nimport { loadBrowserPlugin } from './plugins.ts';\nimport { forgetTransientSecret, peekTransientSecret, rememberLaunchSecrets } from './secrets.ts';\nimport {\n TERMINAL_STATUSES,\n type BrowserPluginHandle,\n type PresentedSession,\n type PresentSessionInput,\n type VerificationStatusEnvelope,\n type VerificationWebControllerOptions,\n} from './types.ts';\n\nexport function createVerificationWebController(options: VerificationWebControllerOptions) {\n const now = options.now ?? (() => new Date());\n\n async function refresh(attemptId: string): Promise<VerificationStatusEnvelope> {\n const envelope = await options.refreshStatus(attemptId);\n rememberLaunchSecrets(attemptId, envelope.launch);\n return envelope;\n }\n\n async function present(input: PresentSessionInput): Promise<PresentedSession> {\n let envelope = input.envelope;\n rememberLaunchSecrets(envelope.attemptId, envelope.launch);\n const abort = new AbortController();\n let handle: BrowserPluginHandle | null = null;\n let continuationUrl = continuationLink(envelope);\n const unsubscribe = subscribeOnlineStatus((online) => {\n if (!online) input.onMessage?.('You are offline. Your place is saved. Reconnect to resume verification.');\n else void refreshAndEmit();\n });\n\n const emit = (next: VerificationStatusEnvelope, message?: string) => {\n envelope = next;\n input.onEnvelopeChange?.(next);\n if (message) input.onMessage?.(message);\n };\n\n const refreshAndEmit = async (): Promise<VerificationStatusEnvelope> => {\n const next = await refresh(envelope.attemptId);\n const verified = next.status === 'verified';\n emit(next, verified\n ? 'Verified. Returning to your saved action.'\n : 'Your status is not approved yet. The protected action stays saved.');\n return next;\n };\n\n const presentPlugin = async (): Promise<void> => {\n handle?.destroy();\n handle = null;\n const launch = envelope.launch;\n if (!launch || envelope.presentation === 'none' || TERMINAL_STATUSES.has(envelope.status)) return;\n if (envelope.status === 'processing' || envelope.status === 'manual_review_required') return;\n if (!isBrowserOnline()) return;\n if (launch.transientSecretExpiresAt && Date.parse(launch.transientSecretExpiresAt) <= now().getTime()) {\n forgetTransientSecret(envelope.attemptId);\n input.onMessage?.('The secure launch credential expired. Checking status.');\n await refreshAndEmit();\n return;\n }\n if (envelope.presentation === 'hosted' && launch.hostedUrl) {\n handle = presentHosted(input.container, launch.hostedUrl, () => { void refreshAndEmit(); });\n return;\n }\n if (envelope.presentation === 'qr' && continuationUrl) {\n handle = await presentQr(input.container, continuationUrl, options.renderQr);\n return;\n }\n const plugin = await loadBrowserPlugin(launch.launcherKey, options.plugins);\n if (!plugin) {\n if (launch.hostedUrl) {\n handle = presentHosted(input.container, launch.hostedUrl, () => { void refreshAndEmit(); });\n }\n return;\n }\n handle = await plugin.present({\n container: input.container,\n launch: {\n ...launch,\n transientSecret: peekTransientSecret(envelope.attemptId) ?? launch.transientSecret,\n },\n signal: abort.signal,\n onComplete: () => {\n input.onMessage?.('Verification was submitted. Checking the server-side result now.');\n void refreshAndEmit();\n },\n onPause: () => { void pause(); },\n onError: (message) => input.onMessage?.(message),\n onOpened: () => undefined,\n });\n };\n\n async function pause(): Promise<void> {\n if (!options.pause) return;\n const result = await options.pause(envelope.attemptId);\n const next = 'attemptId' in result ? result : { ...envelope, status: result.status, canResume: true };\n emit(next as VerificationStatusEnvelope);\n }\n\n async function resume(): Promise<void> {\n if (!options.resume) return;\n const next = await options.resume(envelope.attemptId);\n emit(next);\n continuationUrl = continuationLink(next);\n await presentPlugin();\n }\n\n await presentPlugin();\n\n return {\n continuationUrl,\n destroy() {\n abort.abort();\n unsubscribe();\n handle?.destroy();\n forgetTransientSecret(envelope.attemptId);\n },\n open() {\n handle?.open?.();\n },\n refresh: refreshAndEmit,\n pause,\n resume,\n async retry() {\n if (!options.retry || !envelope.canRetry || !isBrowserOnline()) return;\n const next = await options.retry(envelope.attemptId);\n emit(next);\n continuationUrl = continuationLink(next);\n await presentPlugin();\n },\n async cancel() {\n if (!options.cancel) return;\n const result = await options.cancel(envelope.attemptId);\n const next = 'attemptId' in result ? result : { ...envelope, status: result.status, canResume: false, canRetry: true };\n emit(next as VerificationStatusEnvelope);\n },\n };\n }\n\n function continuationLink(envelope: VerificationStatusEnvelope): string | null {\n if (!envelope.continuation) return null;\n if (options.createContinuationUrl) return options.createContinuationUrl(envelope.continuation, envelope);\n return envelope.continuation.token;\n }\n\n return {\n refresh,\n present,\n isOnline: isBrowserOnline,\n isInstalledPwa,\n peekSecret: peekTransientSecret,\n };\n}\n\nfunction presentHosted(container: HTMLElement, url: string, onOpened: () => void): BrowserPluginHandle {\n const anchor = document.createElement('a');\n anchor.href = url;\n anchor.target = '_blank';\n anchor.rel = 'noopener noreferrer';\n anchor.textContent = 'Open hosted verification';\n anchor.addEventListener('click', onOpened);\n container.replaceChildren(anchor);\n return {\n open() { anchor.click(); },\n destroy() { container.replaceChildren(); },\n };\n}\n\nasync function presentQr(\n container: HTMLElement,\n url: string,\n renderer?: (value: string) => Promise<string>,\n): Promise<BrowserPluginHandle> {\n const image = document.createElement('img');\n image.alt = 'QR code to continue verification on another device';\n const dataUrl = await createQrDataUrl(url, renderer);\n if (dataUrl) image.src = dataUrl;\n else image.dataset.continuation = url;\n container.replaceChildren(image);\n return { destroy() { container.replaceChildren(); } };\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@splitin/verification-web",
|
|
3
|
+
"version": "0.1.0-beta.0",
|
|
4
|
+
"description": "Framework-neutral browser launcher and status controllers for verification V1 envelopes.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "SplitInTech, @ojchavan",
|
|
7
|
+
"homepage": "https://github.com/splitintech/open-internal-tools/tree/main/verification-adapter-sdk/packages/verification-web#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/splitintech/open-internal-tools.git",
|
|
11
|
+
"directory": "verification-adapter-sdk/packages/verification-web"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"sideEffects": false,
|
|
15
|
+
"engines": { "node": ">=20" },
|
|
16
|
+
"main": "./dist/index.cjs",
|
|
17
|
+
"module": "./dist/index.js",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/index.js",
|
|
23
|
+
"require": "./dist/index.cjs"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"files": ["dist", "LICENSE", "NOTICE", "README.md", "package.json"],
|
|
27
|
+
"publishConfig": { "access": "public" },
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsup",
|
|
30
|
+
"test": "vitest run",
|
|
31
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@splitin/verification-adapter-sdk": "0.1.0-beta.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"jsdom": "^26.1.0",
|
|
38
|
+
"tsup": "^8.5.0",
|
|
39
|
+
"typescript": "^5.8.3",
|
|
40
|
+
"vitest": "^3.2.4"
|
|
41
|
+
}
|
|
42
|
+
}
|