@opendatalabs/vana-sdk 4.4.0-connect-sheet.0-pr.218.7f5d342 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -176
- package/dist/direct/connect-flow.cjs +1 -211
- package/dist/direct/connect-flow.cjs.map +1 -1
- package/dist/direct/connect-flow.d.ts +0 -75
- package/dist/direct/connect-flow.js +1 -213
- package/dist/direct/connect-flow.js.map +1 -1
- package/dist/direct/controller.cjs +46 -86
- package/dist/direct/controller.cjs.map +1 -1
- package/dist/direct/controller.d.ts +30 -98
- package/dist/direct/controller.js +48 -86
- package/dist/direct/controller.js.map +1 -1
- package/dist/direct/endpoints.cjs +2 -5
- package/dist/direct/endpoints.cjs.map +1 -1
- package/dist/direct/endpoints.js +2 -5
- package/dist/direct/endpoints.js.map +1 -1
- package/dist/direct/errors.cjs +4 -0
- package/dist/direct/errors.cjs.map +1 -1
- package/dist/direct/errors.d.ts +8 -0
- package/dist/direct/errors.js +3 -0
- package/dist/direct/errors.js.map +1 -1
- package/dist/direct/personal-server-read.cjs +25 -0
- package/dist/direct/personal-server-read.cjs.map +1 -1
- package/dist/direct/personal-server-read.js +30 -1
- package/dist/direct/personal-server-read.js.map +1 -1
- package/dist/direct/test-credit.cjs +126 -0
- package/dist/direct/test-credit.cjs.map +1 -0
- package/dist/direct/test-credit.d.ts +79 -0
- package/dist/direct/test-credit.js +100 -0
- package/dist/direct/test-credit.js.map +1 -0
- package/dist/direct/types.cjs.map +1 -1
- package/dist/direct/types.d.ts +0 -6
- package/dist/direct/types.js.map +1 -1
- package/dist/direct/use-direct-vana-connect.cjs +1 -18
- package/dist/direct/use-direct-vana-connect.cjs.map +1 -1
- package/dist/direct/use-direct-vana-connect.js +1 -18
- package/dist/direct/use-direct-vana-connect.js.map +1 -1
- package/dist/index.browser.d.ts +0 -1
- package/dist/index.browser.js +0 -104
- package/dist/index.browser.js.map +4 -4
- package/dist/index.node.cjs +0 -112
- package/dist/index.node.cjs.map +3 -3
- package/dist/index.node.d.ts +0 -1
- package/dist/index.node.js +5 -105
- package/dist/index.node.js.map +4 -4
- package/dist/protocol/scopes.cjs +0 -17
- package/dist/protocol/scopes.cjs.map +1 -1
- package/dist/protocol/scopes.d.ts +0 -1
- package/dist/protocol/scopes.js +0 -18
- package/dist/protocol/scopes.js.map +1 -1
- package/dist/react.cjs +2 -10
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.ts +1 -4
- package/dist/react.js +1 -11
- package/dist/react.js.map +1 -1
- package/dist/server.cjs +11 -61
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.ts +6 -17
- package/dist/server.js +14 -68
- package/dist/server.js.map +1 -1
- package/package.json +1 -11
- package/dist/connect/account-origin.cjs +0 -72
- package/dist/connect/account-origin.cjs.map +0 -1
- package/dist/connect/account-origin.d.ts +0 -92
- package/dist/connect/account-origin.js +0 -47
- package/dist/connect/account-origin.js.map +0 -1
- package/dist/connect/chrome.cjs +0 -221
- package/dist/connect/chrome.cjs.map +0 -1
- package/dist/connect/chrome.d.ts +0 -40
- package/dist/connect/chrome.js +0 -193
- package/dist/connect/chrome.js.map +0 -1
- package/dist/connect/http-transports.cjs +0 -100
- package/dist/connect/http-transports.cjs.map +0 -1
- package/dist/connect/http-transports.d.ts +0 -77
- package/dist/connect/http-transports.js +0 -75
- package/dist/connect/http-transports.js.map +0 -1
- package/dist/connect/http-transports.test.d.ts +0 -1
- package/dist/connect/launcher.cjs +0 -224
- package/dist/connect/launcher.cjs.map +0 -1
- package/dist/connect/launcher.d.ts +0 -122
- package/dist/connect/launcher.js +0 -210
- package/dist/connect/launcher.js.map +0 -1
- package/dist/connect/launcher.test.d.ts +0 -1
- package/dist/connect/protocol.cjs +0 -244
- package/dist/connect/protocol.cjs.map +0 -1
- package/dist/connect/protocol.d.ts +0 -307
- package/dist/connect/protocol.js +0 -203
- package/dist/connect/protocol.js.map +0 -1
- package/dist/connect/protocol.test.d.ts +0 -1
- package/dist/connect.cjs +0 -55
- package/dist/connect.cjs.map +0 -1
- package/dist/connect.d.ts +0 -35
- package/dist/connect.js +0 -34
- package/dist/connect.js.map +0 -1
- package/dist/direct/connect-flow-sheet.test.d.ts +0 -1
- package/dist/direct/connect-handler.cjs +0 -493
- package/dist/direct/connect-handler.cjs.map +0 -1
- package/dist/direct/connect-handler.d.ts +0 -246
- package/dist/direct/connect-handler.js +0 -476
- package/dist/direct/connect-handler.js.map +0 -1
- package/dist/direct/connect-handler.test.d.ts +0 -1
- package/dist/direct/request-authorization.cjs +0 -65
- package/dist/direct/request-authorization.cjs.map +0 -1
- package/dist/direct/request-authorization.d.ts +0 -65
- package/dist/direct/request-authorization.js +0 -39
- package/dist/direct/request-authorization.js.map +0 -1
- package/dist/direct/request-authorization.test.d.ts +0 -1
- package/dist/direct/use-vana-connect.cjs +0 -84
- package/dist/direct/use-vana-connect.cjs.map +0 -1
- package/dist/direct/use-vana-connect.d.ts +0 -59
- package/dist/direct/use-vana-connect.js +0 -64
- package/dist/direct/use-vana-connect.js.map +0 -1
- package/dist/direct/use-vana-connect.test.d.ts +0 -1
- package/dist/protocol/fixtures/pdpp-scope-keys.json +0 -133
- package/dist/protocol/pdpp-scopes.cjs +0 -134
- package/dist/protocol/pdpp-scopes.cjs.map +0 -1
- package/dist/protocol/pdpp-scopes.d.ts +0 -123
- package/dist/protocol/pdpp-scopes.js +0 -103
- package/dist/protocol/pdpp-scopes.js.map +0 -1
- package/dist/protocol/pdpp-scopes.test.d.ts +0 -1
- /package/dist/{connect/account-origin.test.d.ts → direct/test-credit.test.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -261,49 +261,6 @@ accepts write entries in `scopes`, but its scope part must be a concrete
|
|
|
261
261
|
scope: this flow reads approved scopes back one at a time, so wildcards are
|
|
262
262
|
rejected there for read and write alike.
|
|
263
263
|
|
|
264
|
-
### PDPP grants
|
|
265
|
-
|
|
266
|
-
Ask for PDPP data as **grants**: a registry `source_id` and the streams you
|
|
267
|
-
want from it. The controller turns each stream into one scope key,
|
|
268
|
-
`pdpp.<source>.<stream>`, and that key is what the request, the signed grant
|
|
269
|
-
and your reads carry.
|
|
270
|
-
|
|
271
|
-
```typescript
|
|
272
|
-
export const vana = createDirectDataController({
|
|
273
|
-
// ...
|
|
274
|
-
source: "chatgpt",
|
|
275
|
-
grants: [
|
|
276
|
-
{
|
|
277
|
-
source_id: "https://registry.pdpp.dev/sources/chatgpt",
|
|
278
|
-
streams: ["conversations", "messages"],
|
|
279
|
-
},
|
|
280
|
-
],
|
|
281
|
-
});
|
|
282
|
-
// Requests ["pdpp.chatgpt.conversations", "pdpp.chatgpt.messages"].
|
|
283
|
-
|
|
284
|
-
await vana.readApprovedData({ requestId, scope: "pdpp.chatgpt.messages" });
|
|
285
|
-
```
|
|
286
|
-
|
|
287
|
-
- `grants` and `scopes` can be combined. The request is the union: your
|
|
288
|
-
`scopes` verbatim, then the derived keys that are not already in it.
|
|
289
|
-
Existing apps that pass only `scopes` are unchanged.
|
|
290
|
-
- The `pdpp.` namespace is separate from legacy scopes. PDPP's ChatGPT
|
|
291
|
-
`conversations` stream is `pdpp.chatgpt.conversations`, not the legacy
|
|
292
|
-
`chatgpt.conversations`, and `pdpp.chatgpt.*` covers only PDPP streams.
|
|
293
|
-
A legacy `scopes` entry under `pdpp.` that is not a valid key is rejected.
|
|
294
|
-
- The mapping is strict and never slugifies. `source_id` must be
|
|
295
|
-
`https://registry.pdpp.dev/sources/<source>`, and `<source>` and each stream
|
|
296
|
-
must be lowercase letters, digits and underscores, not starting with an
|
|
297
|
-
underscore. Anything else throws `InvalidPdppGrantError` when the controller
|
|
298
|
-
is created.
|
|
299
|
-
- `pdppScopeKey(source_id, stream)`, `parsePdppScopeKey(key)` (`{ source_id,
|
|
300
|
-
stream }` or `null`), `isPdppScopeKey(key)`, `pdppGrantsToScopes(grants)`
|
|
301
|
-
and `grantRequestScopes({ grants, scopes })` are exported from `/server`,
|
|
302
|
-
the root entry points and `@opendatalabs/vana-sdk/protocol/scopes`. Use them
|
|
303
|
-
rather than building keys by hand. Other implementations can check
|
|
304
|
-
themselves against the vectors in
|
|
305
|
-
[`src/protocol/fixtures/pdpp-scope-keys.json`](src/protocol/fixtures/pdpp-scope-keys.json).
|
|
306
|
-
|
|
307
264
|
### Backend controller
|
|
308
265
|
|
|
309
266
|
```typescript
|
|
@@ -500,139 +457,6 @@ pass the same explicit `idempotencyKey` on the retry to avoid a duplicate DCR.
|
|
|
500
457
|
`react` is an optional peer dependency. The underlying
|
|
501
458
|
`createDirectConnectFlow` store is also exported for non-React frontends.
|
|
502
459
|
|
|
503
|
-
### Connect in Vana's in-page sheet
|
|
504
|
-
|
|
505
|
-
Instead of a Vana tab, the person can approve in a sheet Vana draws over your
|
|
506
|
-
page (it signs them in, shows the request, and finishes inside the sheet). The
|
|
507
|
-
sheet is `presentation: "sheet"` of the same flow: the request, the status poll
|
|
508
|
-
and the read are unchanged. The integration is two files.
|
|
509
|
-
|
|
510
|
-
Backend: one catch-all route serves `config`, `request`, `status` and `data`,
|
|
511
|
-
binding each request to the browser that created it. The controller decides
|
|
512
|
-
what is requested (its `grants` and `scopes`, see [PDPP grants](#pdpp-grants));
|
|
513
|
-
the browser never chooses.
|
|
514
|
-
|
|
515
|
-
```ts
|
|
516
|
-
// app/api/vana/[action]/route.ts
|
|
517
|
-
import { createVanaConnectHandler } from "@opendatalabs/vana-sdk/server";
|
|
518
|
-
import { vana } from "@/lib/vana"; // your createDirectDataController({ grants, ... })
|
|
519
|
-
|
|
520
|
-
const handler = createVanaConnectHandler({
|
|
521
|
-
controller: vana,
|
|
522
|
-
returnUrl: `${process.env.APP_URL}/connect/return`,
|
|
523
|
-
clientId: "my-app", // the embed client id Vana Account registered for your origin
|
|
524
|
-
// Recommended in production: only your signed-in users create requests.
|
|
525
|
-
session: async (request) => (await getSession(request))?.userId ?? null,
|
|
526
|
-
});
|
|
527
|
-
export { handler as GET, handler as POST };
|
|
528
|
-
```
|
|
529
|
-
|
|
530
|
-
The handler reports the Vana Account origin from the controller's `env`
|
|
531
|
-
(`account.vana.org` for `production`, `account-dev.vana.org` for `dev`), so a
|
|
532
|
-
dev app never frames production.
|
|
533
|
-
|
|
534
|
-
Frontend:
|
|
535
|
-
|
|
536
|
-
```tsx
|
|
537
|
-
"use client";
|
|
538
|
-
import { useVanaConnect } from "@opendatalabs/vana-sdk/react";
|
|
539
|
-
|
|
540
|
-
export function ConnectButton() {
|
|
541
|
-
const { state, start } = useVanaConnect<MyData>({ endpoint: "/api/vana" });
|
|
542
|
-
if (state.type === "done") return <Result data={state.result.data} />;
|
|
543
|
-
return (
|
|
544
|
-
<>
|
|
545
|
-
<button
|
|
546
|
-
disabled={
|
|
547
|
-
state.type === "creating" || state.type === "awaiting_approval"
|
|
548
|
-
}
|
|
549
|
-
onClick={start}
|
|
550
|
-
>
|
|
551
|
-
Connect with Vana
|
|
552
|
-
</button>
|
|
553
|
-
{state.type === "awaiting_approval" && state.popupBlocked && (
|
|
554
|
-
// The sheet could not run here (state.sheetUnavailable says why).
|
|
555
|
-
<a href={state.request.approvalUrl} target="_blank" rel="noreferrer">
|
|
556
|
-
Open Vana
|
|
557
|
-
</a>
|
|
558
|
-
)}
|
|
559
|
-
</>
|
|
560
|
-
);
|
|
561
|
-
}
|
|
562
|
-
```
|
|
563
|
-
|
|
564
|
-
- While the sheet is up, `state.sheet` reports `completion` (`in_browser` or
|
|
565
|
-
`remote_collect`) once the person approves, coarse `progress`
|
|
566
|
-
(`collecting`, `writing`), and any `error` it is showing.
|
|
567
|
-
- **Approval is not delivery.** The sheet's `connected` event (and
|
|
568
|
-
`state.sheet.completion`) means the person approved; the data may still be
|
|
569
|
-
collecting. The flow reaches `done` only when your backend's status poll
|
|
570
|
-
says the request is readable and the read succeeds, and `error` when the
|
|
571
|
-
poll reports `denied`, `expired` or `completed` or the read fails. The sheet
|
|
572
|
-
sends no terminal event of its own; its events only make the flow poll
|
|
573
|
-
sooner.
|
|
574
|
-
- Closing the sheet before approving returns to `idle`. Closing it after
|
|
575
|
-
approving keeps polling, since the data may still be readable.
|
|
576
|
-
- If the frame cannot run (`frame_blocked`, `origin_not_registered`,
|
|
577
|
-
`incompatible_version`), the flow falls back to the tab presentation with
|
|
578
|
-
`popupBlocked: true`, so the "Open approval" link above still finishes it.
|
|
579
|
-
`origin_not_registered` arrives at once: the launcher asks Vana
|
|
580
|
-
(`GET <account>/api/embed/connect/client`, no credentials) alongside
|
|
581
|
-
framing. `vana_unavailable` means Vana's own side is down or misconfigured;
|
|
582
|
-
the sheet stays open and says so.
|
|
583
|
-
- The partner reads; it cannot write through the sheet. Pass
|
|
584
|
-
`sheet: { closeAfterDoneMs: null }` to leave the sheet open after the read.
|
|
585
|
-
- If your page sets a CSP, allow the frame and the client check:
|
|
586
|
-
`vanaConnectCsp(env)` from `/server` (or `/connect/protocol`) returns
|
|
587
|
-
`{ "frame-src": [accountOrigin], "connect-src": [accountOrigin] }`. It has
|
|
588
|
-
no default environment.
|
|
589
|
-
- `createVanaConnectHandler` takes `session` to bind requests to your own
|
|
590
|
-
signed-in user (recommended: without it anyone who loads your page can
|
|
591
|
-
create requests as your app), `createOptions` for per-request create input
|
|
592
|
-
(questions, owner), and `read` to replace the default `readApprovedData`.
|
|
593
|
-
Use `toNodeHandler(handler)` for Express or plain `http`.
|
|
594
|
-
- Creating a request is rate limited. The default, `createMemoryRateLimit()`,
|
|
595
|
-
allows 10 creates a minute per client IP and per session, in memory. It is
|
|
596
|
-
not production-grade (each instance counts alone, and the IP comes from
|
|
597
|
-
proxy headers): pass `rateLimit` backed by a shared store, or `false` when
|
|
598
|
-
a limiter already sits in front of the route. A refusal is
|
|
599
|
-
`429 rate_limited` with `Retry-After`.
|
|
600
|
-
|
|
601
|
-
Choosing the Vana Account origin yourself (a custom flow, or preview deploys
|
|
602
|
-
of Vana Account) goes through `resolveVanaAccountOrigin`; never match origins
|
|
603
|
-
with your own pattern. It returns the environment's origin unless overrides
|
|
604
|
-
are switched on for this build and the override is exactly an `https` origin
|
|
605
|
-
on your allowlist:
|
|
606
|
-
|
|
607
|
-
```ts
|
|
608
|
-
import {
|
|
609
|
-
parseOriginAllowlist,
|
|
610
|
-
resolveVanaAccountOrigin,
|
|
611
|
-
} from "@opendatalabs/vana-sdk/connect";
|
|
612
|
-
|
|
613
|
-
const accountOrigin = resolveVanaAccountOrigin({
|
|
614
|
-
env: "dev",
|
|
615
|
-
override: new URLSearchParams(location.search).get("vana_account"),
|
|
616
|
-
previewOrigins: parseOriginAllowlist(
|
|
617
|
-
process.env.NEXT_PUBLIC_VANA_ACCOUNT_PREVIEW_ORIGINS,
|
|
618
|
-
),
|
|
619
|
-
// From build-time config, never the request. Off in production.
|
|
620
|
-
allowOverride: process.env.NEXT_PUBLIC_DEPLOY_ENV === "preview",
|
|
621
|
-
});
|
|
622
|
-
```
|
|
623
|
-
|
|
624
|
-
`createDirectConnectFlow` and the launcher take that origin (or an `env`)
|
|
625
|
-
explicitly; neither falls back to production.
|
|
626
|
-
|
|
627
|
-
Lower-level pieces: `createDirectConnectFlow(transports, { presentation:
|
|
628
|
-
"sheet", sheet: { clientId, appName, accountOrigin } })` with an
|
|
629
|
-
`authorizeRequest` transport backed by
|
|
630
|
-
`controller.authorizeRequestRead(requestId)`; the launcher itself
|
|
631
|
-
(`@opendatalabs/vana-sdk/connect`); and the zero-dependency message protocol
|
|
632
|
-
(`@opendatalabs/vana-sdk/connect/protocol`). A backend in another language
|
|
633
|
-
implements the [connect wire contract](./src/connect/WIRE_CONTRACT.md), which has test
|
|
634
|
-
vectors.
|
|
635
|
-
|
|
636
460
|
### Test with large sample data
|
|
637
461
|
|
|
638
462
|
When testing with realistic exports, use the public fixture catalog in
|
|
@@ -22,13 +22,6 @@ __export(connect_flow_exports, {
|
|
|
22
22
|
});
|
|
23
23
|
module.exports = __toCommonJS(connect_flow_exports);
|
|
24
24
|
var import_types = require("./types");
|
|
25
|
-
var import_launcher = require("../connect/launcher");
|
|
26
|
-
const SHEET_FALLBACK_CODES = [
|
|
27
|
-
"frame_blocked",
|
|
28
|
-
"origin_not_registered",
|
|
29
|
-
"incompatible_version"
|
|
30
|
-
];
|
|
31
|
-
const DEFAULT_CLOSE_AFTER_DONE_MS = 1500;
|
|
32
25
|
const DEFAULT_POLL_INTERVAL_MS = 1500;
|
|
33
26
|
const DEFAULT_TIMEOUT_MS = 3e5;
|
|
34
27
|
function toError(value) {
|
|
@@ -84,18 +77,10 @@ function createDirectConnectFlow(transports, options = {}) {
|
|
|
84
77
|
let state = { type: "idle" };
|
|
85
78
|
const listeners = /* @__PURE__ */ new Set();
|
|
86
79
|
let pollHandle = null;
|
|
87
|
-
let pollTarget = null;
|
|
88
|
-
let pollInFlight = false;
|
|
89
|
-
let pollNudged = false;
|
|
90
80
|
let running = false;
|
|
91
81
|
let approvedRequest = null;
|
|
92
82
|
let activeRunId = 0;
|
|
93
83
|
let openedWindow = null;
|
|
94
|
-
let sheetHandle = null;
|
|
95
|
-
let sheetState = null;
|
|
96
|
-
let sheetUnavailable = null;
|
|
97
|
-
let sheetCloseAfterDoneMs = DEFAULT_CLOSE_AFTER_DONE_MS;
|
|
98
|
-
let sheetCloseTimer = null;
|
|
99
84
|
function emit() {
|
|
100
85
|
for (const listener of listeners) listener();
|
|
101
86
|
}
|
|
@@ -108,19 +93,6 @@ function createDirectConnectFlow(transports, options = {}) {
|
|
|
108
93
|
clearTimeoutFn(pollHandle);
|
|
109
94
|
pollHandle = null;
|
|
110
95
|
}
|
|
111
|
-
pollTarget = null;
|
|
112
|
-
pollNudged = false;
|
|
113
|
-
}
|
|
114
|
-
function nudgePoll() {
|
|
115
|
-
if (!running) return;
|
|
116
|
-
if (pollInFlight) {
|
|
117
|
-
pollNudged = true;
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
const target = pollTarget;
|
|
121
|
-
if (pollHandle === null || !target) return;
|
|
122
|
-
clearPoll();
|
|
123
|
-
void poll(target.request, target.deadline);
|
|
124
96
|
}
|
|
125
97
|
function closeUnnavigatedWindow() {
|
|
126
98
|
if (openedWindow) {
|
|
@@ -131,114 +103,12 @@ function createDirectConnectFlow(transports, options = {}) {
|
|
|
131
103
|
function isRunningPhase() {
|
|
132
104
|
return state.type === "creating" || state.type === "awaiting_approval" || state.type === "ready_to_open" || state.type === "reading";
|
|
133
105
|
}
|
|
134
|
-
function closeSheet() {
|
|
135
|
-
const handle = sheetHandle;
|
|
136
|
-
sheetHandle = null;
|
|
137
|
-
handle?.close();
|
|
138
|
-
}
|
|
139
|
-
function clearSheet() {
|
|
140
|
-
if (sheetCloseTimer !== null) {
|
|
141
|
-
clearTimeoutFn(sheetCloseTimer);
|
|
142
|
-
sheetCloseTimer = null;
|
|
143
|
-
}
|
|
144
|
-
closeSheet();
|
|
145
|
-
sheetState = null;
|
|
146
|
-
sheetUnavailable = null;
|
|
147
|
-
}
|
|
148
|
-
function awaitingState(request) {
|
|
149
|
-
if (sheetUnavailable) {
|
|
150
|
-
return {
|
|
151
|
-
type: "awaiting_approval",
|
|
152
|
-
request,
|
|
153
|
-
popupBlocked: true,
|
|
154
|
-
sheetUnavailable
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
return {
|
|
158
|
-
type: "awaiting_approval",
|
|
159
|
-
request,
|
|
160
|
-
popupBlocked: false,
|
|
161
|
-
...sheetState ? { sheet: { ...sheetState } } : {}
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
function updateSheet(patch) {
|
|
165
|
-
if (!sheetState) return;
|
|
166
|
-
sheetState = { ...sheetState, ...patch };
|
|
167
|
-
if (state.type === "awaiting_approval" && state.sheet) {
|
|
168
|
-
setState(awaitingState(state.request));
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
function failRun(error) {
|
|
172
|
-
running = false;
|
|
173
|
-
clearPoll();
|
|
174
|
-
setState({ type: "error", error });
|
|
175
|
-
}
|
|
176
|
-
function handleSheetEvent(event) {
|
|
177
|
-
switch (event.type) {
|
|
178
|
-
case "connected":
|
|
179
|
-
updateSheet({ completion: event.completion });
|
|
180
|
-
nudgePoll();
|
|
181
|
-
return;
|
|
182
|
-
case "progress":
|
|
183
|
-
updateSheet({ progress: event.phase });
|
|
184
|
-
return;
|
|
185
|
-
case "error": {
|
|
186
|
-
if (event.code === "request_failed") {
|
|
187
|
-
if (!running) return;
|
|
188
|
-
closeSheet();
|
|
189
|
-
failRun(new Error("Could not prepare the request for Vana's sheet"));
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
if (SHEET_FALLBACK_CODES.includes(event.code)) {
|
|
193
|
-
sheetUnavailable = event.code;
|
|
194
|
-
sheetState = null;
|
|
195
|
-
closeSheet();
|
|
196
|
-
if (state.type === "awaiting_approval") {
|
|
197
|
-
setState(awaitingState(state.request));
|
|
198
|
-
}
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
updateSheet({ error: event.code });
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
case "close": {
|
|
205
|
-
if (!sheetHandle) return;
|
|
206
|
-
sheetHandle = null;
|
|
207
|
-
if (!running) return;
|
|
208
|
-
if (sheetState?.error) {
|
|
209
|
-
failRun(new Error(`Vana's sheet failed: ${sheetState.error}`));
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
if (!sheetState?.completion) {
|
|
213
|
-
running = false;
|
|
214
|
-
activeRunId++;
|
|
215
|
-
clearPoll();
|
|
216
|
-
sheetState = null;
|
|
217
|
-
setState({ type: "idle" });
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
updateSheet({ open: false });
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
default:
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
106
|
async function readAndFinish(request) {
|
|
228
107
|
setState({ type: "reading", request });
|
|
229
108
|
try {
|
|
230
109
|
const result = await transports.readResult(request.requestId);
|
|
231
110
|
if (!running) return;
|
|
232
111
|
setState({ type: "done", result });
|
|
233
|
-
if (sheetHandle) {
|
|
234
|
-
sheetHandle.setRequestStatus("completed");
|
|
235
|
-
if (sheetCloseAfterDoneMs !== null) {
|
|
236
|
-
sheetCloseTimer = setTimeoutFn(() => {
|
|
237
|
-
sheetCloseTimer = null;
|
|
238
|
-
closeSheet();
|
|
239
|
-
}, sheetCloseAfterDoneMs);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
112
|
} catch (err) {
|
|
243
113
|
if (!running) return;
|
|
244
114
|
setState({ type: "error", error: toError(err) });
|
|
@@ -247,13 +117,9 @@ function createDirectConnectFlow(transports, options = {}) {
|
|
|
247
117
|
}
|
|
248
118
|
}
|
|
249
119
|
function scheduleNextPoll(request, deadline) {
|
|
250
|
-
const delay = pollNudged ? 0 : pollIntervalMs;
|
|
251
|
-
pollNudged = false;
|
|
252
|
-
pollTarget = { request, deadline };
|
|
253
120
|
pollHandle = setTimeoutFn(() => {
|
|
254
|
-
pollHandle = null;
|
|
255
121
|
void poll(request, deadline);
|
|
256
|
-
},
|
|
122
|
+
}, pollIntervalMs);
|
|
257
123
|
}
|
|
258
124
|
function requestDeadline(request) {
|
|
259
125
|
if (request.expiresAt !== void 0) {
|
|
@@ -263,7 +129,6 @@ function createDirectConnectFlow(transports, options = {}) {
|
|
|
263
129
|
return now() + timeoutMs;
|
|
264
130
|
}
|
|
265
131
|
function startPolling(request, initialState) {
|
|
266
|
-
pollNudged = false;
|
|
267
132
|
setState(initialState);
|
|
268
133
|
const deadline = requestDeadline(request);
|
|
269
134
|
if (now() >= deadline) {
|
|
@@ -287,7 +152,6 @@ function createDirectConnectFlow(transports, options = {}) {
|
|
|
287
152
|
return;
|
|
288
153
|
}
|
|
289
154
|
let status;
|
|
290
|
-
pollInFlight = true;
|
|
291
155
|
try {
|
|
292
156
|
status = await transports.getStatus(request.requestId);
|
|
293
157
|
} catch (err) {
|
|
@@ -295,8 +159,6 @@ function createDirectConnectFlow(transports, options = {}) {
|
|
|
295
159
|
running = false;
|
|
296
160
|
setState({ type: "error", error: toError(err) });
|
|
297
161
|
return;
|
|
298
|
-
} finally {
|
|
299
|
-
pollInFlight = false;
|
|
300
162
|
}
|
|
301
163
|
if (!running) return;
|
|
302
164
|
if (status.status === "pending" && state.type === "ready_to_open") {
|
|
@@ -319,7 +181,6 @@ function createDirectConnectFlow(transports, options = {}) {
|
|
|
319
181
|
return;
|
|
320
182
|
}
|
|
321
183
|
if (status.status === "completed" || status.status === "denied" || status.status === "expired") {
|
|
322
|
-
sheetHandle?.setRequestStatus(status.status);
|
|
323
184
|
running = false;
|
|
324
185
|
setState({
|
|
325
186
|
type: "error",
|
|
@@ -342,11 +203,6 @@ function createDirectConnectFlow(transports, options = {}) {
|
|
|
342
203
|
running = true;
|
|
343
204
|
approvedRequest = null;
|
|
344
205
|
const runId = ++activeRunId;
|
|
345
|
-
clearSheet();
|
|
346
|
-
if (options.presentation === "sheet") {
|
|
347
|
-
await startSheet(runId);
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
206
|
const browserPlatform = (options.browserPlatformPolicy ?? defaultBrowserPlatformPolicy()).current();
|
|
351
207
|
const approvalWindow = browserPlatform === "desktop" ? (options.openApprovalWindow ?? defaultOpenApprovalWindow)() : null;
|
|
352
208
|
openedWindow = approvalWindow;
|
|
@@ -436,75 +292,9 @@ function createDirectConnectFlow(transports, options = {}) {
|
|
|
436
292
|
activeRunId++;
|
|
437
293
|
clearPoll();
|
|
438
294
|
closeUnnavigatedWindow();
|
|
439
|
-
clearSheet();
|
|
440
295
|
setState({ type: "idle" });
|
|
441
296
|
}
|
|
442
297
|
};
|
|
443
|
-
async function startSheet(runId) {
|
|
444
|
-
setState({ type: "creating" });
|
|
445
|
-
const fail = (error) => {
|
|
446
|
-
if (runId !== activeRunId) return;
|
|
447
|
-
clearSheet();
|
|
448
|
-
failRun(toError(error));
|
|
449
|
-
};
|
|
450
|
-
const authorizeRequest = transports.authorizeRequest;
|
|
451
|
-
if (!authorizeRequest) {
|
|
452
|
-
fail(
|
|
453
|
-
new Error('presentation: "sheet" needs the authorizeRequest transport')
|
|
454
|
-
);
|
|
455
|
-
return;
|
|
456
|
-
}
|
|
457
|
-
let sheetOptions;
|
|
458
|
-
try {
|
|
459
|
-
const configured = options.sheet;
|
|
460
|
-
if (!configured) {
|
|
461
|
-
throw new Error('presentation: "sheet" needs sheet options');
|
|
462
|
-
}
|
|
463
|
-
sheetOptions = typeof configured === "function" ? await configured() : configured;
|
|
464
|
-
} catch (err) {
|
|
465
|
-
fail(err);
|
|
466
|
-
return;
|
|
467
|
-
}
|
|
468
|
-
if (runId !== activeRunId) return;
|
|
469
|
-
const created = transports.createRequest();
|
|
470
|
-
created.catch(() => void 0);
|
|
471
|
-
sheetCloseAfterDoneMs = sheetOptions.closeAfterDoneMs === void 0 ? DEFAULT_CLOSE_AFTER_DONE_MS : sheetOptions.closeAfterDoneMs;
|
|
472
|
-
sheetState = { open: true };
|
|
473
|
-
try {
|
|
474
|
-
sheetHandle = (sheetOptions.open ?? import_launcher.openConnectSheet)({
|
|
475
|
-
clientId: sheetOptions.clientId,
|
|
476
|
-
appName: sheetOptions.appName,
|
|
477
|
-
vanaOrigin: sheetOptions.accountOrigin,
|
|
478
|
-
container: sheetOptions.container,
|
|
479
|
-
readyTimeoutMs: sheetOptions.readyTimeoutMs,
|
|
480
|
-
async getRequest() {
|
|
481
|
-
const request2 = await created;
|
|
482
|
-
return {
|
|
483
|
-
id: request2.requestId,
|
|
484
|
-
authorization: await authorizeRequest(request2.requestId)
|
|
485
|
-
};
|
|
486
|
-
},
|
|
487
|
-
onEvent(event) {
|
|
488
|
-
if (runId !== activeRunId) return;
|
|
489
|
-
sheetOptions.onEvent?.(event);
|
|
490
|
-
handleSheetEvent(event);
|
|
491
|
-
}
|
|
492
|
-
});
|
|
493
|
-
} catch {
|
|
494
|
-
sheetHandle = null;
|
|
495
|
-
sheetState = null;
|
|
496
|
-
sheetUnavailable = "sheet_failed";
|
|
497
|
-
}
|
|
498
|
-
let request;
|
|
499
|
-
try {
|
|
500
|
-
request = await created;
|
|
501
|
-
} catch (err) {
|
|
502
|
-
fail(err);
|
|
503
|
-
return;
|
|
504
|
-
}
|
|
505
|
-
if (runId !== activeRunId || !running) return;
|
|
506
|
-
startPolling(request, awaitingState(request));
|
|
507
|
-
}
|
|
508
298
|
return flow;
|
|
509
299
|
}
|
|
510
300
|
// Annotate the CommonJS export names for ESM import in node:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/direct/connect-flow.ts"],"sourcesContent":["/**\n * Framework-agnostic connect-flow state machine for the browser two-tab helper.\n *\n * @remarks\n * This is the testable core behind {@link useDirectVanaConnect}. It is pure\n * TypeScript (no React, no DOM-only APIs beyond an injectable window opener and\n * timers) so the full flow — create request, open Vana, poll status, read data —\n * can be exercised in a Node test environment.\n *\n * The React hook is a thin `useSyncExternalStore` binding over this store.\n *\n * @category Direct\n * @module direct/connect-flow\n */\n\nimport type {\n AccessRequest,\n AccessRequestStatus,\n AccessRequestStatusValue,\n ApprovedDataResult,\n} from \"./types\";\nimport { normalizeMobileContinuationUrl } from \"./types\";\nimport {\n openConnectSheet,\n type ConnectEvent,\n type ConnectHandle,\n type ConnectOpenOptions,\n} from \"../connect/launcher\";\nimport type {\n ConnectCompletion,\n ConnectProgressPhase,\n RequestAuthorization,\n SheetErrorCode,\n} from \"../connect/protocol\";\n\n/**\n * Caller-supplied transports. These typically `fetch` the app's own backend\n * routes, which in turn delegate to a {@link DirectDataController}.\n */\nexport interface DirectConnectTransports<T = unknown> {\n /** Ask the backend to create an access request. */\n createRequest: () => Promise<AccessRequest>;\n /** Ask the backend for the current status of a request. */\n getStatus: (requestId: string) => Promise<AccessRequestStatus>;\n /** Ask the backend to read the approved data. */\n readResult: (requestId: string) => Promise<ApprovedDataResult<T>>;\n /**\n * Ask the backend to sign the sheet's short-lived read capability for a\n * request it created for this browser (`controller.authorizeRequestRead`).\n * Required for `presentation: \"sheet\"`; called each time the sheet asks.\n */\n authorizeRequest?: (requestId: string) => Promise<RequestAuthorization>;\n}\n\n/** How the person approves: a Vana tab (default) or Vana's in-page sheet. */\nexport type DirectConnectPresentation = \"tab\" | \"sheet\";\n\n/** Sheet settings for `presentation: \"sheet\"`. */\nexport interface DirectConnectSheetOptions {\n /** The embed client id Vana Account registered for this page's origin. */\n clientId: string;\n /** Label on the sheet chrome (Vana shows the registered name inside). */\n appName: string;\n /**\n * Vana Account origin that serves the sheet. Required: there is no\n * production default, so a dev app never silently talks to production.\n * Use `VANA_ACCOUNT_ORIGINS[env]`, or `resolveVanaAccountOrigin` for\n * preview deploys. `useVanaConnect` fills it from the backend's config.\n */\n accountOrigin: string;\n /** Element the sheet mounts in. Defaults to `document.body`. */\n container?: HTMLElement;\n /** How long to wait for the sheet to start before falling back. */\n readyTimeoutMs?: number;\n /**\n * Close the sheet this long (ms) after the data is read, so the person sees\n * it finish. Defaults to 1500; `null` leaves it open for the person to close.\n */\n closeAfterDoneMs?: number | null;\n /** Every raw launcher event, for logging or custom UI. */\n onEvent?: (event: ConnectEvent) => void;\n /** Launcher. Defaults to the SDK's `openConnectSheet`; injectable for tests. */\n open?: (options: ConnectOpenOptions) => ConnectHandle;\n}\n\n/**\n * What the sheet has reported, while it presents approval.\n *\n * @remarks\n * These are hints for the UI, not outcomes. The flow settles only from the\n * backend: `done` once the status poll says the request is readable and the\n * read succeeds, `error` when the poll reports `denied`, `expired` or\n * `completed`, or the read fails.\n */\nexport interface DirectConnectSheetState {\n /** `false` once the person closed the sheet after approving. */\n open: boolean;\n /**\n * Set once the person approved: how the data will arrive. Approval is not\n * delivery; wait for `state.type === \"done\"` before treating data as\n * delivered.\n */\n completion?: ConnectCompletion;\n /** Coarse `remote_collect` progress. */\n progress?: ConnectProgressPhase;\n /** A failure the sheet is showing; it stays open for the person to read. */\n error?: SheetErrorCode;\n}\n\n/**\n * Sheet errors after which the flow closes the sheet and falls back to the\n * tab presentation (`popupBlocked: true`, so the UI shows the approval link).\n */\nconst SHEET_FALLBACK_CODES: readonly SheetErrorCode[] = [\n \"frame_blocked\",\n \"origin_not_registered\",\n \"incompatible_version\",\n];\n\nconst DEFAULT_CLOSE_AFTER_DONE_MS = 1500;\n\n/**\n * A handle to a tab opened synchronously under the user's click gesture.\n *\n * @remarks\n * The flow opens this tab *before* it knows the approval URL (popup blockers\n * only allow `window.open()` during the click's transient activation), then\n * navigates it once `createRequest` resolves.\n */\nexport interface ConnectWindow {\n /** Point the already-open tab at the approval URL. */\n navigate(url: string): void;\n /** Close the tab (used to clean up an un-navigated tab on failure/reset). */\n close(): void;\n}\n\n/** Browser class used only to choose the destination returned by Vana. */\nexport type DirectBrowserPlatform = \"desktop\" | \"mobile\";\n\n/** Injectable browser-platform policy; it never asserts whether an app exists. */\nexport interface DirectBrowserPlatformPolicy {\n current(): DirectBrowserPlatform;\n}\n\n/** Tunables for the connect flow. */\nexport interface DirectConnectOptions {\n /** Status poll interval in ms. Defaults to 1500. */\n pollIntervalMs?: number;\n /**\n * Overall timeout in ms before giving up. Defaults to 300000 (5 min).\n * Used only when the access request does not carry an authoritative\n * `expiresAt` value.\n */\n timeoutMs?: number;\n /**\n * Synchronously open a blank tab under the click's transient activation and\n * return a handle to navigate later, or `null` if the browser blocked it.\n * Defaults to `window.open(\"\", \"_blank\")` (with `opener` severed). Injectable\n * for tests.\n *\n * @remarks\n * Renamed from the pre-3.8 `openWindow?: (url) => void`. The old contract was\n * the BUI-622 bug itself (it was called with the URL *after* an `await`, so\n * the popup blocker suppressed it); it cannot be preserved while fixing the\n * bug. Custom openers must now open synchronously and return a navigable\n * handle.\n */\n openApprovalWindow?: () => ConnectWindow | null;\n /** SDK-owned mobile/desktop policy. Injectable for deterministic tests. */\n browserPlatformPolicy?: DirectBrowserPlatformPolicy;\n /** `setTimeout`. Injectable for tests. Defaults to `globalThis.setTimeout`. */\n setTimeoutFn?: (cb: () => void, ms: number) => unknown;\n /**\n * `\"sheet\"` presents approval in Vana's in-page sheet instead of a tab;\n * needs {@link DirectConnectOptions.sheet} and the `authorizeRequest`\n * transport. Defaults to `\"tab\"`.\n */\n presentation?: DirectConnectPresentation;\n /** Sheet settings, or a loader for them (fetched at `start()`). */\n sheet?:\n | DirectConnectSheetOptions\n | (() => Promise<DirectConnectSheetOptions>);\n /** `clearTimeout`. Injectable for tests. Defaults to `globalThis.clearTimeout`. */\n clearTimeoutFn?: (handle: unknown) => void;\n /** Clock source in ms. Injectable for tests. Defaults to `Date.now`. */\n now?: () => number;\n}\n\n/**\n * Discriminated connect-flow state.\n *\n * @remarks\n * `type` matches the builder guide: it starts at `\"idle\"` and is non-idle while\n * connecting. The intermediate phases give richer UIs something to render.\n *\n * Desktop and light-data requests move through `\"awaiting_approval\"` (Vana Web\n * opens in a popup). A deep Direct request on a mobile browser moves through\n * `\"ready_to_open\"` instead: the SDK exposes a plain HTTPS\n * `mobileContinuationUrl` for the UI to render as a primary \"Open Vana\" link,\n * never launching it automatically, and keeps polling in memory.\n */\nexport type DirectConnectState<T = unknown> =\n | { type: \"idle\" }\n | { type: \"creating\" }\n | {\n type: \"awaiting_approval\";\n request: AccessRequest;\n /**\n * `true` when the popup was blocked. The UI should render the universal\n * HTTPS `request.approvalUrl` as a manual \"Open approval\" link.\n */\n popupBlocked: boolean;\n /** Present while Vana's sheet presents approval (`presentation: \"sheet\"`). */\n sheet?: DirectConnectSheetState;\n /**\n * Set when the sheet could not run here and the flow fell back to the\n * tab presentation (with `popupBlocked: true`).\n */\n sheetUnavailable?: SheetErrorCode;\n }\n | {\n type: \"ready_to_open\";\n request: AccessRequest;\n /**\n * Validated HTTPS continuation URL the mobile UI renders as the primary\n * \"Open Vana\" tap. Polling continues while it is shown; its embedded\n * ticket may rotate to a fresh URL between polls.\n */\n mobileContinuationUrl: string;\n }\n | { type: \"reading\"; request: AccessRequest }\n | { type: \"done\"; result: ApprovedDataResult<T> }\n | { type: \"error\"; error: Error };\n\n/** Whether an explicit read retry reused consent or started fresh approval. */\nexport type DirectConnectRetryOutcome =\n | \"retried_existing_grant\"\n | \"fresh_approval_required\";\n\n/** The store returned by {@link createDirectConnectFlow}. */\nexport interface DirectConnectFlow<T = unknown> {\n /** Current state. */\n getState(): DirectConnectState<T>;\n /** Subscribe to state changes; returns an unsubscribe function. */\n subscribe(listener: () => void): () => void;\n /** Begin the flow. No-op if already running. */\n start(): Promise<void>;\n /**\n * Retry a failed read, reusing a still-live approved request when possible.\n *\n * @remarks\n * This explicit path avoids the observed double-approval symptom where\n * \"Try that again\" minted a new request after a transient read failure.\n * The return value tells callers whether existing consent was reused or a\n * fresh approval was required.\n */\n retryRead(): Promise<DirectConnectRetryOutcome>;\n /** Reset to `idle` and stop any in-flight polling. */\n reset(): void;\n}\n\nconst DEFAULT_POLL_INTERVAL_MS = 1500;\nconst DEFAULT_TIMEOUT_MS = 300_000;\n\nfunction toError(value: unknown): Error {\n return value instanceof Error ? value : new Error(String(value));\n}\n\nfunction isReadReadyStatus(status: AccessRequestStatusValue): boolean {\n return status === \"approved\" || status === \"ready_for_read\";\n}\n\nconst MOBILE_USER_AGENT =\n /Android|iPhone|iPad|iPod|Mobile|Silk|Kindle|Opera Mini|IEMobile/i;\n\nfunction defaultBrowserPlatformPolicy(): DirectBrowserPlatformPolicy {\n return {\n current() {\n if (typeof navigator === \"undefined\") return \"desktop\";\n const isTouchCapableIpad =\n navigator.platform === \"MacIntel\" && navigator.maxTouchPoints > 1;\n return MOBILE_USER_AGENT.test(navigator.userAgent) || isTouchCapableIpad\n ? \"mobile\"\n : \"desktop\";\n },\n };\n}\n\n/**\n * Default {@link DirectConnectOptions.openApprovalWindow}: open a blank tab\n * synchronously (inside the click gesture) and return a handle to navigate\n * once the approval URL is known. Returns `null` when blocked or non-DOM.\n */\nfunction defaultOpenApprovalWindow(): ConnectWindow | null {\n if (typeof window === \"undefined\" || !window.open) return null;\n // We can't pass the \"noopener\"/\"noreferrer\" feature string here: it makes\n // window.open() return null, which would throw away the handle we need to\n // navigate later. So we open plain and re-create both protections by hand.\n const opened = window.open(\"\", \"_blank\");\n if (!opened) return null;\n // Sever the opener link while the tab is still about:blank, so the approval\n // page can't reach back into the app (reverse tab-nabbing).\n try {\n opened.opener = null;\n } catch {\n // Some environments make `opener` read-only; best-effort only.\n }\n return {\n navigate(url: string) {\n // Restore the no-referrer protection the old \"noreferrer\" feature gave:\n // tag the blank document so the upcoming navigation sends no Referer to\n // the approval page (best-effort; the blank doc is same-origin here).\n try {\n const meta = opened.document.createElement(\"meta\");\n meta.name = \"referrer\";\n meta.content = \"no-referrer\";\n (opened.document.head ?? opened.document.documentElement)?.appendChild(\n meta,\n );\n } catch {\n // Cross-origin/unavailable document: skip, navigation still proceeds.\n }\n opened.location.href = url;\n },\n close() {\n opened.close();\n },\n };\n}\n\n/**\n * Create a connect-flow store.\n *\n * @param transports - Backend transports (`createRequest`, `getStatus`, `readResult`).\n * @param options - Polling/timeout tunables and injectable side effects.\n * @returns A {@link DirectConnectFlow} store.\n */\nexport function createDirectConnectFlow<T = unknown>(\n transports: DirectConnectTransports<T>,\n options: DirectConnectOptions = {},\n): DirectConnectFlow<T> {\n const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n // `openApprovalWindow` and `browserPlatformPolicy` are resolved lazily at\n // start() (see below) so options swapped in after construction are still\n // honoured — matching the latest-callback pattern the React hook uses for its\n // transports.\n const setTimeoutFn =\n options.setTimeoutFn ??\n ((cb: () => void, ms: number) => globalThis.setTimeout(cb, ms));\n const clearTimeoutFn =\n options.clearTimeoutFn ??\n ((handle: unknown) => {\n globalThis.clearTimeout(handle as never);\n });\n const now = options.now ?? (() => Date.now());\n let state: DirectConnectState<T> = { type: \"idle\" };\n const listeners = new Set<() => void>();\n let pollHandle: unknown = null;\n // The request and deadline the poll loop is on, so a sheet event can ask\n // for the next poll now instead of after the interval.\n let pollTarget: { request: AccessRequest; deadline: number } | null = null;\n let pollInFlight = false;\n let pollNudged = false;\n let running = false;\n // Retained only after status proved this request had a read-ready grant.\n // A retry rechecks that status before trusting the prior consent.\n let approvedRequest: AccessRequest | null = null;\n // Monotonic id for the current start() invocation. reset() (and an\n // immediately following start()) bumps it, so a previous run whose async\n // createRequest is still in flight can detect it has been superseded and\n // avoid touching shared state / the newer run's tab.\n let activeRunId = 0;\n // Holds the tab we opened only while it is still blank (un-navigated). Once\n // navigated to the approval URL we drop the reference so reset/cleanup never\n // closes the live approval tab the user is interacting with.\n let openedWindow: ConnectWindow | null = null;\n // The sheet of the current run (`presentation: \"sheet\"`), while open.\n let sheetHandle: ConnectHandle | null = null;\n let sheetState: DirectConnectSheetState | null = null;\n let sheetUnavailable: SheetErrorCode | null = null;\n let sheetCloseAfterDoneMs: number | null = DEFAULT_CLOSE_AFTER_DONE_MS;\n let sheetCloseTimer: unknown = null;\n\n function emit(): void {\n for (const listener of listeners) listener();\n }\n\n function setState(next: DirectConnectState<T>): void {\n state = next;\n emit();\n }\n\n function clearPoll(): void {\n if (pollHandle !== null) {\n clearTimeoutFn(pollHandle);\n pollHandle = null;\n }\n pollTarget = null;\n pollNudged = false;\n }\n\n /**\n * Poll now rather than after the interval. Sheet events call this: they\n * hint that the backend's status may have moved, but never settle the flow\n * themselves. The poll stays the one authority for `done` and `error`.\n */\n function nudgePoll(): void {\n if (!running) return;\n if (pollInFlight) {\n pollNudged = true;\n return;\n }\n const target = pollTarget;\n if (pollHandle === null || !target) return;\n clearPoll();\n void poll(target.request, target.deadline);\n }\n\n /** Close the opened tab if it is still blank (never navigated). */\n function closeUnnavigatedWindow(): void {\n if (openedWindow) {\n openedWindow.close();\n openedWindow = null;\n }\n }\n\n function isRunningPhase(): boolean {\n return (\n state.type === \"creating\" ||\n state.type === \"awaiting_approval\" ||\n state.type === \"ready_to_open\" ||\n state.type === \"reading\"\n );\n }\n\n /** Close the sheet without treating its close event as the person's. */\n function closeSheet(): void {\n const handle = sheetHandle;\n sheetHandle = null;\n handle?.close();\n }\n\n function clearSheet(): void {\n if (sheetCloseTimer !== null) {\n clearTimeoutFn(sheetCloseTimer);\n sheetCloseTimer = null;\n }\n closeSheet();\n sheetState = null;\n sheetUnavailable = null;\n }\n\n /** The waiting state for `request`, given what the sheet has reported. */\n function awaitingState(request: AccessRequest): DirectConnectState<T> {\n if (sheetUnavailable) {\n return {\n type: \"awaiting_approval\",\n request,\n popupBlocked: true,\n sheetUnavailable,\n };\n }\n return {\n type: \"awaiting_approval\",\n request,\n popupBlocked: false,\n ...(sheetState ? { sheet: { ...sheetState } } : {}),\n };\n }\n\n function updateSheet(patch: Partial<DirectConnectSheetState>): void {\n if (!sheetState) return;\n sheetState = { ...sheetState, ...patch };\n if (state.type === \"awaiting_approval\" && state.sheet) {\n setState(awaitingState(state.request));\n }\n }\n\n function failRun(error: Error): void {\n running = false;\n clearPoll();\n setState({ type: \"error\", error });\n }\n\n /**\n * Sheet events update what the UI shows and nudge the status poll; none of\n * them is an outcome. `connected` is the person's approval, not delivery.\n * A future terminal sheet event belongs here too, as another nudge: the\n * backend poll and read still decide `done` or `error`.\n */\n function handleSheetEvent(event: ConnectEvent): void {\n switch (event.type) {\n case \"connected\":\n updateSheet({ completion: event.completion });\n nudgePoll();\n return;\n case \"progress\":\n updateSheet({ progress: event.phase });\n return;\n case \"error\": {\n if (event.code === \"request_failed\") {\n // The backend could not create or sign the request for the sheet.\n if (!running) return;\n closeSheet();\n failRun(new Error(\"Could not prepare the request for Vana's sheet\"));\n return;\n }\n if (SHEET_FALLBACK_CODES.includes(event.code)) {\n // The sheet cannot run on this page: fall back to the approval URL.\n sheetUnavailable = event.code;\n sheetState = null;\n closeSheet();\n if (state.type === \"awaiting_approval\") {\n setState(awaitingState(state.request));\n }\n return;\n }\n updateSheet({ error: event.code });\n return;\n }\n case \"close\": {\n if (!sheetHandle) return; // closed by the flow itself\n sheetHandle = null;\n if (!running) return;\n if (sheetState?.error) {\n failRun(new Error(`Vana's sheet failed: ${sheetState.error}`));\n return;\n }\n if (!sheetState?.completion) {\n // Closed before approving: the person backed out.\n running = false;\n activeRunId++;\n clearPoll();\n sheetState = null;\n setState({ type: \"idle\" });\n return;\n }\n // Approved, then closed. The data may still be readable (a durable\n // Personal Server), so keep polling until the request settles.\n updateSheet({ open: false });\n return;\n }\n default:\n return;\n }\n }\n\n async function readAndFinish(request: AccessRequest): Promise<void> {\n setState({ type: \"reading\", request });\n try {\n const result = await transports.readResult(request.requestId);\n if (!running) return;\n setState({ type: \"done\", result });\n if (sheetHandle) {\n sheetHandle.setRequestStatus(\"completed\");\n if (sheetCloseAfterDoneMs !== null) {\n sheetCloseTimer = setTimeoutFn(() => {\n sheetCloseTimer = null;\n closeSheet();\n }, sheetCloseAfterDoneMs);\n }\n }\n } catch (err) {\n if (!running) return;\n setState({ type: \"error\", error: toError(err) });\n } finally {\n running = false;\n }\n }\n\n function scheduleNextPoll(request: AccessRequest, deadline: number): void {\n const delay = pollNudged ? 0 : pollIntervalMs;\n pollNudged = false;\n pollTarget = { request, deadline };\n pollHandle = setTimeoutFn(() => {\n pollHandle = null;\n void poll(request, deadline);\n }, delay);\n }\n\n function requestDeadline(request: AccessRequest): number {\n if (request.expiresAt !== undefined) {\n const expiresAt = Date.parse(request.expiresAt);\n if (Number.isFinite(expiresAt)) return expiresAt;\n }\n return now() + timeoutMs;\n }\n\n /**\n * Enter the polling loop from the given initial state (either\n * `awaiting_approval` for desktop/light or `ready_to_open` for mobile-deep).\n * Errors out immediately if the request has already expired.\n */\n function startPolling(\n request: AccessRequest,\n initialState: DirectConnectState<T>,\n ): void {\n pollNudged = false;\n setState(initialState);\n const deadline = requestDeadline(request);\n if (now() >= deadline) {\n running = false;\n setState({\n type: \"error\",\n error: new Error(\"Access request expired\"),\n });\n return;\n }\n scheduleNextPoll(request, deadline);\n }\n\n async function poll(request: AccessRequest, deadline: number): Promise<void> {\n if (!running) return;\n if (now() >= deadline) {\n running = false;\n setState({\n type: \"error\",\n error: new Error(\"Timed out waiting for approval\"),\n });\n return;\n }\n let status: AccessRequestStatus;\n pollInFlight = true;\n try {\n status = await transports.getStatus(request.requestId);\n } catch (err) {\n if (!running) return;\n running = false;\n setState({ type: \"error\", error: toError(err) });\n return;\n } finally {\n pollInFlight = false;\n }\n if (!running) return;\n\n // A pending deep-mobile status may rotate the continuation ticket. Adopt a\n // fresh, still-valid URL so the rendered \"Open Vana\" link always points at a\n // live ticket; ignore it on the desktop/light path.\n if (status.status === \"pending\" && state.type === \"ready_to_open\") {\n const refreshed = normalizeMobileContinuationUrl(\n status.mobileContinuationUrl,\n );\n if (refreshed && refreshed !== state.mobileContinuationUrl) {\n request = { ...request, mobileContinuationUrl: refreshed };\n setState({\n type: \"ready_to_open\",\n request,\n mobileContinuationUrl: refreshed,\n });\n }\n }\n\n if (isReadReadyStatus(status.status)) {\n clearPoll();\n approvedRequest = request;\n await readAndFinish(request);\n return;\n }\n if (\n status.status === \"completed\" ||\n status.status === \"denied\" ||\n status.status === \"expired\"\n ) {\n // The sheet shows the outcome; it stays open for the person to close.\n sheetHandle?.setRequestStatus(status.status);\n running = false;\n setState({\n type: \"error\",\n error: new Error(`Access request ${status.status}`),\n });\n return;\n }\n scheduleNextPoll(request, deadline);\n }\n\n const flow: DirectConnectFlow<T> = {\n getState() {\n return state;\n },\n\n subscribe(listener: () => void) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n\n async start(): Promise<void> {\n if (running || isRunningPhase()) return;\n running = true;\n // start() deliberately keeps its established first-run semantics: every\n // explicit start creates a request. Only retryRead() may reuse consent.\n approvedRequest = null;\n const runId = ++activeRunId;\n clearSheet();\n if (options.presentation === \"sheet\") {\n await startSheet(runId);\n return;\n }\n // Read the platform policy at start time, like openApprovalWindow below,\n // so a policy swapped in after construction (a React rerender forwards\n // options through a ref) still decides this run's destination.\n const browserPlatform = (\n options.browserPlatformPolicy ?? defaultBrowserPlatformPolicy()\n ).current();\n\n // Desktop preserves the pre-mobile synchronous popup contract: open a\n // blank tab while the click's transient activation is live, then navigate\n // it once createRequest returns the approval URL (BUI-622). Mobile never\n // creates that transient tab; deep requests expose one explicit HTTPS\n // link, while light requests retain the manual approvalUrl fallback.\n // Read the opener option at start time so a swapped-in custom opener is\n // still honored for desktop flows.\n const approvalWindow =\n browserPlatform === \"desktop\"\n ? (options.openApprovalWindow ?? defaultOpenApprovalWindow)()\n : null;\n openedWindow = approvalWindow;\n\n setState({ type: \"creating\" });\n\n let request: AccessRequest;\n try {\n request = await transports.createRequest();\n } catch (err) {\n // If we were superseded (reset, possibly + a newer start()) while this\n // request was in flight, only clean up our own tab — never the shared\n // state or the newer run's window.\n if (runId !== activeRunId) {\n approvalWindow?.close();\n return;\n }\n running = false;\n closeUnnavigatedWindow();\n setState({ type: \"error\", error: toError(err) });\n return;\n }\n if (runId !== activeRunId) {\n approvalWindow?.close();\n return;\n }\n // Re-validate the continuation URL at the SDK boundary (defense in depth\n // for custom transports that bypass the default client).\n request = {\n ...request,\n mobileContinuationUrl: normalizeMobileContinuationUrl(\n request.mobileContinuationUrl,\n ),\n };\n\n // The SDK owns only the small mobile-versus-desktop destination choice.\n // A deep Direct request on mobile carries a validated continuation URL;\n // desktop keeps its popup contract, while mobile light exposes the HTTPS\n // approval URL as the existing manual fallback without opening a tab.\n const mobileContinuationUrl =\n browserPlatform === \"mobile\"\n ? request.mobileContinuationUrl\n : undefined;\n\n if (mobileContinuationUrl) {\n // Do not auto-launch: DCR creation is async, so the original Connect\n // gesture can no longer be trusted to retain iOS user activation. Let\n // the UI render an explicit primary \"Open Vana\" link; polling continues\n // in this tab.\n startPolling(request, {\n type: \"ready_to_open\",\n request,\n mobileContinuationUrl,\n });\n return;\n }\n\n // Desktop/light: navigate the synchronously-opened tab to the HTTPS\n // approval URL. `approvalWindow === null` means the popup was blocked;\n // surface it so the UI renders request.approvalUrl as a visible manual\n // \"Open approval\" link instead of hanging. We poll either way, so a manual\n // open still resolves the flow, and the timeout still bounds the wait.\n if (approvalWindow) {\n approvalWindow.navigate(request.approvalUrl);\n // Hand the tab off to the user; we no longer own/close it.\n openedWindow = null;\n }\n startPolling(request, {\n type: \"awaiting_approval\",\n request,\n popupBlocked: approvalWindow === null,\n });\n },\n\n async retryRead(): Promise<DirectConnectRetryOutcome> {\n if (running || isRunningPhase()) {\n throw new Error(\n \"Cannot retry a read while the connect flow is running\",\n );\n }\n\n const request = approvedRequest;\n const parsedExpiry = request?.expiresAt\n ? Date.parse(request.expiresAt)\n : Number.NaN;\n const requestExpired =\n Number.isFinite(parsedExpiry) && now() >= parsedExpiry;\n\n if (request && !requestExpired) {\n running = true;\n const runId = ++activeRunId;\n let status: AccessRequestStatus;\n try {\n status = await transports.getStatus(request.requestId);\n } catch (err) {\n if (runId !== activeRunId) {\n throw new Error(\"Read retry was superseded\");\n }\n running = false;\n const error = toError(err);\n setState({ type: \"error\", error });\n throw error;\n }\n if (runId !== activeRunId) {\n throw new Error(\"Read retry was superseded\");\n }\n if (isReadReadyStatus(status.status)) {\n await readAndFinish(request);\n return \"retried_existing_grant\";\n }\n running = false;\n }\n\n approvedRequest = null;\n await flow.start();\n return \"fresh_approval_required\";\n },\n\n reset(): void {\n running = false;\n approvedRequest = null;\n // Invalidate any in-flight start() so a late createRequest can't clobber\n // a subsequent run.\n activeRunId++;\n clearPoll();\n closeUnnavigatedWindow();\n clearSheet();\n setState({ type: \"idle\" });\n },\n };\n\n /**\n * `presentation: \"sheet\"`: open Vana's sheet at once and create the request\n * alongside it, so the request is ready by the time the person has signed\n * in. The sheet asks for the request's read capability; the status poll and\n * the read are the tab flow's.\n */\n async function startSheet(runId: number): Promise<void> {\n setState({ type: \"creating\" });\n const fail = (error: unknown) => {\n if (runId !== activeRunId) return;\n clearSheet();\n failRun(toError(error));\n };\n\n const authorizeRequest = transports.authorizeRequest;\n if (!authorizeRequest) {\n fail(\n new Error('presentation: \"sheet\" needs the authorizeRequest transport'),\n );\n return;\n }\n let sheetOptions: DirectConnectSheetOptions;\n try {\n const configured = options.sheet;\n if (!configured) {\n throw new Error('presentation: \"sheet\" needs sheet options');\n }\n sheetOptions =\n typeof configured === \"function\" ? await configured() : configured;\n } catch (err) {\n fail(err);\n return;\n }\n if (runId !== activeRunId) return;\n\n const created = transports.createRequest();\n // Observed below; this keeps an early rejection from going unhandled.\n created.catch(() => undefined);\n sheetCloseAfterDoneMs =\n sheetOptions.closeAfterDoneMs === undefined\n ? DEFAULT_CLOSE_AFTER_DONE_MS\n : sheetOptions.closeAfterDoneMs;\n sheetState = { open: true };\n try {\n sheetHandle = (sheetOptions.open ?? openConnectSheet)({\n clientId: sheetOptions.clientId,\n appName: sheetOptions.appName,\n vanaOrigin: sheetOptions.accountOrigin,\n container: sheetOptions.container,\n readyTimeoutMs: sheetOptions.readyTimeoutMs,\n async getRequest() {\n const request = await created;\n return {\n id: request.requestId,\n authorization: await authorizeRequest(request.requestId),\n };\n },\n onEvent(event) {\n if (runId !== activeRunId) return;\n sheetOptions.onEvent?.(event);\n handleSheetEvent(event);\n },\n });\n } catch {\n // A launcher that cannot open (no DOM, bad origin) is a sheet that\n // cannot run here: fall back to the approval URL.\n sheetHandle = null;\n sheetState = null;\n sheetUnavailable = \"sheet_failed\";\n }\n\n let request: AccessRequest;\n try {\n request = await created;\n } catch (err) {\n fail(err);\n return;\n }\n if (runId !== activeRunId || !running) return;\n startPolling(request, awaitingState(request));\n }\n\n return flow;\n}\n"],"mappings":"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","names":["request"]}
|
|
1
|
+
{"version":3,"sources":["../../src/direct/connect-flow.ts"],"sourcesContent":["/**\n * Framework-agnostic connect-flow state machine for the browser two-tab helper.\n *\n * @remarks\n * This is the testable core behind {@link useDirectVanaConnect}. It is pure\n * TypeScript (no React, no DOM-only APIs beyond an injectable window opener and\n * timers) so the full flow — create request, open Vana, poll status, read data —\n * can be exercised in a Node test environment.\n *\n * The React hook is a thin `useSyncExternalStore` binding over this store.\n *\n * @category Direct\n * @module direct/connect-flow\n */\n\nimport type {\n AccessRequest,\n AccessRequestStatus,\n AccessRequestStatusValue,\n ApprovedDataResult,\n} from \"./types\";\nimport { normalizeMobileContinuationUrl } from \"./types\";\n\n/**\n * Caller-supplied transports. These typically `fetch` the app's own backend\n * routes, which in turn delegate to a {@link DirectDataController}.\n */\nexport interface DirectConnectTransports<T = unknown> {\n /** Ask the backend to create an access request. */\n createRequest: () => Promise<AccessRequest>;\n /** Ask the backend for the current status of a request. */\n getStatus: (requestId: string) => Promise<AccessRequestStatus>;\n /** Ask the backend to read the approved data. */\n readResult: (requestId: string) => Promise<ApprovedDataResult<T>>;\n}\n\n/**\n * A handle to a tab opened synchronously under the user's click gesture.\n *\n * @remarks\n * The flow opens this tab *before* it knows the approval URL (popup blockers\n * only allow `window.open()` during the click's transient activation), then\n * navigates it once `createRequest` resolves.\n */\nexport interface ConnectWindow {\n /** Point the already-open tab at the approval URL. */\n navigate(url: string): void;\n /** Close the tab (used to clean up an un-navigated tab on failure/reset). */\n close(): void;\n}\n\n/** Browser class used only to choose the destination returned by Vana. */\nexport type DirectBrowserPlatform = \"desktop\" | \"mobile\";\n\n/** Injectable browser-platform policy; it never asserts whether an app exists. */\nexport interface DirectBrowserPlatformPolicy {\n current(): DirectBrowserPlatform;\n}\n\n/** Tunables for the connect flow. */\nexport interface DirectConnectOptions {\n /** Status poll interval in ms. Defaults to 1500. */\n pollIntervalMs?: number;\n /**\n * Overall timeout in ms before giving up. Defaults to 300000 (5 min).\n * Used only when the access request does not carry an authoritative\n * `expiresAt` value.\n */\n timeoutMs?: number;\n /**\n * Synchronously open a blank tab under the click's transient activation and\n * return a handle to navigate later, or `null` if the browser blocked it.\n * Defaults to `window.open(\"\", \"_blank\")` (with `opener` severed). Injectable\n * for tests.\n *\n * @remarks\n * Renamed from the pre-3.8 `openWindow?: (url) => void`. The old contract was\n * the BUI-622 bug itself (it was called with the URL *after* an `await`, so\n * the popup blocker suppressed it); it cannot be preserved while fixing the\n * bug. Custom openers must now open synchronously and return a navigable\n * handle.\n */\n openApprovalWindow?: () => ConnectWindow | null;\n /** SDK-owned mobile/desktop policy. Injectable for deterministic tests. */\n browserPlatformPolicy?: DirectBrowserPlatformPolicy;\n /** `setTimeout`. Injectable for tests. Defaults to `globalThis.setTimeout`. */\n setTimeoutFn?: (cb: () => void, ms: number) => unknown;\n /** `clearTimeout`. Injectable for tests. Defaults to `globalThis.clearTimeout`. */\n clearTimeoutFn?: (handle: unknown) => void;\n /** Clock source in ms. Injectable for tests. Defaults to `Date.now`. */\n now?: () => number;\n}\n\n/**\n * Discriminated connect-flow state.\n *\n * @remarks\n * `type` matches the builder guide: it starts at `\"idle\"` and is non-idle while\n * connecting. The intermediate phases give richer UIs something to render.\n *\n * Desktop and light-data requests move through `\"awaiting_approval\"` (Vana Web\n * opens in a popup). A deep Direct request on a mobile browser moves through\n * `\"ready_to_open\"` instead: the SDK exposes a plain HTTPS\n * `mobileContinuationUrl` for the UI to render as a primary \"Open Vana\" link,\n * never launching it automatically, and keeps polling in memory.\n */\nexport type DirectConnectState<T = unknown> =\n | { type: \"idle\" }\n | { type: \"creating\" }\n | {\n type: \"awaiting_approval\";\n request: AccessRequest;\n /**\n * `true` when the popup was blocked. The UI should render the universal\n * HTTPS `request.approvalUrl` as a manual \"Open approval\" link.\n */\n popupBlocked: boolean;\n }\n | {\n type: \"ready_to_open\";\n request: AccessRequest;\n /**\n * Validated HTTPS continuation URL the mobile UI renders as the primary\n * \"Open Vana\" tap. Polling continues while it is shown; its embedded\n * ticket may rotate to a fresh URL between polls.\n */\n mobileContinuationUrl: string;\n }\n | { type: \"reading\"; request: AccessRequest }\n | { type: \"done\"; result: ApprovedDataResult<T> }\n | { type: \"error\"; error: Error };\n\n/** Whether an explicit read retry reused consent or started fresh approval. */\nexport type DirectConnectRetryOutcome =\n | \"retried_existing_grant\"\n | \"fresh_approval_required\";\n\n/** The store returned by {@link createDirectConnectFlow}. */\nexport interface DirectConnectFlow<T = unknown> {\n /** Current state. */\n getState(): DirectConnectState<T>;\n /** Subscribe to state changes; returns an unsubscribe function. */\n subscribe(listener: () => void): () => void;\n /** Begin the flow. No-op if already running. */\n start(): Promise<void>;\n /**\n * Retry a failed read, reusing a still-live approved request when possible.\n *\n * @remarks\n * This explicit path avoids the observed double-approval symptom where\n * \"Try that again\" minted a new request after a transient read failure.\n * The return value tells callers whether existing consent was reused or a\n * fresh approval was required.\n */\n retryRead(): Promise<DirectConnectRetryOutcome>;\n /** Reset to `idle` and stop any in-flight polling. */\n reset(): void;\n}\n\nconst DEFAULT_POLL_INTERVAL_MS = 1500;\nconst DEFAULT_TIMEOUT_MS = 300_000;\n\nfunction toError(value: unknown): Error {\n return value instanceof Error ? value : new Error(String(value));\n}\n\nfunction isReadReadyStatus(status: AccessRequestStatusValue): boolean {\n return status === \"approved\" || status === \"ready_for_read\";\n}\n\nconst MOBILE_USER_AGENT =\n /Android|iPhone|iPad|iPod|Mobile|Silk|Kindle|Opera Mini|IEMobile/i;\n\nfunction defaultBrowserPlatformPolicy(): DirectBrowserPlatformPolicy {\n return {\n current() {\n if (typeof navigator === \"undefined\") return \"desktop\";\n const isTouchCapableIpad =\n navigator.platform === \"MacIntel\" && navigator.maxTouchPoints > 1;\n return MOBILE_USER_AGENT.test(navigator.userAgent) || isTouchCapableIpad\n ? \"mobile\"\n : \"desktop\";\n },\n };\n}\n\n/**\n * Default {@link DirectConnectOptions.openApprovalWindow}: open a blank tab\n * synchronously (inside the click gesture) and return a handle to navigate\n * once the approval URL is known. Returns `null` when blocked or non-DOM.\n */\nfunction defaultOpenApprovalWindow(): ConnectWindow | null {\n if (typeof window === \"undefined\" || !window.open) return null;\n // We can't pass the \"noopener\"/\"noreferrer\" feature string here: it makes\n // window.open() return null, which would throw away the handle we need to\n // navigate later. So we open plain and re-create both protections by hand.\n const opened = window.open(\"\", \"_blank\");\n if (!opened) return null;\n // Sever the opener link while the tab is still about:blank, so the approval\n // page can't reach back into the app (reverse tab-nabbing).\n try {\n opened.opener = null;\n } catch {\n // Some environments make `opener` read-only; best-effort only.\n }\n return {\n navigate(url: string) {\n // Restore the no-referrer protection the old \"noreferrer\" feature gave:\n // tag the blank document so the upcoming navigation sends no Referer to\n // the approval page (best-effort; the blank doc is same-origin here).\n try {\n const meta = opened.document.createElement(\"meta\");\n meta.name = \"referrer\";\n meta.content = \"no-referrer\";\n (opened.document.head ?? opened.document.documentElement)?.appendChild(\n meta,\n );\n } catch {\n // Cross-origin/unavailable document: skip, navigation still proceeds.\n }\n opened.location.href = url;\n },\n close() {\n opened.close();\n },\n };\n}\n\n/**\n * Create a connect-flow store.\n *\n * @param transports - Backend transports (`createRequest`, `getStatus`, `readResult`).\n * @param options - Polling/timeout tunables and injectable side effects.\n * @returns A {@link DirectConnectFlow} store.\n */\nexport function createDirectConnectFlow<T = unknown>(\n transports: DirectConnectTransports<T>,\n options: DirectConnectOptions = {},\n): DirectConnectFlow<T> {\n const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n // `openApprovalWindow` and `browserPlatformPolicy` are resolved lazily at\n // start() (see below) so options swapped in after construction are still\n // honoured — matching the latest-callback pattern the React hook uses for its\n // transports.\n const setTimeoutFn =\n options.setTimeoutFn ??\n ((cb: () => void, ms: number) => globalThis.setTimeout(cb, ms));\n const clearTimeoutFn =\n options.clearTimeoutFn ??\n ((handle: unknown) => {\n globalThis.clearTimeout(handle as never);\n });\n const now = options.now ?? (() => Date.now());\n let state: DirectConnectState<T> = { type: \"idle\" };\n const listeners = new Set<() => void>();\n let pollHandle: unknown = null;\n let running = false;\n // Retained only after status proved this request had a read-ready grant.\n // A retry rechecks that status before trusting the prior consent.\n let approvedRequest: AccessRequest | null = null;\n // Monotonic id for the current start() invocation. reset() (and an\n // immediately following start()) bumps it, so a previous run whose async\n // createRequest is still in flight can detect it has been superseded and\n // avoid touching shared state / the newer run's tab.\n let activeRunId = 0;\n // Holds the tab we opened only while it is still blank (un-navigated). Once\n // navigated to the approval URL we drop the reference so reset/cleanup never\n // closes the live approval tab the user is interacting with.\n let openedWindow: ConnectWindow | null = null;\n\n function emit(): void {\n for (const listener of listeners) listener();\n }\n\n function setState(next: DirectConnectState<T>): void {\n state = next;\n emit();\n }\n\n function clearPoll(): void {\n if (pollHandle !== null) {\n clearTimeoutFn(pollHandle);\n pollHandle = null;\n }\n }\n\n /** Close the opened tab if it is still blank (never navigated). */\n function closeUnnavigatedWindow(): void {\n if (openedWindow) {\n openedWindow.close();\n openedWindow = null;\n }\n }\n\n function isRunningPhase(): boolean {\n return (\n state.type === \"creating\" ||\n state.type === \"awaiting_approval\" ||\n state.type === \"ready_to_open\" ||\n state.type === \"reading\"\n );\n }\n\n async function readAndFinish(request: AccessRequest): Promise<void> {\n setState({ type: \"reading\", request });\n try {\n const result = await transports.readResult(request.requestId);\n if (!running) return;\n setState({ type: \"done\", result });\n } catch (err) {\n if (!running) return;\n setState({ type: \"error\", error: toError(err) });\n } finally {\n running = false;\n }\n }\n\n function scheduleNextPoll(request: AccessRequest, deadline: number): void {\n pollHandle = setTimeoutFn(() => {\n void poll(request, deadline);\n }, pollIntervalMs);\n }\n\n function requestDeadline(request: AccessRequest): number {\n if (request.expiresAt !== undefined) {\n const expiresAt = Date.parse(request.expiresAt);\n if (Number.isFinite(expiresAt)) return expiresAt;\n }\n return now() + timeoutMs;\n }\n\n /**\n * Enter the polling loop from the given initial state (either\n * `awaiting_approval` for desktop/light or `ready_to_open` for mobile-deep).\n * Errors out immediately if the request has already expired.\n */\n function startPolling(\n request: AccessRequest,\n initialState: DirectConnectState<T>,\n ): void {\n setState(initialState);\n const deadline = requestDeadline(request);\n if (now() >= deadline) {\n running = false;\n setState({\n type: \"error\",\n error: new Error(\"Access request expired\"),\n });\n return;\n }\n scheduleNextPoll(request, deadline);\n }\n\n async function poll(request: AccessRequest, deadline: number): Promise<void> {\n if (!running) return;\n if (now() >= deadline) {\n running = false;\n setState({\n type: \"error\",\n error: new Error(\"Timed out waiting for approval\"),\n });\n return;\n }\n let status: AccessRequestStatus;\n try {\n status = await transports.getStatus(request.requestId);\n } catch (err) {\n if (!running) return;\n running = false;\n setState({ type: \"error\", error: toError(err) });\n return;\n }\n if (!running) return;\n\n // A pending deep-mobile status may rotate the continuation ticket. Adopt a\n // fresh, still-valid URL so the rendered \"Open Vana\" link always points at a\n // live ticket; ignore it on the desktop/light path.\n if (status.status === \"pending\" && state.type === \"ready_to_open\") {\n const refreshed = normalizeMobileContinuationUrl(\n status.mobileContinuationUrl,\n );\n if (refreshed && refreshed !== state.mobileContinuationUrl) {\n request = { ...request, mobileContinuationUrl: refreshed };\n setState({\n type: \"ready_to_open\",\n request,\n mobileContinuationUrl: refreshed,\n });\n }\n }\n\n if (isReadReadyStatus(status.status)) {\n clearPoll();\n approvedRequest = request;\n await readAndFinish(request);\n return;\n }\n if (\n status.status === \"completed\" ||\n status.status === \"denied\" ||\n status.status === \"expired\"\n ) {\n running = false;\n setState({\n type: \"error\",\n error: new Error(`Access request ${status.status}`),\n });\n return;\n }\n scheduleNextPoll(request, deadline);\n }\n\n const flow: DirectConnectFlow<T> = {\n getState() {\n return state;\n },\n\n subscribe(listener: () => void) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n\n async start(): Promise<void> {\n if (running || isRunningPhase()) return;\n running = true;\n // start() deliberately keeps its established first-run semantics: every\n // explicit start creates a request. Only retryRead() may reuse consent.\n approvedRequest = null;\n const runId = ++activeRunId;\n // Read the platform policy at start time, like openApprovalWindow below,\n // so a policy swapped in after construction (a React rerender forwards\n // options through a ref) still decides this run's destination.\n const browserPlatform = (\n options.browserPlatformPolicy ?? defaultBrowserPlatformPolicy()\n ).current();\n\n // Desktop preserves the pre-mobile synchronous popup contract: open a\n // blank tab while the click's transient activation is live, then navigate\n // it once createRequest returns the approval URL (BUI-622). Mobile never\n // creates that transient tab; deep requests expose one explicit HTTPS\n // link, while light requests retain the manual approvalUrl fallback.\n // Read the opener option at start time so a swapped-in custom opener is\n // still honored for desktop flows.\n const approvalWindow =\n browserPlatform === \"desktop\"\n ? (options.openApprovalWindow ?? defaultOpenApprovalWindow)()\n : null;\n openedWindow = approvalWindow;\n\n setState({ type: \"creating\" });\n\n let request: AccessRequest;\n try {\n request = await transports.createRequest();\n } catch (err) {\n // If we were superseded (reset, possibly + a newer start()) while this\n // request was in flight, only clean up our own tab — never the shared\n // state or the newer run's window.\n if (runId !== activeRunId) {\n approvalWindow?.close();\n return;\n }\n running = false;\n closeUnnavigatedWindow();\n setState({ type: \"error\", error: toError(err) });\n return;\n }\n if (runId !== activeRunId) {\n approvalWindow?.close();\n return;\n }\n // Re-validate the continuation URL at the SDK boundary (defense in depth\n // for custom transports that bypass the default client).\n request = {\n ...request,\n mobileContinuationUrl: normalizeMobileContinuationUrl(\n request.mobileContinuationUrl,\n ),\n };\n\n // The SDK owns only the small mobile-versus-desktop destination choice.\n // A deep Direct request on mobile carries a validated continuation URL;\n // desktop keeps its popup contract, while mobile light exposes the HTTPS\n // approval URL as the existing manual fallback without opening a tab.\n const mobileContinuationUrl =\n browserPlatform === \"mobile\"\n ? request.mobileContinuationUrl\n : undefined;\n\n if (mobileContinuationUrl) {\n // Do not auto-launch: DCR creation is async, so the original Connect\n // gesture can no longer be trusted to retain iOS user activation. Let\n // the UI render an explicit primary \"Open Vana\" link; polling continues\n // in this tab.\n startPolling(request, {\n type: \"ready_to_open\",\n request,\n mobileContinuationUrl,\n });\n return;\n }\n\n // Desktop/light: navigate the synchronously-opened tab to the HTTPS\n // approval URL. `approvalWindow === null` means the popup was blocked;\n // surface it so the UI renders request.approvalUrl as a visible manual\n // \"Open approval\" link instead of hanging. We poll either way, so a manual\n // open still resolves the flow, and the timeout still bounds the wait.\n if (approvalWindow) {\n approvalWindow.navigate(request.approvalUrl);\n // Hand the tab off to the user; we no longer own/close it.\n openedWindow = null;\n }\n startPolling(request, {\n type: \"awaiting_approval\",\n request,\n popupBlocked: approvalWindow === null,\n });\n },\n\n async retryRead(): Promise<DirectConnectRetryOutcome> {\n if (running || isRunningPhase()) {\n throw new Error(\n \"Cannot retry a read while the connect flow is running\",\n );\n }\n\n const request = approvedRequest;\n const parsedExpiry = request?.expiresAt\n ? Date.parse(request.expiresAt)\n : Number.NaN;\n const requestExpired =\n Number.isFinite(parsedExpiry) && now() >= parsedExpiry;\n\n if (request && !requestExpired) {\n running = true;\n const runId = ++activeRunId;\n let status: AccessRequestStatus;\n try {\n status = await transports.getStatus(request.requestId);\n } catch (err) {\n if (runId !== activeRunId) {\n throw new Error(\"Read retry was superseded\");\n }\n running = false;\n const error = toError(err);\n setState({ type: \"error\", error });\n throw error;\n }\n if (runId !== activeRunId) {\n throw new Error(\"Read retry was superseded\");\n }\n if (isReadReadyStatus(status.status)) {\n await readAndFinish(request);\n return \"retried_existing_grant\";\n }\n running = false;\n }\n\n approvedRequest = null;\n await flow.start();\n return \"fresh_approval_required\";\n },\n\n reset(): void {\n running = false;\n approvedRequest = null;\n // Invalidate any in-flight start() so a late createRequest can't clobber\n // a subsequent run.\n activeRunId++;\n clearPoll();\n closeUnnavigatedWindow();\n setState({ type: \"idle\" });\n },\n };\n\n return flow;\n}\n"],"mappings":"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","names":[]}
|