@reclaimprotocol/client 0.1.28 → 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,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
+ }