@reclaimprotocol/client 0.1.29 → 0.1.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/README.md +255 -234
  2. package/lib/api.d.ts +2 -0
  3. package/lib/api.js +2 -0
  4. package/lib/client.js +2 -0
  5. package/lib/enckeys.d.ts +1 -1
  6. package/lib/enckeys.js +2 -2
  7. package/lib/encoding.d.ts +4 -0
  8. package/lib/encoding.js +10 -0
  9. package/lib/feature-flags.d.ts +9 -0
  10. package/lib/feature-flags.js +19 -0
  11. package/lib/index.d.ts +4 -4
  12. package/lib/index.js +4 -4
  13. package/lib/jwks.d.ts +8 -8
  14. package/lib/jwks.js +38 -17
  15. package/lib/launch/destination.d.ts +14 -0
  16. package/lib/launch/destination.js +89 -0
  17. package/lib/launch/device.d.ts +30 -0
  18. package/lib/launch/device.js +168 -0
  19. package/lib/launch/environment.d.ts +224 -0
  20. package/lib/launch/environment.js +98 -0
  21. package/lib/launch/errors.d.ts +20 -0
  22. package/lib/launch/errors.js +25 -0
  23. package/lib/launch/extension.d.ts +42 -0
  24. package/lib/launch/extension.js +72 -0
  25. package/lib/launch/flow.d.ts +82 -0
  26. package/lib/launch/flow.js +222 -0
  27. package/lib/launch/index.d.ts +16 -0
  28. package/lib/launch/index.js +12 -0
  29. package/lib/launch/mobile.d.ts +77 -0
  30. package/lib/launch/mobile.js +124 -0
  31. package/lib/launch/modal.d.ts +50 -0
  32. package/lib/launch/modal.js +258 -0
  33. package/lib/launch/options.d.ts +74 -0
  34. package/lib/launch/options.js +20 -0
  35. package/lib/launch/portal.d.ts +28 -0
  36. package/lib/launch/portal.js +68 -0
  37. package/lib/launch/qr.d.ts +5 -0
  38. package/lib/launch/qr.js +28 -0
  39. package/lib/launch/target.d.ts +46 -0
  40. package/lib/launch/target.js +128 -0
  41. package/lib/launch/vendor/qrcodegen.d.ts +134 -0
  42. package/lib/launch/vendor/qrcodegen.js +822 -0
  43. package/lib/legacy-proof.d.ts +2 -0
  44. package/lib/legacy-proof.js +37 -7
  45. package/lib/proof-hash.d.ts +5 -5
  46. package/lib/proof-hash.js +1 -1
  47. package/lib/proof-recipe.d.ts +3 -3
  48. package/lib/proof-recipe.js +1 -1
  49. package/lib/proof-types.d.ts +35 -8
  50. package/lib/reclaim.d.ts +81 -70
  51. package/lib/reclaim.js +79 -130
  52. package/lib/request-proof.d.ts +60 -0
  53. package/lib/request-proof.js +241 -0
  54. package/lib/result.d.ts +46 -38
  55. package/lib/result.js +160 -100
  56. package/lib/routes.gen.d.ts +76 -28
  57. package/lib/routes.gen.js +24 -11
  58. package/lib/tee-binding.d.ts +1 -1
  59. package/lib/types/openapi.d.ts +1 -0
  60. package/lib/types/openapi.gen.d.ts +1594 -488
  61. package/lib/types/openapi.gen.js +5 -0
  62. package/lib/verification-clients.d.ts +28 -14
  63. package/lib/verification-clients.js +31 -11
  64. package/lib/verify-attestor-tee.d.ts +19 -2
  65. package/lib/verify-attestor-tee.js +40 -4
  66. package/lib/verify-tee.d.ts +10 -1
  67. package/lib/verify-tee.js +34 -10
  68. package/package.json +5 -1
  69. package/src/api.ts +9 -0
  70. package/src/client.ts +2 -0
  71. package/src/enckeys.ts +2 -2
  72. package/src/encoding.ts +12 -0
  73. package/src/feature-flags.ts +39 -0
  74. package/src/index.ts +13 -14
  75. package/src/jwks.ts +52 -30
  76. package/src/launch/destination.ts +111 -0
  77. package/src/launch/device.ts +242 -0
  78. package/src/launch/environment.ts +337 -0
  79. package/src/launch/errors.ts +49 -0
  80. package/src/launch/extension.ts +124 -0
  81. package/src/launch/flow.ts +369 -0
  82. package/src/launch/index.ts +42 -0
  83. package/src/launch/mobile.ts +184 -0
  84. package/src/launch/modal.ts +378 -0
  85. package/src/launch/options.ts +99 -0
  86. package/src/launch/portal.ts +104 -0
  87. package/src/launch/qr.ts +37 -0
  88. package/src/launch/target.ts +150 -0
  89. package/src/launch/vendor/qrcodegen.ts +1034 -0
  90. package/src/proof-hash.ts +6 -6
  91. package/src/proof-recipe.ts +3 -3
  92. package/src/proof-types.ts +36 -8
  93. package/src/reclaim.ts +185 -218
  94. package/src/{legacy-proof.ts → request-proof.ts} +81 -36
  95. package/src/result.ts +238 -172
  96. package/src/routes.gen.ts +24 -11
  97. package/src/tee-binding.ts +1 -1
  98. package/src/types/openapi.gen.ts +1599 -488
  99. package/src/types/openapi.ts +3 -1
  100. package/src/verification-clients.ts +33 -19
  101. package/src/verify-attestor-tee.ts +66 -8
  102. package/src/verify-tee.ts +44 -13
  103. package/lib/api-client.d.ts +0 -31
  104. package/lib/api-client.js +0 -112
  105. package/lib/api-consts.d.ts +0 -4
  106. package/lib/api-consts.js +0 -4
  107. package/lib/api-errors.d.ts +0 -13
  108. package/lib/api-errors.js +0 -30
  109. package/lib/eip712.d.ts +0 -20
  110. package/lib/eip712.js +0 -108
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Injectable environment port for browser globals.
3
+ *
4
+ * The reference JS SDK's `device.ts` reads `navigator`/`window` at *import*
5
+ * time, which breaks under SSR and makes device detection untestable
6
+ * without stubbing real globals — forbidden by `packages/client/AGENTS.md`,
7
+ * and this package has no DOM test library. Every function under
8
+ * `src/launch/` takes a `LaunchEnvironment` instead of touching a global
9
+ * directly, and `defaultEnvironment()` — the only place real globals are
10
+ * read — is called lazily by `ReclaimFlow.from()`, never at module scope.
11
+ *
12
+ * The port only exposes the narrow surface launch code needs, typed with
13
+ * our own interfaces rather than lib "dom" types. This file deliberately
14
+ * does NOT `/// <reference lib="dom" />` (unlike `packages/app/src/client/*`,
15
+ * which can): `packages/client`'s `tsconfig.json` compiles this whole
16
+ * package as one program, including Node-oriented files like
17
+ * `verify-tee.ts` that type `crypto.subtle` against `node:crypto`'s
18
+ * `webcrypto.SubtleCrypto`. Pulling in the "dom" lib anywhere merges its
19
+ * *global* `SubtleCrypto` (and `Response`/`Headers`/etc.) declarations into
20
+ * the same program, and the DOM lib's `SubtleCrypto` has since grown
21
+ * post-quantum KEM methods Node's typings don't — breaking that unrelated
22
+ * file. Instead, the `declare global` block below adds ambient `window`/
23
+ * `document`/`navigator` typed narrowly and exactly to what this port
24
+ * needs, so real browser globals type-check with no cast and no leakage of
25
+ * anything else "dom" would bring in.
26
+ */
27
+ /** A `MessageEvent`-shaped value, narrowed to what the extension handshake
28
+ * and popup-blocking checks read. */
29
+ export interface LaunchMessageEvent {
30
+ data: unknown;
31
+ origin?: string;
32
+ }
33
+ export interface LaunchUserAgentData {
34
+ mobile?: boolean;
35
+ platform?: string;
36
+ }
37
+ export interface LaunchClipboard {
38
+ writeText(text: string): Promise<void>;
39
+ }
40
+ export interface LaunchNavigator {
41
+ userAgent: string;
42
+ maxTouchPoints?: number;
43
+ /** Chromium's low-entropy User-Agent Client Hints. Absent on every
44
+ * WebKit browser (all of iOS) and on Firefox — an augmenting signal
45
+ * only, never the sole basis for a decision. */
46
+ userAgentData?: LaunchUserAgentData;
47
+ clipboard?: LaunchClipboard;
48
+ }
49
+ export interface LaunchMediaQueryResult {
50
+ matches: boolean;
51
+ }
52
+ export interface LaunchCss {
53
+ supports?(property: string, value: string): boolean;
54
+ }
55
+ /** Only the inline-style properties `src/launch/` actually sets (the QR
56
+ * modal's chrome, and the hidden iframes used to probe mobile deep links).
57
+ * Every name here matches a real `CSSStyleDeclaration` property 1:1, so a
58
+ * real element's `.style` satisfies this without a cast. */
59
+ export interface LaunchStyleDeclaration {
60
+ width?: string;
61
+ height?: string;
62
+ border?: string;
63
+ display?: string;
64
+ position?: string;
65
+ inset?: string;
66
+ alignItems?: string;
67
+ justifyContent?: string;
68
+ background?: string;
69
+ zIndex?: string;
70
+ color?: string;
71
+ borderRadius?: string;
72
+ padding?: string;
73
+ maxWidth?: string;
74
+ textAlign?: string;
75
+ fontFamily?: string;
76
+ fontSize?: string;
77
+ fontWeight?: string;
78
+ margin?: string;
79
+ marginBottom?: string;
80
+ marginLeft?: string;
81
+ marginTop?: string;
82
+ cursor?: string;
83
+ top?: string;
84
+ right?: string;
85
+ boxShadow?: string;
86
+ boxSizing?: string;
87
+ lineHeight?: string;
88
+ overflow?: string;
89
+ transition?: string;
90
+ textDecoration?: string;
91
+ backgroundColor?: string;
92
+ gap?: string;
93
+ }
94
+ /** Only the one field the QR modal's backdrop handler reads, to tell a click
95
+ * on the backdrop from one that bubbled out of the card. */
96
+ export interface LaunchMouseEvent {
97
+ readonly target?: unknown;
98
+ }
99
+ /**
100
+ * Minimal surface of an element this package creates and mutates (an
101
+ * embedded iframe, a QR container, or the QR modal's own chrome). `src` is
102
+ * optional — only the iframes need it, plain containers/buttons don't have
103
+ * it at all — so a real `HTMLElement` (not just `HTMLIFrameElement`)
104
+ * satisfies this without a cast.
105
+ */
106
+ export interface LaunchElement {
107
+ src?: string;
108
+ innerHTML: string;
109
+ readonly style: LaunchStyleDeclaration;
110
+ setAttribute(name: string, value: string): void;
111
+ appendChild(el: LaunchElement): void;
112
+ addEventListener(type: 'click', listener: (event?: LaunchMouseEvent) => void): void;
113
+ removeEventListener(type: 'click', listener: (event?: LaunchMouseEvent) => void): void;
114
+ remove(): void;
115
+ }
116
+ /** A container `launch()` embeds an iframe into — `options.target`. A real
117
+ * `HTMLElement` satisfies this. */
118
+ export interface LaunchContainer {
119
+ appendChild(el: LaunchElement): void;
120
+ }
121
+ /** A window/tab this package opened or was handed pre-opened. A real
122
+ * `Window` returned by `window.open()` does not have `navigate()`, so
123
+ * `defaultEnvironment()` wraps it — see below. */
124
+ export interface LaunchWindowHandle {
125
+ readonly location: {
126
+ href: string;
127
+ };
128
+ readonly closed?: boolean;
129
+ navigate(url: string): void;
130
+ close(): void;
131
+ }
132
+ export interface LaunchDocumentBody {
133
+ appendChild(el: LaunchElement): void;
134
+ }
135
+ /** Only the one field the QR modal's Escape handler reads. */
136
+ export interface LaunchKeyboardEvent {
137
+ readonly key: string;
138
+ }
139
+ export interface LaunchDocument {
140
+ readonly hidden?: boolean;
141
+ createElement(tagName: string): LaunchElement;
142
+ querySelector(selector: string): unknown;
143
+ /** Null only in the (practically unreachable, for code that only runs
144
+ * after DOM APIs already exist) case that the document has no body yet. */
145
+ readonly body: LaunchDocumentBody | null;
146
+ addEventListener(type: 'visibilitychange', listener: () => void, options?: {
147
+ once?: boolean;
148
+ }): void;
149
+ /** `keydown` is here only so the QR modal can close on Escape. */
150
+ addEventListener(type: 'keydown', listener: (event: LaunchKeyboardEvent) => void): void;
151
+ removeEventListener(type: 'visibilitychange', listener: () => void): void;
152
+ removeEventListener(type: 'keydown', listener: (event: LaunchKeyboardEvent) => void): void;
153
+ }
154
+ export interface LaunchWindow {
155
+ innerWidth?: number;
156
+ innerHeight?: number;
157
+ devicePixelRatio?: number;
158
+ screen?: {
159
+ width?: number;
160
+ height?: number;
161
+ };
162
+ /** Presence (any value, including `undefined`) signals touch-event
163
+ * support — mirrors the reference SDK's `'ontouchstart' in window`. */
164
+ ontouchstart?: unknown;
165
+ DeviceMotionEvent?: {
166
+ requestPermission?(): Promise<string>;
167
+ };
168
+ DeviceOrientationEvent?: unknown;
169
+ orientation?: unknown;
170
+ CSS?: LaunchCss;
171
+ matchMedia?(query: string): LaunchMediaQueryResult | null | undefined;
172
+ addEventListener(type: 'message', listener: (event: LaunchMessageEvent) => void): void;
173
+ removeEventListener(type: 'message', listener: (event: LaunchMessageEvent) => void): void;
174
+ postMessage(message: unknown, targetOrigin: string): void;
175
+ }
176
+ export interface LaunchEnvironment {
177
+ window?: LaunchWindow;
178
+ document?: LaunchDocument;
179
+ navigator?: LaunchNavigator;
180
+ /** Opens a URL in a new tab/window (`window.open`). Returns `null` when
181
+ * blocked or unavailable — the same contract as the real DOM method. */
182
+ open(url?: string, target?: string, features?: string): LaunchWindowHandle | null;
183
+ /** Navigates the current tab (`window.location.href = url`) — used for
184
+ * the mobile deep-link/App-Clip/store redirects, which happen in the
185
+ * claimant's own tab, not a popup. */
186
+ navigate(url: string): void;
187
+ /** Real `setTimeout`/`clearTimeout` by default. Injectable so tests can
188
+ * fake the 500ms about:blank recovery check, the extension probe
189
+ * timeout, and the mobile store-fallback delays without waiting on
190
+ * real wall-clock timers or stubbing the global timer functions. */
191
+ setTimeout(handler: () => void, delayMs: number): unknown;
192
+ clearTimeout(handle: unknown): void;
193
+ }
194
+ /**
195
+ * The real global `window`'s shape, exactly as far as this file's own
196
+ * `open()`/`navigate()` need — a superset of `LaunchWindow` (which is what
197
+ * the rest of `src/launch/` sees through `LaunchEnvironment.window`) by
198
+ * `location`/`open`. See the file header for why this is declared here
199
+ * rather than obtained via `/// <reference lib="dom" />`.
200
+ */
201
+ interface GlobalWindow extends LaunchWindow {
202
+ location: {
203
+ href: string;
204
+ };
205
+ open(url?: string, target?: string, features?: string): GlobalWindowHandle | null;
206
+ }
207
+ interface GlobalWindowHandle {
208
+ location: {
209
+ href: string;
210
+ };
211
+ closed: boolean;
212
+ close(): void;
213
+ }
214
+ declare global {
215
+ var window: GlobalWindow | undefined;
216
+ var document: LaunchDocument | undefined;
217
+ }
218
+ /**
219
+ * The real browser globals, wrapped to satisfy `LaunchEnvironment`. Reads
220
+ * `window`/`document`/`navigator` only when called — never at module scope
221
+ * — so importing `src/launch/` has no effect under SSR.
222
+ */
223
+ export declare function defaultEnvironment(): LaunchEnvironment;
224
+ export {};
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Injectable environment port for browser globals.
3
+ *
4
+ * The reference JS SDK's `device.ts` reads `navigator`/`window` at *import*
5
+ * time, which breaks under SSR and makes device detection untestable
6
+ * without stubbing real globals — forbidden by `packages/client/AGENTS.md`,
7
+ * and this package has no DOM test library. Every function under
8
+ * `src/launch/` takes a `LaunchEnvironment` instead of touching a global
9
+ * directly, and `defaultEnvironment()` — the only place real globals are
10
+ * read — is called lazily by `ReclaimFlow.from()`, never at module scope.
11
+ *
12
+ * The port only exposes the narrow surface launch code needs, typed with
13
+ * our own interfaces rather than lib "dom" types. This file deliberately
14
+ * does NOT `/// <reference lib="dom" />` (unlike `packages/app/src/client/*`,
15
+ * which can): `packages/client`'s `tsconfig.json` compiles this whole
16
+ * package as one program, including Node-oriented files like
17
+ * `verify-tee.ts` that type `crypto.subtle` against `node:crypto`'s
18
+ * `webcrypto.SubtleCrypto`. Pulling in the "dom" lib anywhere merges its
19
+ * *global* `SubtleCrypto` (and `Response`/`Headers`/etc.) declarations into
20
+ * the same program, and the DOM lib's `SubtleCrypto` has since grown
21
+ * post-quantum KEM methods Node's typings don't — breaking that unrelated
22
+ * file. Instead, the `declare global` block below adds ambient `window`/
23
+ * `document`/`navigator` typed narrowly and exactly to what this port
24
+ * needs, so real browser globals type-check with no cast and no leakage of
25
+ * anything else "dom" would bring in.
26
+ */
27
+ /**
28
+ * Reads the browser-only extras (`userAgentData`, `maxTouchPoints`,
29
+ * `clipboard`) off the real `navigator` global through a loosely-typed bag
30
+ * rather than widening `@types/node`'s fixed ambient `Navigator` type (see
31
+ * the `declare global` comment above) or asserting `nav` itself to
32
+ * `any`/`unknown`. A real browser's `navigator` has all of these; Node's
33
+ * ambient type just doesn't say so.
34
+ */
35
+ function wrapNavigator(nav) {
36
+ const extras = nav;
37
+ return {
38
+ get userAgent() {
39
+ return nav.userAgent;
40
+ },
41
+ get maxTouchPoints() {
42
+ return extras.maxTouchPoints;
43
+ },
44
+ get userAgentData() {
45
+ return extras.userAgentData;
46
+ },
47
+ get clipboard() {
48
+ return extras.clipboard;
49
+ },
50
+ };
51
+ }
52
+ /**
53
+ * The real browser globals, wrapped to satisfy `LaunchEnvironment`. Reads
54
+ * `window`/`document`/`navigator` only when called — never at module scope
55
+ * — so importing `src/launch/` has no effect under SSR.
56
+ */
57
+ export function defaultEnvironment() {
58
+ const win = typeof window === 'undefined' ? undefined : window;
59
+ return {
60
+ window: win,
61
+ document: typeof document === 'undefined' ? undefined : document,
62
+ navigator: typeof navigator === 'undefined'
63
+ ? undefined
64
+ : wrapNavigator(navigator),
65
+ open(url, target, features) {
66
+ const handle = win?.open(url, target, features);
67
+ if (!handle) {
68
+ return null;
69
+ }
70
+ return {
71
+ get location() {
72
+ return handle.location;
73
+ },
74
+ get closed() {
75
+ return handle.closed;
76
+ },
77
+ navigate(u) {
78
+ handle.location.href = u;
79
+ },
80
+ close() {
81
+ handle.close();
82
+ },
83
+ };
84
+ },
85
+ navigate(url) {
86
+ if (win) {
87
+ win.location.href = url;
88
+ }
89
+ },
90
+ setTimeout(handler, delayMs) {
91
+ return setTimeout(handler, delayMs);
92
+ },
93
+ clearTimeout(handle) {
94
+ // `handle` is always something this same `setTimeout` returned.
95
+ clearTimeout(handle);
96
+ },
97
+ };
98
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Distinguishable rejection for an extension-bound session this device can't
3
+ * run — no extension present, or opened on a phone (the extension has no
4
+ * mobile mechanism at all). `launch()` rejects with this rather than
5
+ * silently falling back to another mechanism: the session names one client,
6
+ * and switching would misattribute it (see the launch-API plan's "Launch
7
+ * mechanisms" section).
8
+ *
9
+ * A plain `Error` subclass, not a `@hapi/boom` error:
10
+ * `packages/client/AGENTS.md` requires browser-safe modules to avoid
11
+ * `node:assert` and the Boom runtime, and `src/launch/` is loaded directly
12
+ * into a browser bundle.
13
+ */
14
+ export type ReclaimFlowUnavailableReason = 'mobile-device' | 'extension-disabled' | 'extension-not-found';
15
+ export declare class ReclaimFlowUnavailableError extends Error {
16
+ readonly name = "ReclaimFlowUnavailableError";
17
+ readonly reason: ReclaimFlowUnavailableReason;
18
+ constructor(reason: ReclaimFlowUnavailableReason);
19
+ }
20
+ export declare function isReclaimFlowUnavailableError(error: unknown): error is ReclaimFlowUnavailableError;
@@ -0,0 +1,25 @@
1
+ export class ReclaimFlowUnavailableError extends Error {
2
+ constructor(reason) {
3
+ super(reasonMessage(reason));
4
+ this.name = 'ReclaimFlowUnavailableError';
5
+ this.reason = reason;
6
+ }
7
+ }
8
+ export function isReclaimFlowUnavailableError(error) {
9
+ return error instanceof ReclaimFlowUnavailableError;
10
+ }
11
+ function reasonMessage(reason) {
12
+ switch (reason) {
13
+ case 'mobile-device':
14
+ return 'This session is bound to the Reclaim browser extension, '
15
+ + 'which has no mobile mechanism. Open it on a desktop browser '
16
+ + 'instead.';
17
+ case 'extension-disabled':
18
+ return 'This session is bound to the Reclaim browser extension, '
19
+ + 'but options.useBrowserExtension is false.';
20
+ case 'extension-not-found':
21
+ return 'This session is bound to the Reclaim browser extension, '
22
+ + 'which was not found on this device. Offer the install link '
23
+ + 'instead.';
24
+ }
25
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The `reclaim-browser-extension` target's mechanism: no navigation, just a
3
+ * `window.postMessage` handshake with the content script the extension
4
+ * injects into every page. Speaks the protocol directly rather than
5
+ * depending on `@reclaimprotocol/browser-extension-sdk` (a second SDK for a
6
+ * ping and a handoff).
7
+ *
8
+ * Action constants match `reclaim-browser-extension-sdk/src/utils/constants/
9
+ * interfaces.js`'s `RECLAIM_SDK_ACTIONS`. The extension now supplies its own
10
+ * Verification Client id (`reclaim-browser-extension-sdk` commit b2f35fa —
11
+ * "the extension is always the `reclaim-browser-extension` client, so it
12
+ * supplies its own id rather than trusting the page for one"), so the
13
+ * `RECLAIM_START_VERIFICATION` payload below deliberately does NOT carry
14
+ * one: a page handing the extension a different client's id would leave a
15
+ * session recorded against a codebase that never ran it.
16
+ */
17
+ import type { LaunchEnvironment } from './environment.ts';
18
+ export declare const RECLAIM_EXTENSION_CHECK = "RECLAIM_EXTENSION_CHECK";
19
+ export declare const RECLAIM_EXTENSION_RESPONSE = "RECLAIM_EXTENSION_RESPONSE";
20
+ export declare const RECLAIM_START_VERIFICATION = "RECLAIM_START_VERIFICATION";
21
+ export declare const RECLAIM_CANCEL_VERIFICATION = "RECLAIM_CANCEL_VERIFICATION";
22
+ export interface ExtensionHandoff {
23
+ sessionId: string;
24
+ diag: boolean;
25
+ extensionID?: string;
26
+ }
27
+ /** Probes for the extension with a short-lived `postMessage` round trip.
28
+ * Resolves `false` on timeout, on no `window`, or if nothing answers —
29
+ * never rejects. */
30
+ export declare function probeExtension(env: LaunchEnvironment, options?: {
31
+ extensionID?: string;
32
+ extensionTimeout?: number;
33
+ }): Promise<boolean>;
34
+ /** Hands the session off to the extension. No navigation, no return value —
35
+ * the extension runs the verification and the consumer's backend receives
36
+ * the callback (see the launch-API plan's "The page cannot poll" risk). */
37
+ export declare function sendExtensionStartVerification(env: LaunchEnvironment, handoff: ExtensionHandoff): void;
38
+ /** Tears down the extension's in-progress verification UI. This is what
39
+ * `FlowHandle.close()` does for the extension-handoff mechanism — there's
40
+ * no tab or iframe to close instead. Does not cancel the Builder session
41
+ * itself (see `FlowHandle.close()`'s contract in ./flow.ts). */
42
+ export declare function sendExtensionCancelVerification(env: LaunchEnvironment, handoff: Pick<ExtensionHandoff, 'sessionId' | 'extensionID'>): void;
@@ -0,0 +1,72 @@
1
+ export const RECLAIM_EXTENSION_CHECK = 'RECLAIM_EXTENSION_CHECK';
2
+ export const RECLAIM_EXTENSION_RESPONSE = 'RECLAIM_EXTENSION_RESPONSE';
3
+ export const RECLAIM_START_VERIFICATION = 'RECLAIM_START_VERIFICATION';
4
+ export const RECLAIM_CANCEL_VERIFICATION = 'RECLAIM_CANCEL_VERIFICATION';
5
+ function isRecord(value) {
6
+ return typeof value === 'object' && value !== null;
7
+ }
8
+ /** Probes for the extension with a short-lived `postMessage` round trip.
9
+ * Resolves `false` on timeout, on no `window`, or if nothing answers —
10
+ * never rejects. */
11
+ export function probeExtension(env, options = {}) {
12
+ const win = env.window;
13
+ if (!win) {
14
+ return Promise.resolve(false);
15
+ }
16
+ const extensionTimeout = options.extensionTimeout ?? 200;
17
+ const messageId = `reclaim-check-${Math.random().toString(36).slice(2)}`;
18
+ return new Promise(resolve => {
19
+ // An arrow (not a hoisted `function`) so TypeScript's control-flow
20
+ // analysis keeps treating `win` as defined inside this closure — a
21
+ // nested function *declaration* can in principle be invoked before the
22
+ // enclosing `if(!win) return` runs, so TS won't narrow through one.
23
+ const onMessage = (event) => {
24
+ const data = event.data;
25
+ if (isRecord(data)
26
+ && data.action === RECLAIM_EXTENSION_RESPONSE
27
+ && data.messageId === messageId) {
28
+ env.clearTimeout(timeoutHandle);
29
+ win.removeEventListener('message', onMessage);
30
+ resolve(!!data.installed);
31
+ }
32
+ };
33
+ const timeoutHandle = env.setTimeout(() => {
34
+ win.removeEventListener('message', onMessage);
35
+ resolve(false);
36
+ }, extensionTimeout);
37
+ win.addEventListener('message', onMessage);
38
+ win.postMessage({
39
+ action: RECLAIM_EXTENSION_CHECK,
40
+ extensionID: options.extensionID,
41
+ messageId,
42
+ }, '*');
43
+ });
44
+ }
45
+ /** Hands the session off to the extension. No navigation, no return value —
46
+ * the extension runs the verification and the consumer's backend receives
47
+ * the callback (see the launch-API plan's "The page cannot poll" risk). */
48
+ export function sendExtensionStartVerification(env, handoff) {
49
+ env.window?.postMessage({
50
+ action: RECLAIM_START_VERIFICATION,
51
+ messageId: handoff.sessionId,
52
+ extensionID: handoff.extensionID,
53
+ data: {
54
+ builder: {
55
+ apiVersion: '2',
56
+ sessionId: handoff.sessionId,
57
+ ...(handoff.diag ? { diagnosticMode: true } : {}),
58
+ },
59
+ },
60
+ }, '*');
61
+ }
62
+ /** Tears down the extension's in-progress verification UI. This is what
63
+ * `FlowHandle.close()` does for the extension-handoff mechanism — there's
64
+ * no tab or iframe to close instead. Does not cancel the Builder session
65
+ * itself (see `FlowHandle.close()`'s contract in ./flow.ts). */
66
+ export function sendExtensionCancelVerification(env, handoff) {
67
+ env.window?.postMessage({
68
+ action: RECLAIM_CANCEL_VERIFICATION,
69
+ messageId: handoff.sessionId,
70
+ extensionID: handoff.extensionID,
71
+ }, '*');
72
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * `ReclaimFlow` — the public entry point. One constructor
3
+ * (`ReclaimFlow.from`), per the launch-API plan: no
4
+ * `fromSession`/`fromVerificationUrl`/`fromDescriptor` family.
5
+ *
6
+ * `resolveTarget()`/`launch()` decide the mechanism from the URL's client
7
+ * (see ./target.ts) and the device (see ./device.ts); `launch()` runs it
8
+ * (./portal.ts, ./mobile.ts, ./extension.ts, ./modal.ts) and hands back a
9
+ * `FlowHandle` that tears down whatever UI it created.
10
+ */
11
+ import type { VerificationSession } from '../types/openapi.ts';
12
+ import type { DetectionConfidence, DeviceKind } from './device.ts';
13
+ import type { LaunchElement, LaunchWindowHandle } from './environment.ts';
14
+ import type { ReclaimFlowUnavailableReason } from './errors.ts';
15
+ import type { FlowOptions } from './options.ts';
16
+ import { resolveAndroidAppDownloadUrl, resolveAndroidDeepLinkPackage } from './options.ts';
17
+ import type { VerificationTarget } from './target.ts';
18
+ export type { FlowOptions } from './options.ts';
19
+ export type { VerificationTarget } from './target.ts';
20
+ export type { DetectionConfidence, DeviceKind } from './device.ts';
21
+ export { ReclaimFlowUnavailableError, isReclaimFlowUnavailableError, } from './errors.ts';
22
+ export type { ReclaimFlowUnavailableReason } from './errors.ts';
23
+ /** A `verificationUrl` string, or any object carrying one — a full
24
+ * `VerificationSession` satisfies it. */
25
+ export type FlowInput = string | VerificationSession | {
26
+ verificationUrl: string;
27
+ };
28
+ export type LaunchMechanism = 'new-tab' | 'iframe' | 'extension-handoff' | 'qr-modal' | 'ios-app-clip' | 'ios-deep-link' | 'android-deep-link' | 'unavailable';
29
+ /** Which client and mechanism a flow resolves to on this device. Read-only —
30
+ * `resolveTarget()` changes nothing (see the plan's "Targets" section). */
31
+ export interface ResolvedTarget {
32
+ /** The client the URL names, or `undefined` for an unrecognised path
33
+ * (degrades to `mechanism: 'new-tab'`). */
34
+ client: VerificationTarget | undefined;
35
+ device: DeviceKind;
36
+ /** Exposed so a caller can render a chooser instead of trusting a
37
+ * borderline device-detection guess — see ./device.ts. */
38
+ confidence: DetectionConfidence;
39
+ mechanism: LaunchMechanism;
40
+ /** `false` only for an extension-bound session this device can't run
41
+ * (see `reason`); `launch()` rejects rather than substituting another
42
+ * mechanism. */
43
+ available: boolean;
44
+ reason?: ReclaimFlowUnavailableReason;
45
+ }
46
+ export interface FlowHandle {
47
+ /** Tears down the UI this launch created (tab/iframe/modal, or the
48
+ * extension's in-progress verification). Does not cancel the session —
49
+ * the Builder session outcome still arrives at the consumer's backend
50
+ * callback. */
51
+ close(): void;
52
+ readonly target: ResolvedTarget;
53
+ readonly iframe?: LaunchElement;
54
+ readonly tab?: LaunchWindowHandle | null;
55
+ }
56
+ export declare class ReclaimFlow {
57
+ #private;
58
+ private constructor();
59
+ /** The only constructor — no `fromSession`/`fromVerificationUrl`/
60
+ * `fromDescriptor` family. */
61
+ static from(input: FlowInput, options?: FlowOptions): ReclaimFlow;
62
+ /** The `session`/object this flow was built from, or `undefined` when
63
+ * `from()` was given a bare `verificationUrl` string. */
64
+ get session(): VerificationSession | {
65
+ verificationUrl: string;
66
+ } | undefined;
67
+ /** Merges `options` for later `getUrl()`/`resolveTarget()`/`launch()`
68
+ * calls. Chainable. */
69
+ setOptions(options: FlowOptions): this;
70
+ /** Resolves the target and returns the claimant URL, without navigating. */
71
+ getUrl(): Promise<string>;
72
+ /** Which client and mechanism this flow will use on this device. A read:
73
+ * reports the mechanism and changes nothing (the one exception is a
74
+ * short-lived `postMessage` probe for an extension-bound session, which
75
+ * navigates nothing and shows no UI). */
76
+ resolveTarget(): Promise<ResolvedTarget>;
77
+ /** Opens it. Rejects with `ReclaimFlowUnavailableError` for an
78
+ * extension-bound session this device can't run, rather than silently
79
+ * falling back to another mechanism. */
80
+ launch(): Promise<FlowHandle>;
81
+ }
82
+ export { resolveAndroidAppDownloadUrl, resolveAndroidDeepLinkPackage };