@reclaimprotocol/client 0.1.29 → 0.1.32
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 +1598 -485
- 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 +1603 -485
- 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,337 @@
|
|
|
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
|
+
/** A `MessageEvent`-shaped value, narrowed to what the extension handshake
|
|
29
|
+
* and popup-blocking checks read. */
|
|
30
|
+
export interface LaunchMessageEvent {
|
|
31
|
+
data: unknown
|
|
32
|
+
origin?: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface LaunchUserAgentData {
|
|
36
|
+
mobile?: boolean
|
|
37
|
+
platform?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface LaunchClipboard {
|
|
41
|
+
writeText(text: string): Promise<void>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface LaunchNavigator {
|
|
45
|
+
userAgent: string
|
|
46
|
+
maxTouchPoints?: number
|
|
47
|
+
/** Chromium's low-entropy User-Agent Client Hints. Absent on every
|
|
48
|
+
* WebKit browser (all of iOS) and on Firefox — an augmenting signal
|
|
49
|
+
* only, never the sole basis for a decision. */
|
|
50
|
+
userAgentData?: LaunchUserAgentData
|
|
51
|
+
clipboard?: LaunchClipboard
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface LaunchMediaQueryResult {
|
|
55
|
+
matches: boolean
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface LaunchCss {
|
|
59
|
+
supports?(property: string, value: string): boolean
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Only the inline-style properties `src/launch/` actually sets (the QR
|
|
63
|
+
* modal's chrome, and the hidden iframes used to probe mobile deep links).
|
|
64
|
+
* Every name here matches a real `CSSStyleDeclaration` property 1:1, so a
|
|
65
|
+
* real element's `.style` satisfies this without a cast. */
|
|
66
|
+
export interface LaunchStyleDeclaration {
|
|
67
|
+
width?: string
|
|
68
|
+
height?: string
|
|
69
|
+
border?: string
|
|
70
|
+
display?: string
|
|
71
|
+
position?: string
|
|
72
|
+
inset?: string
|
|
73
|
+
alignItems?: string
|
|
74
|
+
justifyContent?: string
|
|
75
|
+
background?: string
|
|
76
|
+
zIndex?: string
|
|
77
|
+
color?: string
|
|
78
|
+
borderRadius?: string
|
|
79
|
+
padding?: string
|
|
80
|
+
maxWidth?: string
|
|
81
|
+
textAlign?: string
|
|
82
|
+
fontFamily?: string
|
|
83
|
+
fontSize?: string
|
|
84
|
+
fontWeight?: string
|
|
85
|
+
margin?: string
|
|
86
|
+
marginBottom?: string
|
|
87
|
+
marginLeft?: string
|
|
88
|
+
marginTop?: string
|
|
89
|
+
cursor?: string
|
|
90
|
+
top?: string
|
|
91
|
+
right?: string
|
|
92
|
+
boxShadow?: string
|
|
93
|
+
boxSizing?: string
|
|
94
|
+
lineHeight?: string
|
|
95
|
+
overflow?: string
|
|
96
|
+
transition?: string
|
|
97
|
+
textDecoration?: string
|
|
98
|
+
backgroundColor?: string
|
|
99
|
+
gap?: string
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Only the one field the QR modal's backdrop handler reads, to tell a click
|
|
103
|
+
* on the backdrop from one that bubbled out of the card. */
|
|
104
|
+
export interface LaunchMouseEvent {
|
|
105
|
+
readonly target?: unknown
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Minimal surface of an element this package creates and mutates (an
|
|
110
|
+
* embedded iframe, a QR container, or the QR modal's own chrome). `src` is
|
|
111
|
+
* optional — only the iframes need it, plain containers/buttons don't have
|
|
112
|
+
* it at all — so a real `HTMLElement` (not just `HTMLIFrameElement`)
|
|
113
|
+
* satisfies this without a cast.
|
|
114
|
+
*/
|
|
115
|
+
export interface LaunchElement {
|
|
116
|
+
src?: string
|
|
117
|
+
innerHTML: string
|
|
118
|
+
readonly style: LaunchStyleDeclaration
|
|
119
|
+
setAttribute(name: string, value: string): void
|
|
120
|
+
appendChild(el: LaunchElement): void
|
|
121
|
+
addEventListener(
|
|
122
|
+
type: 'click', listener: (event?: LaunchMouseEvent) => void,
|
|
123
|
+
): void
|
|
124
|
+
removeEventListener(
|
|
125
|
+
type: 'click', listener: (event?: LaunchMouseEvent) => void,
|
|
126
|
+
): void
|
|
127
|
+
remove(): void
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** A container `launch()` embeds an iframe into — `options.target`. A real
|
|
131
|
+
* `HTMLElement` satisfies this. */
|
|
132
|
+
export interface LaunchContainer {
|
|
133
|
+
appendChild(el: LaunchElement): void
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** A window/tab this package opened or was handed pre-opened. A real
|
|
137
|
+
* `Window` returned by `window.open()` does not have `navigate()`, so
|
|
138
|
+
* `defaultEnvironment()` wraps it — see below. */
|
|
139
|
+
export interface LaunchWindowHandle {
|
|
140
|
+
readonly location: { href: string }
|
|
141
|
+
readonly closed?: boolean
|
|
142
|
+
navigate(url: string): void
|
|
143
|
+
close(): void
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface LaunchDocumentBody {
|
|
147
|
+
appendChild(el: LaunchElement): void
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Only the one field the QR modal's Escape handler reads. */
|
|
151
|
+
export interface LaunchKeyboardEvent {
|
|
152
|
+
readonly key: string
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface LaunchDocument {
|
|
156
|
+
readonly hidden?: boolean
|
|
157
|
+
createElement(tagName: string): LaunchElement
|
|
158
|
+
querySelector(selector: string): unknown
|
|
159
|
+
/** Null only in the (practically unreachable, for code that only runs
|
|
160
|
+
* after DOM APIs already exist) case that the document has no body yet. */
|
|
161
|
+
readonly body: LaunchDocumentBody | null
|
|
162
|
+
addEventListener(
|
|
163
|
+
type: 'visibilitychange',
|
|
164
|
+
listener: () => void,
|
|
165
|
+
options?: { once?: boolean },
|
|
166
|
+
): void
|
|
167
|
+
/** `keydown` is here only so the QR modal can close on Escape. */
|
|
168
|
+
addEventListener(
|
|
169
|
+
type: 'keydown',
|
|
170
|
+
listener: (event: LaunchKeyboardEvent) => void,
|
|
171
|
+
): void
|
|
172
|
+
removeEventListener(type: 'visibilitychange', listener: () => void): void
|
|
173
|
+
removeEventListener(
|
|
174
|
+
type: 'keydown',
|
|
175
|
+
listener: (event: LaunchKeyboardEvent) => void,
|
|
176
|
+
): void
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface LaunchWindow {
|
|
180
|
+
innerWidth?: number
|
|
181
|
+
innerHeight?: number
|
|
182
|
+
devicePixelRatio?: number
|
|
183
|
+
screen?: { width?: number, height?: number }
|
|
184
|
+
/** Presence (any value, including `undefined`) signals touch-event
|
|
185
|
+
* support — mirrors the reference SDK's `'ontouchstart' in window`. */
|
|
186
|
+
ontouchstart?: unknown
|
|
187
|
+
DeviceMotionEvent?: { requestPermission?(): Promise<string> }
|
|
188
|
+
DeviceOrientationEvent?: unknown
|
|
189
|
+
orientation?: unknown
|
|
190
|
+
CSS?: LaunchCss
|
|
191
|
+
matchMedia?(query: string): LaunchMediaQueryResult | null | undefined
|
|
192
|
+
addEventListener(
|
|
193
|
+
type: 'message',
|
|
194
|
+
listener: (event: LaunchMessageEvent) => void,
|
|
195
|
+
): void
|
|
196
|
+
removeEventListener(
|
|
197
|
+
type: 'message',
|
|
198
|
+
listener: (event: LaunchMessageEvent) => void,
|
|
199
|
+
): void
|
|
200
|
+
postMessage(message: unknown, targetOrigin: string): void
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export interface LaunchEnvironment {
|
|
204
|
+
window?: LaunchWindow
|
|
205
|
+
document?: LaunchDocument
|
|
206
|
+
navigator?: LaunchNavigator
|
|
207
|
+
/** Opens a URL in a new tab/window (`window.open`). Returns `null` when
|
|
208
|
+
* blocked or unavailable — the same contract as the real DOM method. */
|
|
209
|
+
open(
|
|
210
|
+
url?: string, target?: string, features?: string,
|
|
211
|
+
): LaunchWindowHandle | null
|
|
212
|
+
/** Navigates the current tab (`window.location.href = url`) — used for
|
|
213
|
+
* the mobile deep-link/App-Clip/store redirects, which happen in the
|
|
214
|
+
* claimant's own tab, not a popup. */
|
|
215
|
+
navigate(url: string): void
|
|
216
|
+
/** Real `setTimeout`/`clearTimeout` by default. Injectable so tests can
|
|
217
|
+
* fake the 500ms about:blank recovery check, the extension probe
|
|
218
|
+
* timeout, and the mobile store-fallback delays without waiting on
|
|
219
|
+
* real wall-clock timers or stubbing the global timer functions. */
|
|
220
|
+
setTimeout(handler: () => void, delayMs: number): unknown
|
|
221
|
+
clearTimeout(handle: unknown): void
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* The real global `window`'s shape, exactly as far as this file's own
|
|
226
|
+
* `open()`/`navigate()` need — a superset of `LaunchWindow` (which is what
|
|
227
|
+
* the rest of `src/launch/` sees through `LaunchEnvironment.window`) by
|
|
228
|
+
* `location`/`open`. See the file header for why this is declared here
|
|
229
|
+
* rather than obtained via `/// <reference lib="dom" />`.
|
|
230
|
+
*/
|
|
231
|
+
interface GlobalWindow extends LaunchWindow {
|
|
232
|
+
location: { href: string }
|
|
233
|
+
open(
|
|
234
|
+
url?: string, target?: string, features?: string,
|
|
235
|
+
): GlobalWindowHandle | null
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
interface GlobalWindowHandle {
|
|
239
|
+
location: { href: string }
|
|
240
|
+
closed: boolean
|
|
241
|
+
close(): void
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
declare global {
|
|
245
|
+
// `navigator` is NOT declared here: `@types/node/web-globals/navigator.d.ts`
|
|
246
|
+
// already declares a global `navigator` (a narrower ambient `Navigator` —
|
|
247
|
+
// no "dom"/"webworker" lib loaded, per the file header — with only
|
|
248
|
+
// `userAgent`/`platform`/etc), and TypeScript requires every declaration
|
|
249
|
+
// of the same ambient `var` to share one identical type. `window`/
|
|
250
|
+
// `document` aren't pre-declared anywhere in this program, so those two
|
|
251
|
+
// are safe to introduce narrowly here.
|
|
252
|
+
|
|
253
|
+
var window: GlobalWindow | undefined
|
|
254
|
+
|
|
255
|
+
var document: LaunchDocument | undefined
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Reads the browser-only extras (`userAgentData`, `maxTouchPoints`,
|
|
260
|
+
* `clipboard`) off the real `navigator` global through a loosely-typed bag
|
|
261
|
+
* rather than widening `@types/node`'s fixed ambient `Navigator` type (see
|
|
262
|
+
* the `declare global` comment above) or asserting `nav` itself to
|
|
263
|
+
* `any`/`unknown`. A real browser's `navigator` has all of these; Node's
|
|
264
|
+
* ambient type just doesn't say so.
|
|
265
|
+
*/
|
|
266
|
+
function wrapNavigator(nav: Navigator): LaunchNavigator {
|
|
267
|
+
const extras = nav as {
|
|
268
|
+
userAgentData?: LaunchUserAgentData
|
|
269
|
+
maxTouchPoints?: number
|
|
270
|
+
clipboard?: LaunchClipboard
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
get userAgent() {
|
|
275
|
+
return nav.userAgent
|
|
276
|
+
},
|
|
277
|
+
get maxTouchPoints() {
|
|
278
|
+
return extras.maxTouchPoints
|
|
279
|
+
},
|
|
280
|
+
get userAgentData() {
|
|
281
|
+
return extras.userAgentData
|
|
282
|
+
},
|
|
283
|
+
get clipboard() {
|
|
284
|
+
return extras.clipboard
|
|
285
|
+
},
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* The real browser globals, wrapped to satisfy `LaunchEnvironment`. Reads
|
|
291
|
+
* `window`/`document`/`navigator` only when called — never at module scope
|
|
292
|
+
* — so importing `src/launch/` has no effect under SSR.
|
|
293
|
+
*/
|
|
294
|
+
export function defaultEnvironment(): LaunchEnvironment {
|
|
295
|
+
const win = typeof window === 'undefined' ? undefined : window
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
window: win,
|
|
299
|
+
document: typeof document === 'undefined' ? undefined : document,
|
|
300
|
+
navigator: typeof navigator === 'undefined'
|
|
301
|
+
? undefined
|
|
302
|
+
: wrapNavigator(navigator),
|
|
303
|
+
open(url, target, features) {
|
|
304
|
+
const handle = win?.open(url, target, features)
|
|
305
|
+
if(!handle) {
|
|
306
|
+
return null
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
get location() {
|
|
311
|
+
return handle.location
|
|
312
|
+
},
|
|
313
|
+
get closed() {
|
|
314
|
+
return handle.closed
|
|
315
|
+
},
|
|
316
|
+
navigate(u) {
|
|
317
|
+
handle.location.href = u
|
|
318
|
+
},
|
|
319
|
+
close() {
|
|
320
|
+
handle.close()
|
|
321
|
+
},
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
navigate(url) {
|
|
325
|
+
if(win) {
|
|
326
|
+
win.location.href = url
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
setTimeout(handler, delayMs) {
|
|
330
|
+
return setTimeout(handler, delayMs)
|
|
331
|
+
},
|
|
332
|
+
clearTimeout(handle) {
|
|
333
|
+
// `handle` is always something this same `setTimeout` returned.
|
|
334
|
+
clearTimeout(handle as Parameters<typeof clearTimeout>[0])
|
|
335
|
+
},
|
|
336
|
+
}
|
|
337
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
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 =
|
|
15
|
+
| 'mobile-device'
|
|
16
|
+
| 'extension-disabled'
|
|
17
|
+
| 'extension-not-found'
|
|
18
|
+
|
|
19
|
+
export class ReclaimFlowUnavailableError extends Error {
|
|
20
|
+
readonly name = 'ReclaimFlowUnavailableError'
|
|
21
|
+
readonly reason: ReclaimFlowUnavailableReason
|
|
22
|
+
|
|
23
|
+
constructor(reason: ReclaimFlowUnavailableReason) {
|
|
24
|
+
super(reasonMessage(reason))
|
|
25
|
+
this.reason = reason
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function isReclaimFlowUnavailableError(
|
|
30
|
+
error: unknown,
|
|
31
|
+
): error is ReclaimFlowUnavailableError {
|
|
32
|
+
return error instanceof ReclaimFlowUnavailableError
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function reasonMessage(reason: ReclaimFlowUnavailableReason): string {
|
|
36
|
+
switch (reason) {
|
|
37
|
+
case 'mobile-device':
|
|
38
|
+
return 'This session is bound to the Reclaim browser extension, '
|
|
39
|
+
+ 'which has no mobile mechanism. Open it on a desktop browser '
|
|
40
|
+
+ 'instead.'
|
|
41
|
+
case 'extension-disabled':
|
|
42
|
+
return 'This session is bound to the Reclaim browser extension, '
|
|
43
|
+
+ 'but options.useBrowserExtension is false.'
|
|
44
|
+
case 'extension-not-found':
|
|
45
|
+
return 'This session is bound to the Reclaim browser extension, '
|
|
46
|
+
+ 'which was not found on this device. Offer the install link '
|
|
47
|
+
+ 'instead.'
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
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
|
+
|
|
19
|
+
export const RECLAIM_EXTENSION_CHECK = 'RECLAIM_EXTENSION_CHECK'
|
|
20
|
+
export const RECLAIM_EXTENSION_RESPONSE = 'RECLAIM_EXTENSION_RESPONSE'
|
|
21
|
+
export const RECLAIM_START_VERIFICATION = 'RECLAIM_START_VERIFICATION'
|
|
22
|
+
export const RECLAIM_CANCEL_VERIFICATION = 'RECLAIM_CANCEL_VERIFICATION'
|
|
23
|
+
|
|
24
|
+
export interface ExtensionHandoff {
|
|
25
|
+
sessionId: string
|
|
26
|
+
diag: boolean
|
|
27
|
+
extensionID?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
31
|
+
return typeof value === 'object' && value !== null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Probes for the extension with a short-lived `postMessage` round trip.
|
|
35
|
+
* Resolves `false` on timeout, on no `window`, or if nothing answers —
|
|
36
|
+
* never rejects. */
|
|
37
|
+
export function probeExtension(
|
|
38
|
+
env: LaunchEnvironment,
|
|
39
|
+
options: { extensionID?: string, extensionTimeout?: number } = {},
|
|
40
|
+
): Promise<boolean> {
|
|
41
|
+
const win = env.window
|
|
42
|
+
if(!win) {
|
|
43
|
+
return Promise.resolve(false)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const extensionTimeout = options.extensionTimeout ?? 200
|
|
47
|
+
const messageId = `reclaim-check-${Math.random().toString(36).slice(2)}`
|
|
48
|
+
|
|
49
|
+
return new Promise(resolve => {
|
|
50
|
+
// An arrow (not a hoisted `function`) so TypeScript's control-flow
|
|
51
|
+
// analysis keeps treating `win` as defined inside this closure — a
|
|
52
|
+
// nested function *declaration* can in principle be invoked before the
|
|
53
|
+
// enclosing `if(!win) return` runs, so TS won't narrow through one.
|
|
54
|
+
const onMessage = (event: { data: unknown }) => {
|
|
55
|
+
const data = event.data
|
|
56
|
+
if(
|
|
57
|
+
isRecord(data)
|
|
58
|
+
&& data.action === RECLAIM_EXTENSION_RESPONSE
|
|
59
|
+
&& data.messageId === messageId
|
|
60
|
+
) {
|
|
61
|
+
env.clearTimeout(timeoutHandle)
|
|
62
|
+
win.removeEventListener('message', onMessage)
|
|
63
|
+
resolve(!!data.installed)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const timeoutHandle = env.setTimeout(() => {
|
|
68
|
+
win.removeEventListener('message', onMessage)
|
|
69
|
+
resolve(false)
|
|
70
|
+
}, extensionTimeout)
|
|
71
|
+
|
|
72
|
+
win.addEventListener('message', onMessage)
|
|
73
|
+
win.postMessage(
|
|
74
|
+
{
|
|
75
|
+
action: RECLAIM_EXTENSION_CHECK,
|
|
76
|
+
extensionID: options.extensionID,
|
|
77
|
+
messageId,
|
|
78
|
+
},
|
|
79
|
+
'*',
|
|
80
|
+
)
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Hands the session off to the extension. No navigation, no return value —
|
|
85
|
+
* the extension runs the verification and the consumer's backend receives
|
|
86
|
+
* the callback (see the launch-API plan's "The page cannot poll" risk). */
|
|
87
|
+
export function sendExtensionStartVerification(
|
|
88
|
+
env: LaunchEnvironment,
|
|
89
|
+
handoff: ExtensionHandoff,
|
|
90
|
+
): void {
|
|
91
|
+
env.window?.postMessage(
|
|
92
|
+
{
|
|
93
|
+
action: RECLAIM_START_VERIFICATION,
|
|
94
|
+
messageId: handoff.sessionId,
|
|
95
|
+
extensionID: handoff.extensionID,
|
|
96
|
+
data: {
|
|
97
|
+
builder: {
|
|
98
|
+
apiVersion: '2',
|
|
99
|
+
sessionId: handoff.sessionId,
|
|
100
|
+
...(handoff.diag ? { diagnosticMode: true } : {}),
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
'*',
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Tears down the extension's in-progress verification UI. This is what
|
|
109
|
+
* `FlowHandle.close()` does for the extension-handoff mechanism — there's
|
|
110
|
+
* no tab or iframe to close instead. Does not cancel the Builder session
|
|
111
|
+
* itself (see `FlowHandle.close()`'s contract in ./flow.ts). */
|
|
112
|
+
export function sendExtensionCancelVerification(
|
|
113
|
+
env: LaunchEnvironment,
|
|
114
|
+
handoff: Pick<ExtensionHandoff, 'sessionId' | 'extensionID'>,
|
|
115
|
+
): void {
|
|
116
|
+
env.window?.postMessage(
|
|
117
|
+
{
|
|
118
|
+
action: RECLAIM_CANCEL_VERIFICATION,
|
|
119
|
+
messageId: handoff.sessionId,
|
|
120
|
+
extensionID: handoff.extensionID,
|
|
121
|
+
},
|
|
122
|
+
'*',
|
|
123
|
+
)
|
|
124
|
+
}
|