@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.
- package/README.md +255 -234
- package/lib/api.d.ts +2 -0
- package/lib/api.js +2 -0
- package/lib/client.js +2 -0
- package/lib/enckeys.d.ts +1 -1
- package/lib/enckeys.js +2 -2
- package/lib/encoding.d.ts +4 -0
- package/lib/encoding.js +10 -0
- package/lib/feature-flags.d.ts +9 -0
- package/lib/feature-flags.js +19 -0
- package/lib/index.d.ts +4 -4
- package/lib/index.js +4 -4
- package/lib/jwks.d.ts +8 -8
- package/lib/jwks.js +38 -17
- package/lib/launch/destination.d.ts +14 -0
- package/lib/launch/destination.js +89 -0
- package/lib/launch/device.d.ts +30 -0
- package/lib/launch/device.js +168 -0
- package/lib/launch/environment.d.ts +224 -0
- package/lib/launch/environment.js +98 -0
- package/lib/launch/errors.d.ts +20 -0
- package/lib/launch/errors.js +25 -0
- package/lib/launch/extension.d.ts +42 -0
- package/lib/launch/extension.js +72 -0
- package/lib/launch/flow.d.ts +82 -0
- package/lib/launch/flow.js +222 -0
- package/lib/launch/index.d.ts +16 -0
- package/lib/launch/index.js +12 -0
- package/lib/launch/mobile.d.ts +77 -0
- package/lib/launch/mobile.js +124 -0
- package/lib/launch/modal.d.ts +50 -0
- package/lib/launch/modal.js +258 -0
- package/lib/launch/options.d.ts +74 -0
- package/lib/launch/options.js +20 -0
- package/lib/launch/portal.d.ts +28 -0
- package/lib/launch/portal.js +68 -0
- package/lib/launch/qr.d.ts +5 -0
- package/lib/launch/qr.js +28 -0
- package/lib/launch/target.d.ts +46 -0
- package/lib/launch/target.js +128 -0
- package/lib/launch/vendor/qrcodegen.d.ts +134 -0
- package/lib/launch/vendor/qrcodegen.js +822 -0
- package/lib/legacy-proof.d.ts +2 -0
- package/lib/legacy-proof.js +37 -7
- package/lib/proof-hash.d.ts +5 -5
- package/lib/proof-hash.js +1 -1
- package/lib/proof-recipe.d.ts +3 -3
- package/lib/proof-recipe.js +1 -1
- package/lib/proof-types.d.ts +35 -8
- package/lib/reclaim.d.ts +81 -70
- package/lib/reclaim.js +79 -130
- package/lib/request-proof.d.ts +60 -0
- package/lib/request-proof.js +241 -0
- package/lib/result.d.ts +46 -38
- package/lib/result.js +160 -100
- package/lib/routes.gen.d.ts +76 -28
- package/lib/routes.gen.js +24 -11
- package/lib/tee-binding.d.ts +1 -1
- package/lib/types/openapi.d.ts +1 -0
- package/lib/types/openapi.gen.d.ts +1594 -488
- package/lib/types/openapi.gen.js +5 -0
- package/lib/verification-clients.d.ts +28 -14
- package/lib/verification-clients.js +31 -11
- package/lib/verify-attestor-tee.d.ts +19 -2
- package/lib/verify-attestor-tee.js +40 -4
- package/lib/verify-tee.d.ts +10 -1
- package/lib/verify-tee.js +34 -10
- package/package.json +5 -1
- package/src/api.ts +9 -0
- package/src/client.ts +2 -0
- package/src/enckeys.ts +2 -2
- package/src/encoding.ts +12 -0
- package/src/feature-flags.ts +39 -0
- package/src/index.ts +13 -14
- package/src/jwks.ts +52 -30
- package/src/launch/destination.ts +111 -0
- package/src/launch/device.ts +242 -0
- package/src/launch/environment.ts +337 -0
- package/src/launch/errors.ts +49 -0
- package/src/launch/extension.ts +124 -0
- package/src/launch/flow.ts +369 -0
- package/src/launch/index.ts +42 -0
- package/src/launch/mobile.ts +184 -0
- package/src/launch/modal.ts +378 -0
- package/src/launch/options.ts +99 -0
- package/src/launch/portal.ts +104 -0
- package/src/launch/qr.ts +37 -0
- package/src/launch/target.ts +150 -0
- package/src/launch/vendor/qrcodegen.ts +1034 -0
- package/src/proof-hash.ts +6 -6
- package/src/proof-recipe.ts +3 -3
- package/src/proof-types.ts +36 -8
- package/src/reclaim.ts +185 -218
- package/src/{legacy-proof.ts → request-proof.ts} +81 -36
- package/src/result.ts +238 -172
- package/src/routes.gen.ts +24 -11
- package/src/tee-binding.ts +1 -1
- package/src/types/openapi.gen.ts +1599 -488
- package/src/types/openapi.ts +3 -1
- package/src/verification-clients.ts +33 -19
- package/src/verify-attestor-tee.ts +66 -8
- package/src/verify-tee.ts +44 -13
- package/lib/api-client.d.ts +0 -31
- package/lib/api-client.js +0 -112
- package/lib/api-consts.d.ts +0 -4
- package/lib/api-consts.js +0 -4
- package/lib/api-errors.d.ts +0 -13
- package/lib/api-errors.js +0 -30
- package/lib/eip712.d.ts +0 -20
- 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 };
|