@opendatalabs/vana-sdk 4.3.2 → 4.4.0-connect-sheet.0-pr.218.295a8da
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 +131 -0
- package/dist/connect/account-origin.cjs +72 -0
- package/dist/connect/account-origin.cjs.map +1 -0
- package/dist/connect/account-origin.d.ts +92 -0
- package/dist/connect/account-origin.js +47 -0
- package/dist/connect/account-origin.js.map +1 -0
- package/dist/connect/account-origin.test.d.ts +1 -0
- package/dist/connect/chrome.cjs +221 -0
- package/dist/connect/chrome.cjs.map +1 -0
- package/dist/connect/chrome.d.ts +40 -0
- package/dist/connect/chrome.js +193 -0
- package/dist/connect/chrome.js.map +1 -0
- package/dist/connect/http-transports.cjs +100 -0
- package/dist/connect/http-transports.cjs.map +1 -0
- package/dist/connect/http-transports.d.ts +77 -0
- package/dist/connect/http-transports.js +75 -0
- package/dist/connect/http-transports.js.map +1 -0
- package/dist/connect/http-transports.test.d.ts +1 -0
- package/dist/connect/launcher.cjs +224 -0
- package/dist/connect/launcher.cjs.map +1 -0
- package/dist/connect/launcher.d.ts +122 -0
- package/dist/connect/launcher.js +210 -0
- package/dist/connect/launcher.js.map +1 -0
- package/dist/connect/launcher.test.d.ts +1 -0
- package/dist/connect/protocol.cjs +244 -0
- package/dist/connect/protocol.cjs.map +1 -0
- package/dist/connect/protocol.d.ts +307 -0
- package/dist/connect/protocol.js +203 -0
- package/dist/connect/protocol.js.map +1 -0
- package/dist/connect/protocol.test.d.ts +1 -0
- package/dist/connect.cjs +55 -0
- package/dist/connect.cjs.map +1 -0
- package/dist/connect.d.ts +35 -0
- package/dist/connect.js +34 -0
- package/dist/connect.js.map +1 -0
- package/dist/direct/connect-flow-sheet.test.d.ts +1 -0
- package/dist/direct/connect-flow.cjs +211 -1
- package/dist/direct/connect-flow.cjs.map +1 -1
- package/dist/direct/connect-flow.d.ts +75 -0
- package/dist/direct/connect-flow.js +213 -1
- package/dist/direct/connect-flow.js.map +1 -1
- package/dist/direct/connect-handler.cjs +493 -0
- package/dist/direct/connect-handler.cjs.map +1 -0
- package/dist/direct/connect-handler.d.ts +241 -0
- package/dist/direct/connect-handler.js +476 -0
- package/dist/direct/connect-handler.js.map +1 -0
- package/dist/direct/connect-handler.test.d.ts +1 -0
- package/dist/direct/controller.cjs +58 -3
- package/dist/direct/controller.cjs.map +1 -1
- package/dist/direct/controller.d.ts +79 -3
- package/dist/direct/controller.js +54 -2
- package/dist/direct/controller.js.map +1 -1
- package/dist/direct/endpoints.cjs +5 -2
- package/dist/direct/endpoints.cjs.map +1 -1
- package/dist/direct/endpoints.js +5 -2
- package/dist/direct/endpoints.js.map +1 -1
- package/dist/direct/request-authorization.cjs +65 -0
- package/dist/direct/request-authorization.cjs.map +1 -0
- package/dist/direct/request-authorization.d.ts +65 -0
- package/dist/direct/request-authorization.js +39 -0
- package/dist/direct/request-authorization.js.map +1 -0
- package/dist/direct/request-authorization.test.d.ts +1 -0
- package/dist/direct/types.cjs.map +1 -1
- package/dist/direct/types.d.ts +6 -0
- package/dist/direct/types.js.map +1 -1
- package/dist/direct/use-direct-vana-connect.cjs +18 -1
- package/dist/direct/use-direct-vana-connect.cjs.map +1 -1
- package/dist/direct/use-direct-vana-connect.js +18 -1
- package/dist/direct/use-direct-vana-connect.js.map +1 -1
- package/dist/direct/use-vana-connect.cjs +84 -0
- package/dist/direct/use-vana-connect.cjs.map +1 -0
- package/dist/direct/use-vana-connect.d.ts +59 -0
- package/dist/direct/use-vana-connect.js +64 -0
- package/dist/direct/use-vana-connect.js.map +1 -0
- package/dist/direct/use-vana-connect.test.d.ts +1 -0
- package/dist/react.cjs +10 -2
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.ts +4 -1
- package/dist/react.js +11 -1
- package/dist/react.js.map +1 -1
- package/dist/server.cjs +44 -2
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.ts +6 -2
- package/dist/server.js +49 -3
- package/dist/server.js.map +1 -1
- package/package.json +11 -1
package/README.md
CHANGED
|
@@ -457,6 +457,137 @@ pass the same explicit `idempotencyKey` on the retry to avoid a duplicate DCR.
|
|
|
457
457
|
`react` is an optional peer dependency. The underlying
|
|
458
458
|
`createDirectConnectFlow` store is also exported for non-React frontends.
|
|
459
459
|
|
|
460
|
+
### Connect in Vana's in-page sheet
|
|
461
|
+
|
|
462
|
+
Instead of a Vana tab, the person can approve in a sheet Vana draws over your
|
|
463
|
+
page (it signs them in, shows the request, and finishes inside the sheet). The
|
|
464
|
+
sheet is `presentation: "sheet"` of the same flow: the request, the status poll
|
|
465
|
+
and the read are unchanged. The integration is two files.
|
|
466
|
+
|
|
467
|
+
Backend: one catch-all route serves `config`, `request`, `status` and `data`,
|
|
468
|
+
binding each request to the browser that created it.
|
|
469
|
+
|
|
470
|
+
```ts
|
|
471
|
+
// app/api/vana/[action]/route.ts
|
|
472
|
+
import { createVanaConnectHandler } from "@opendatalabs/vana-sdk/server";
|
|
473
|
+
import { vana } from "@/lib/vana"; // your createDirectDataController(...)
|
|
474
|
+
|
|
475
|
+
const handler = createVanaConnectHandler({
|
|
476
|
+
controller: vana,
|
|
477
|
+
returnUrl: `${process.env.APP_URL}/connect/return`,
|
|
478
|
+
clientId: "my-app", // the embed client id Vana Account registered for your origin
|
|
479
|
+
// Recommended in production: only your signed-in users create requests.
|
|
480
|
+
session: async (request) => (await getSession(request))?.userId ?? null,
|
|
481
|
+
});
|
|
482
|
+
export { handler as GET, handler as POST };
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
The handler reports the Vana Account origin from the controller's `env`
|
|
486
|
+
(`account.vana.org` for `production`, `account-dev.vana.org` for `dev`), so a
|
|
487
|
+
dev app never frames production.
|
|
488
|
+
|
|
489
|
+
Frontend:
|
|
490
|
+
|
|
491
|
+
```tsx
|
|
492
|
+
"use client";
|
|
493
|
+
import { useVanaConnect } from "@opendatalabs/vana-sdk/react";
|
|
494
|
+
|
|
495
|
+
export function ConnectButton() {
|
|
496
|
+
const { state, start } = useVanaConnect<MyData>({ endpoint: "/api/vana" });
|
|
497
|
+
if (state.type === "done") return <Result data={state.result.data} />;
|
|
498
|
+
return (
|
|
499
|
+
<>
|
|
500
|
+
<button
|
|
501
|
+
disabled={
|
|
502
|
+
state.type === "creating" || state.type === "awaiting_approval"
|
|
503
|
+
}
|
|
504
|
+
onClick={start}
|
|
505
|
+
>
|
|
506
|
+
Connect with Vana
|
|
507
|
+
</button>
|
|
508
|
+
{state.type === "awaiting_approval" && state.popupBlocked && (
|
|
509
|
+
// The sheet could not run here (state.sheetUnavailable says why).
|
|
510
|
+
<a href={state.request.approvalUrl} target="_blank" rel="noreferrer">
|
|
511
|
+
Open Vana
|
|
512
|
+
</a>
|
|
513
|
+
)}
|
|
514
|
+
</>
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
- While the sheet is up, `state.sheet` reports `completion` (`in_browser` or
|
|
520
|
+
`remote_collect`) once the person approves, coarse `progress`
|
|
521
|
+
(`collecting`, `writing`), and any `error` it is showing.
|
|
522
|
+
- **Approval is not delivery.** The sheet's `connected` event (and
|
|
523
|
+
`state.sheet.completion`) means the person approved; the data may still be
|
|
524
|
+
collecting. The flow reaches `done` only when your backend's status poll
|
|
525
|
+
says the request is readable and the read succeeds, and `error` when the
|
|
526
|
+
poll reports `denied`, `expired` or `completed` or the read fails. The sheet
|
|
527
|
+
sends no terminal event of its own; its events only make the flow poll
|
|
528
|
+
sooner.
|
|
529
|
+
- Closing the sheet before approving returns to `idle`. Closing it after
|
|
530
|
+
approving keeps polling, since the data may still be readable.
|
|
531
|
+
- If the frame cannot run (`frame_blocked`, `origin_not_registered`,
|
|
532
|
+
`incompatible_version`), the flow falls back to the tab presentation with
|
|
533
|
+
`popupBlocked: true`, so the "Open approval" link above still finishes it.
|
|
534
|
+
`origin_not_registered` arrives at once: the launcher asks Vana
|
|
535
|
+
(`GET <account>/api/embed/connect/client`, no credentials) alongside
|
|
536
|
+
framing. `vana_unavailable` means Vana's own side is down or misconfigured;
|
|
537
|
+
the sheet stays open and says so.
|
|
538
|
+
- The partner reads; it cannot write through the sheet. Pass
|
|
539
|
+
`sheet: { closeAfterDoneMs: null }` to leave the sheet open after the read.
|
|
540
|
+
- If your page sets a CSP, allow the frame and the client check:
|
|
541
|
+
`vanaConnectCsp(env)` from `/server` (or `/connect/protocol`) returns
|
|
542
|
+
`{ "frame-src": [accountOrigin], "connect-src": [accountOrigin] }`. It has
|
|
543
|
+
no default environment.
|
|
544
|
+
- `createVanaConnectHandler` takes `session` to bind requests to your own
|
|
545
|
+
signed-in user (recommended: without it anyone who loads your page can
|
|
546
|
+
create requests as your app), `createOptions` for per-request create input
|
|
547
|
+
(questions, owner), and `read` to replace the default `readApprovedData`.
|
|
548
|
+
Use `toNodeHandler(handler)` for Express or plain `http`.
|
|
549
|
+
- Creating a request is rate limited. The default, `createMemoryRateLimit()`,
|
|
550
|
+
allows 10 creates a minute per client IP and per session, in memory. It is
|
|
551
|
+
not production-grade (each instance counts alone, and the IP comes from
|
|
552
|
+
proxy headers): pass `rateLimit` backed by a shared store, or `false` when
|
|
553
|
+
a limiter already sits in front of the route. A refusal is
|
|
554
|
+
`429 rate_limited` with `Retry-After`.
|
|
555
|
+
|
|
556
|
+
Choosing the Vana Account origin yourself (a custom flow, or preview deploys
|
|
557
|
+
of Vana Account) goes through `resolveVanaAccountOrigin`; never match origins
|
|
558
|
+
with your own pattern. It returns the environment's origin unless overrides
|
|
559
|
+
are switched on for this build and the override is exactly an `https` origin
|
|
560
|
+
on your allowlist:
|
|
561
|
+
|
|
562
|
+
```ts
|
|
563
|
+
import {
|
|
564
|
+
parseOriginAllowlist,
|
|
565
|
+
resolveVanaAccountOrigin,
|
|
566
|
+
} from "@opendatalabs/vana-sdk/connect";
|
|
567
|
+
|
|
568
|
+
const accountOrigin = resolveVanaAccountOrigin({
|
|
569
|
+
env: "dev",
|
|
570
|
+
override: new URLSearchParams(location.search).get("vana_account"),
|
|
571
|
+
previewOrigins: parseOriginAllowlist(
|
|
572
|
+
process.env.NEXT_PUBLIC_VANA_ACCOUNT_PREVIEW_ORIGINS,
|
|
573
|
+
),
|
|
574
|
+
// From build-time config, never the request. Off in production.
|
|
575
|
+
allowOverride: process.env.NEXT_PUBLIC_DEPLOY_ENV === "preview",
|
|
576
|
+
});
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
`createDirectConnectFlow` and the launcher take that origin (or an `env`)
|
|
580
|
+
explicitly; neither falls back to production.
|
|
581
|
+
|
|
582
|
+
Lower-level pieces: `createDirectConnectFlow(transports, { presentation:
|
|
583
|
+
"sheet", sheet: { clientId, appName, accountOrigin } })` with an
|
|
584
|
+
`authorizeRequest` transport backed by
|
|
585
|
+
`controller.authorizeRequestRead(requestId)`; the launcher itself
|
|
586
|
+
(`@opendatalabs/vana-sdk/connect`); and the zero-dependency message protocol
|
|
587
|
+
(`@opendatalabs/vana-sdk/connect/protocol`). A backend in another language
|
|
588
|
+
implements the [connect wire contract](./src/connect/WIRE_CONTRACT.md), which has test
|
|
589
|
+
vectors.
|
|
590
|
+
|
|
460
591
|
### Test with large sample data
|
|
461
592
|
|
|
462
593
|
When testing with realistic exports, use the public fixture catalog in
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var account_origin_exports = {};
|
|
20
|
+
__export(account_origin_exports, {
|
|
21
|
+
parseOriginAllowlist: () => parseOriginAllowlist,
|
|
22
|
+
resolveVanaAccountOrigin: () => resolveVanaAccountOrigin
|
|
23
|
+
});
|
|
24
|
+
module.exports = __toCommonJS(account_origin_exports);
|
|
25
|
+
var import_protocol = require("./protocol");
|
|
26
|
+
const LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
27
|
+
const HOSTNAME = /^(?:\[[0-9a-f:.]+\]|[a-z0-9-]+(?:\.[a-z0-9-]+)*)$/;
|
|
28
|
+
function exactOrigin(value, protocol) {
|
|
29
|
+
let url;
|
|
30
|
+
try {
|
|
31
|
+
url = new URL(value);
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
return url.protocol === protocol && url.origin === value && HOSTNAME.test(url.hostname) ? value : null;
|
|
36
|
+
}
|
|
37
|
+
function parseOriginAllowlist(csv) {
|
|
38
|
+
if (!csv) return [];
|
|
39
|
+
const origins = [];
|
|
40
|
+
for (const entry of csv.split(",")) {
|
|
41
|
+
const origin = exactOrigin(entry.trim(), "https:");
|
|
42
|
+
if (origin && !origins.includes(origin)) origins.push(origin);
|
|
43
|
+
}
|
|
44
|
+
return origins;
|
|
45
|
+
}
|
|
46
|
+
function resolveVanaAccountOrigin(input) {
|
|
47
|
+
if (input.env !== "production" && input.env !== "dev") {
|
|
48
|
+
throw new TypeError('env must be "production" or "dev"');
|
|
49
|
+
}
|
|
50
|
+
const fallback = import_protocol.VANA_ACCOUNT_ORIGINS[input.env];
|
|
51
|
+
const { override } = input;
|
|
52
|
+
if (input.allowOverride !== true || typeof override !== "string") {
|
|
53
|
+
return fallback;
|
|
54
|
+
}
|
|
55
|
+
const https = exactOrigin(override, "https:");
|
|
56
|
+
if (https && (input.previewOrigins ?? []).includes(https)) {
|
|
57
|
+
return https;
|
|
58
|
+
}
|
|
59
|
+
if (input.allowLocal === true) {
|
|
60
|
+
const local = exactOrigin(override, "http:");
|
|
61
|
+
if (local && LOOPBACK_HOSTS.has(new URL(local).hostname)) {
|
|
62
|
+
return local;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return fallback;
|
|
66
|
+
}
|
|
67
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
68
|
+
0 && (module.exports = {
|
|
69
|
+
parseOriginAllowlist,
|
|
70
|
+
resolveVanaAccountOrigin
|
|
71
|
+
});
|
|
72
|
+
//# sourceMappingURL=account-origin.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/connect/account-origin.ts"],"sourcesContent":["/**\n * Which Vana Account origin serves the connect sheet.\n *\n * @remarks\n * The sheet origin decides who the person signs in to and who receives the\n * partner's request capability, so it must never come from a pattern the\n * page URL can satisfy. {@link resolveVanaAccountOrigin} returns the\n * environment's Vana Account origin and accepts an override only when the\n * caller switched overrides on and the override is an exact origin from an\n * explicit allowlist. There is deliberately no pattern or wildcard option.\n *\n * Zero-dependency and browser-safe, like the protocol it builds on.\n *\n * @category Connect\n * @module connect/account-origin\n */\n\nimport { VANA_ACCOUNT_ORIGINS, type VanaConnectEnv } from \"./protocol\";\n\n/** Input for {@link resolveVanaAccountOrigin}. */\nexport interface ResolveVanaAccountOriginInput {\n /** The deployment environment the app talks to. Required: there is no default. */\n env: VanaConnectEnv;\n /**\n * A requested Account origin, typically read from a query parameter on a\n * preview deploy. Untrusted. Ignored unless {@link allowOverride} is true.\n */\n override?: string | null;\n /**\n * Exact `https` origins (`https://host[:port]`, no path, no trailing slash)\n * an override may name. Build it with {@link parseOriginAllowlist}. Entries\n * that are not exact https origins never match.\n */\n previewOrigins?: readonly string[];\n /**\n * Whether this build honours an override at all. Decide it from build-time\n * configuration (a non-production deploy), never from the request. Keep it\n * `false` in production.\n */\n allowOverride: boolean;\n /**\n * Also accept an exact `http` loopback origin (`localhost`, `127.0.0.1`,\n * `[::1]`, any port) without listing it, for a locally run Vana Account.\n * Only applies when {@link allowOverride} is true. Defaults to `false`.\n */\n allowLocal?: boolean;\n}\n\nconst LOOPBACK_HOSTS = new Set([\"localhost\", \"127.0.0.1\", \"[::1]\"]);\n/** DNS labels (punycode included) or a bracketed IPv6 literal; no wildcards. */\nconst HOSTNAME = /^(?:\\[[0-9a-f:.]+\\]|[a-z0-9-]+(?:\\.[a-z0-9-]+)*)$/;\n\n/** `value` itself when it is a bare origin with `protocol`, else `null`. */\nfunction exactOrigin(\n value: string,\n protocol: \"https:\" | \"http:\",\n): string | null {\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n return null;\n }\n // `url.origin === value` rejects paths, queries, fragments, credentials,\n // trailing slashes and any spelling the URL parser would rewrite.\n return url.protocol === protocol &&\n url.origin === value &&\n HOSTNAME.test(url.hostname)\n ? value\n : null;\n}\n\n/**\n * Parse a comma-separated allowlist of exact `https` origins.\n *\n * @remarks\n * Whitespace around entries is trimmed. An entry that is not an exact\n * `https://host[:port]` origin (a path, a trailing slash, `http:`, a\n * wildcard such as `https://*.vercel.app`) is dropped, so a typo narrows\n * the list instead of widening it.\n *\n * @example\n * ```typescript\n * parseOriginAllowlist(process.env.VANA_ACCOUNT_PREVIEW_ORIGINS);\n * // [\"https://account-git-my-branch.vercel.app\"]\n * ```\n *\n * @param csv - The raw list, for example an environment variable.\n * @returns The exact https origins, deduplicated, in order.\n */\nexport function parseOriginAllowlist(csv: string | null | undefined): string[] {\n if (!csv) return [];\n const origins: string[] = [];\n for (const entry of csv.split(\",\")) {\n const origin = exactOrigin(entry.trim(), \"https:\");\n if (origin && !origins.includes(origin)) origins.push(origin);\n }\n return origins;\n}\n\n/**\n * Resolve the Vana Account origin that serves the connect sheet.\n *\n * @remarks\n * Returns `VANA_ACCOUNT_ORIGINS[env]` unless `allowOverride` is true and\n * `override` is exactly one of `previewOrigins` (an `https` origin), or,\n * with `allowLocal`, exactly an `http` loopback origin. Anything else (a\n * path, a different scheme, a near-miss host, a lookalike) falls back to the\n * environment's origin. Matching is exact string equality: no patterns, no\n * suffixes, no normalization that could widen a match.\n *\n * @example\n * ```typescript\n * const accountOrigin = resolveVanaAccountOrigin({\n * env: \"dev\",\n * override: new URLSearchParams(location.search).get(\"vana_account\"),\n * previewOrigins: parseOriginAllowlist(\n * process.env.NEXT_PUBLIC_VANA_ACCOUNT_PREVIEW_ORIGINS,\n * ),\n * allowOverride: process.env.NEXT_PUBLIC_DEPLOY_ENV === \"preview\",\n * });\n * ```\n *\n * @param input - Environment, untrusted override, allowlist and switches.\n * @returns A bare Vana Account origin.\n * @throws TypeError when `env` is not `\"production\"` or `\"dev\"`.\n */\nexport function resolveVanaAccountOrigin(\n input: ResolveVanaAccountOriginInput,\n): string {\n if (input.env !== \"production\" && input.env !== \"dev\") {\n throw new TypeError('env must be \"production\" or \"dev\"');\n }\n const fallback: string = VANA_ACCOUNT_ORIGINS[input.env];\n const { override } = input;\n if (input.allowOverride !== true || typeof override !== \"string\") {\n return fallback;\n }\n const https = exactOrigin(override, \"https:\");\n if (https && (input.previewOrigins ?? []).includes(https)) {\n return https;\n }\n if (input.allowLocal === true) {\n const local = exactOrigin(override, \"http:\");\n if (local && LOOPBACK_HOSTS.has(new URL(local).hostname)) {\n return local;\n }\n }\n return fallback;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiBA,sBAA0D;AA+B1D,MAAM,iBAAiB,oBAAI,IAAI,CAAC,aAAa,aAAa,OAAO,CAAC;AAElE,MAAM,WAAW;AAGjB,SAAS,YACP,OACA,UACe;AACf,MAAI;AACJ,MAAI;AACF,UAAM,IAAI,IAAI,KAAK;AAAA,EACrB,QAAQ;AACN,WAAO;AAAA,EACT;AAGA,SAAO,IAAI,aAAa,YACtB,IAAI,WAAW,SACf,SAAS,KAAK,IAAI,QAAQ,IACxB,QACA;AACN;AAoBO,SAAS,qBAAqB,KAA0C;AAC7E,MAAI,CAAC,IAAK,QAAO,CAAC;AAClB,QAAM,UAAoB,CAAC;AAC3B,aAAW,SAAS,IAAI,MAAM,GAAG,GAAG;AAClC,UAAM,SAAS,YAAY,MAAM,KAAK,GAAG,QAAQ;AACjD,QAAI,UAAU,CAAC,QAAQ,SAAS,MAAM,EAAG,SAAQ,KAAK,MAAM;AAAA,EAC9D;AACA,SAAO;AACT;AA6BO,SAAS,yBACd,OACQ;AACR,MAAI,MAAM,QAAQ,gBAAgB,MAAM,QAAQ,OAAO;AACrD,UAAM,IAAI,UAAU,mCAAmC;AAAA,EACzD;AACA,QAAM,WAAmB,qCAAqB,MAAM,GAAG;AACvD,QAAM,EAAE,SAAS,IAAI;AACrB,MAAI,MAAM,kBAAkB,QAAQ,OAAO,aAAa,UAAU;AAChE,WAAO;AAAA,EACT;AACA,QAAM,QAAQ,YAAY,UAAU,QAAQ;AAC5C,MAAI,UAAU,MAAM,kBAAkB,CAAC,GAAG,SAAS,KAAK,GAAG;AACzD,WAAO;AAAA,EACT;AACA,MAAI,MAAM,eAAe,MAAM;AAC7B,UAAM,QAAQ,YAAY,UAAU,OAAO;AAC3C,QAAI,SAAS,eAAe,IAAI,IAAI,IAAI,KAAK,EAAE,QAAQ,GAAG;AACxD,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;","names":[]}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which Vana Account origin serves the connect sheet.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* The sheet origin decides who the person signs in to and who receives the
|
|
6
|
+
* partner's request capability, so it must never come from a pattern the
|
|
7
|
+
* page URL can satisfy. {@link resolveVanaAccountOrigin} returns the
|
|
8
|
+
* environment's Vana Account origin and accepts an override only when the
|
|
9
|
+
* caller switched overrides on and the override is an exact origin from an
|
|
10
|
+
* explicit allowlist. There is deliberately no pattern or wildcard option.
|
|
11
|
+
*
|
|
12
|
+
* Zero-dependency and browser-safe, like the protocol it builds on.
|
|
13
|
+
*
|
|
14
|
+
* @category Connect
|
|
15
|
+
* @module connect/account-origin
|
|
16
|
+
*/
|
|
17
|
+
import { type VanaConnectEnv } from "./protocol.js";
|
|
18
|
+
/** Input for {@link resolveVanaAccountOrigin}. */
|
|
19
|
+
export interface ResolveVanaAccountOriginInput {
|
|
20
|
+
/** The deployment environment the app talks to. Required: there is no default. */
|
|
21
|
+
env: VanaConnectEnv;
|
|
22
|
+
/**
|
|
23
|
+
* A requested Account origin, typically read from a query parameter on a
|
|
24
|
+
* preview deploy. Untrusted. Ignored unless {@link allowOverride} is true.
|
|
25
|
+
*/
|
|
26
|
+
override?: string | null;
|
|
27
|
+
/**
|
|
28
|
+
* Exact `https` origins (`https://host[:port]`, no path, no trailing slash)
|
|
29
|
+
* an override may name. Build it with {@link parseOriginAllowlist}. Entries
|
|
30
|
+
* that are not exact https origins never match.
|
|
31
|
+
*/
|
|
32
|
+
previewOrigins?: readonly string[];
|
|
33
|
+
/**
|
|
34
|
+
* Whether this build honours an override at all. Decide it from build-time
|
|
35
|
+
* configuration (a non-production deploy), never from the request. Keep it
|
|
36
|
+
* `false` in production.
|
|
37
|
+
*/
|
|
38
|
+
allowOverride: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Also accept an exact `http` loopback origin (`localhost`, `127.0.0.1`,
|
|
41
|
+
* `[::1]`, any port) without listing it, for a locally run Vana Account.
|
|
42
|
+
* Only applies when {@link allowOverride} is true. Defaults to `false`.
|
|
43
|
+
*/
|
|
44
|
+
allowLocal?: boolean;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Parse a comma-separated allowlist of exact `https` origins.
|
|
48
|
+
*
|
|
49
|
+
* @remarks
|
|
50
|
+
* Whitespace around entries is trimmed. An entry that is not an exact
|
|
51
|
+
* `https://host[:port]` origin (a path, a trailing slash, `http:`, a
|
|
52
|
+
* wildcard such as `https://*.vercel.app`) is dropped, so a typo narrows
|
|
53
|
+
* the list instead of widening it.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```typescript
|
|
57
|
+
* parseOriginAllowlist(process.env.VANA_ACCOUNT_PREVIEW_ORIGINS);
|
|
58
|
+
* // ["https://account-git-my-branch.vercel.app"]
|
|
59
|
+
* ```
|
|
60
|
+
*
|
|
61
|
+
* @param csv - The raw list, for example an environment variable.
|
|
62
|
+
* @returns The exact https origins, deduplicated, in order.
|
|
63
|
+
*/
|
|
64
|
+
export declare function parseOriginAllowlist(csv: string | null | undefined): string[];
|
|
65
|
+
/**
|
|
66
|
+
* Resolve the Vana Account origin that serves the connect sheet.
|
|
67
|
+
*
|
|
68
|
+
* @remarks
|
|
69
|
+
* Returns `VANA_ACCOUNT_ORIGINS[env]` unless `allowOverride` is true and
|
|
70
|
+
* `override` is exactly one of `previewOrigins` (an `https` origin), or,
|
|
71
|
+
* with `allowLocal`, exactly an `http` loopback origin. Anything else (a
|
|
72
|
+
* path, a different scheme, a near-miss host, a lookalike) falls back to the
|
|
73
|
+
* environment's origin. Matching is exact string equality: no patterns, no
|
|
74
|
+
* suffixes, no normalization that could widen a match.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```typescript
|
|
78
|
+
* const accountOrigin = resolveVanaAccountOrigin({
|
|
79
|
+
* env: "dev",
|
|
80
|
+
* override: new URLSearchParams(location.search).get("vana_account"),
|
|
81
|
+
* previewOrigins: parseOriginAllowlist(
|
|
82
|
+
* process.env.NEXT_PUBLIC_VANA_ACCOUNT_PREVIEW_ORIGINS,
|
|
83
|
+
* ),
|
|
84
|
+
* allowOverride: process.env.NEXT_PUBLIC_DEPLOY_ENV === "preview",
|
|
85
|
+
* });
|
|
86
|
+
* ```
|
|
87
|
+
*
|
|
88
|
+
* @param input - Environment, untrusted override, allowlist and switches.
|
|
89
|
+
* @returns A bare Vana Account origin.
|
|
90
|
+
* @throws TypeError when `env` is not `"production"` or `"dev"`.
|
|
91
|
+
*/
|
|
92
|
+
export declare function resolveVanaAccountOrigin(input: ResolveVanaAccountOriginInput): string;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { VANA_ACCOUNT_ORIGINS } from "./protocol.js";
|
|
2
|
+
const LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
3
|
+
const HOSTNAME = /^(?:\[[0-9a-f:.]+\]|[a-z0-9-]+(?:\.[a-z0-9-]+)*)$/;
|
|
4
|
+
function exactOrigin(value, protocol) {
|
|
5
|
+
let url;
|
|
6
|
+
try {
|
|
7
|
+
url = new URL(value);
|
|
8
|
+
} catch {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
return url.protocol === protocol && url.origin === value && HOSTNAME.test(url.hostname) ? value : null;
|
|
12
|
+
}
|
|
13
|
+
function parseOriginAllowlist(csv) {
|
|
14
|
+
if (!csv) return [];
|
|
15
|
+
const origins = [];
|
|
16
|
+
for (const entry of csv.split(",")) {
|
|
17
|
+
const origin = exactOrigin(entry.trim(), "https:");
|
|
18
|
+
if (origin && !origins.includes(origin)) origins.push(origin);
|
|
19
|
+
}
|
|
20
|
+
return origins;
|
|
21
|
+
}
|
|
22
|
+
function resolveVanaAccountOrigin(input) {
|
|
23
|
+
if (input.env !== "production" && input.env !== "dev") {
|
|
24
|
+
throw new TypeError('env must be "production" or "dev"');
|
|
25
|
+
}
|
|
26
|
+
const fallback = VANA_ACCOUNT_ORIGINS[input.env];
|
|
27
|
+
const { override } = input;
|
|
28
|
+
if (input.allowOverride !== true || typeof override !== "string") {
|
|
29
|
+
return fallback;
|
|
30
|
+
}
|
|
31
|
+
const https = exactOrigin(override, "https:");
|
|
32
|
+
if (https && (input.previewOrigins ?? []).includes(https)) {
|
|
33
|
+
return https;
|
|
34
|
+
}
|
|
35
|
+
if (input.allowLocal === true) {
|
|
36
|
+
const local = exactOrigin(override, "http:");
|
|
37
|
+
if (local && LOOPBACK_HOSTS.has(new URL(local).hostname)) {
|
|
38
|
+
return local;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return fallback;
|
|
42
|
+
}
|
|
43
|
+
export {
|
|
44
|
+
parseOriginAllowlist,
|
|
45
|
+
resolveVanaAccountOrigin
|
|
46
|
+
};
|
|
47
|
+
//# sourceMappingURL=account-origin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/connect/account-origin.ts"],"sourcesContent":["/**\n * Which Vana Account origin serves the connect sheet.\n *\n * @remarks\n * The sheet origin decides who the person signs in to and who receives the\n * partner's request capability, so it must never come from a pattern the\n * page URL can satisfy. {@link resolveVanaAccountOrigin} returns the\n * environment's Vana Account origin and accepts an override only when the\n * caller switched overrides on and the override is an exact origin from an\n * explicit allowlist. There is deliberately no pattern or wildcard option.\n *\n * Zero-dependency and browser-safe, like the protocol it builds on.\n *\n * @category Connect\n * @module connect/account-origin\n */\n\nimport { VANA_ACCOUNT_ORIGINS, type VanaConnectEnv } from \"./protocol\";\n\n/** Input for {@link resolveVanaAccountOrigin}. */\nexport interface ResolveVanaAccountOriginInput {\n /** The deployment environment the app talks to. Required: there is no default. */\n env: VanaConnectEnv;\n /**\n * A requested Account origin, typically read from a query parameter on a\n * preview deploy. Untrusted. Ignored unless {@link allowOverride} is true.\n */\n override?: string | null;\n /**\n * Exact `https` origins (`https://host[:port]`, no path, no trailing slash)\n * an override may name. Build it with {@link parseOriginAllowlist}. Entries\n * that are not exact https origins never match.\n */\n previewOrigins?: readonly string[];\n /**\n * Whether this build honours an override at all. Decide it from build-time\n * configuration (a non-production deploy), never from the request. Keep it\n * `false` in production.\n */\n allowOverride: boolean;\n /**\n * Also accept an exact `http` loopback origin (`localhost`, `127.0.0.1`,\n * `[::1]`, any port) without listing it, for a locally run Vana Account.\n * Only applies when {@link allowOverride} is true. Defaults to `false`.\n */\n allowLocal?: boolean;\n}\n\nconst LOOPBACK_HOSTS = new Set([\"localhost\", \"127.0.0.1\", \"[::1]\"]);\n/** DNS labels (punycode included) or a bracketed IPv6 literal; no wildcards. */\nconst HOSTNAME = /^(?:\\[[0-9a-f:.]+\\]|[a-z0-9-]+(?:\\.[a-z0-9-]+)*)$/;\n\n/** `value` itself when it is a bare origin with `protocol`, else `null`. */\nfunction exactOrigin(\n value: string,\n protocol: \"https:\" | \"http:\",\n): string | null {\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n return null;\n }\n // `url.origin === value` rejects paths, queries, fragments, credentials,\n // trailing slashes and any spelling the URL parser would rewrite.\n return url.protocol === protocol &&\n url.origin === value &&\n HOSTNAME.test(url.hostname)\n ? value\n : null;\n}\n\n/**\n * Parse a comma-separated allowlist of exact `https` origins.\n *\n * @remarks\n * Whitespace around entries is trimmed. An entry that is not an exact\n * `https://host[:port]` origin (a path, a trailing slash, `http:`, a\n * wildcard such as `https://*.vercel.app`) is dropped, so a typo narrows\n * the list instead of widening it.\n *\n * @example\n * ```typescript\n * parseOriginAllowlist(process.env.VANA_ACCOUNT_PREVIEW_ORIGINS);\n * // [\"https://account-git-my-branch.vercel.app\"]\n * ```\n *\n * @param csv - The raw list, for example an environment variable.\n * @returns The exact https origins, deduplicated, in order.\n */\nexport function parseOriginAllowlist(csv: string | null | undefined): string[] {\n if (!csv) return [];\n const origins: string[] = [];\n for (const entry of csv.split(\",\")) {\n const origin = exactOrigin(entry.trim(), \"https:\");\n if (origin && !origins.includes(origin)) origins.push(origin);\n }\n return origins;\n}\n\n/**\n * Resolve the Vana Account origin that serves the connect sheet.\n *\n * @remarks\n * Returns `VANA_ACCOUNT_ORIGINS[env]` unless `allowOverride` is true and\n * `override` is exactly one of `previewOrigins` (an `https` origin), or,\n * with `allowLocal`, exactly an `http` loopback origin. Anything else (a\n * path, a different scheme, a near-miss host, a lookalike) falls back to the\n * environment's origin. Matching is exact string equality: no patterns, no\n * suffixes, no normalization that could widen a match.\n *\n * @example\n * ```typescript\n * const accountOrigin = resolveVanaAccountOrigin({\n * env: \"dev\",\n * override: new URLSearchParams(location.search).get(\"vana_account\"),\n * previewOrigins: parseOriginAllowlist(\n * process.env.NEXT_PUBLIC_VANA_ACCOUNT_PREVIEW_ORIGINS,\n * ),\n * allowOverride: process.env.NEXT_PUBLIC_DEPLOY_ENV === \"preview\",\n * });\n * ```\n *\n * @param input - Environment, untrusted override, allowlist and switches.\n * @returns A bare Vana Account origin.\n * @throws TypeError when `env` is not `\"production\"` or `\"dev\"`.\n */\nexport function resolveVanaAccountOrigin(\n input: ResolveVanaAccountOriginInput,\n): string {\n if (input.env !== \"production\" && input.env !== \"dev\") {\n throw new TypeError('env must be \"production\" or \"dev\"');\n }\n const fallback: string = VANA_ACCOUNT_ORIGINS[input.env];\n const { override } = input;\n if (input.allowOverride !== true || typeof override !== \"string\") {\n return fallback;\n }\n const https = exactOrigin(override, \"https:\");\n if (https && (input.previewOrigins ?? []).includes(https)) {\n return https;\n }\n if (input.allowLocal === true) {\n const local = exactOrigin(override, \"http:\");\n if (local && LOOPBACK_HOSTS.has(new URL(local).hostname)) {\n return local;\n }\n }\n return fallback;\n}\n"],"mappings":"AAiBA,SAAS,4BAAiD;AA+B1D,MAAM,iBAAiB,oBAAI,IAAI,CAAC,aAAa,aAAa,OAAO,CAAC;AAElE,MAAM,WAAW;AAGjB,SAAS,YACP,OACA,UACe;AACf,MAAI;AACJ,MAAI;AACF,UAAM,IAAI,IAAI,KAAK;AAAA,EACrB,QAAQ;AACN,WAAO;AAAA,EACT;AAGA,SAAO,IAAI,aAAa,YACtB,IAAI,WAAW,SACf,SAAS,KAAK,IAAI,QAAQ,IACxB,QACA;AACN;AAoBO,SAAS,qBAAqB,KAA0C;AAC7E,MAAI,CAAC,IAAK,QAAO,CAAC;AAClB,QAAM,UAAoB,CAAC;AAC3B,aAAW,SAAS,IAAI,MAAM,GAAG,GAAG;AAClC,UAAM,SAAS,YAAY,MAAM,KAAK,GAAG,QAAQ;AACjD,QAAI,UAAU,CAAC,QAAQ,SAAS,MAAM,EAAG,SAAQ,KAAK,MAAM;AAAA,EAC9D;AACA,SAAO;AACT;AA6BO,SAAS,yBACd,OACQ;AACR,MAAI,MAAM,QAAQ,gBAAgB,MAAM,QAAQ,OAAO;AACrD,UAAM,IAAI,UAAU,mCAAmC;AAAA,EACzD;AACA,QAAM,WAAmB,qBAAqB,MAAM,GAAG;AACvD,QAAM,EAAE,SAAS,IAAI;AACrB,MAAI,MAAM,kBAAkB,QAAQ,OAAO,aAAa,UAAU;AAChE,WAAO;AAAA,EACT;AACA,QAAM,QAAQ,YAAY,UAAU,QAAQ;AAC5C,MAAI,UAAU,MAAM,kBAAkB,CAAC,GAAG,SAAS,KAAK,GAAG;AACzD,WAAO;AAAA,EACT;AACA,MAAI,MAAM,eAAe,MAAM;AAC7B,UAAM,QAAQ,YAAY,UAAU,OAAO;AAC3C,QAAI,SAAS,eAAe,IAAI,IAAI,IAAI,KAAK,EAAE,QAAQ,GAAG;AACxD,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var chrome_exports = {};
|
|
20
|
+
__export(chrome_exports, {
|
|
21
|
+
DRAG_CLOSE_PX: () => DRAG_CLOSE_PX,
|
|
22
|
+
MAX_SHEET_RATIO: () => MAX_SHEET_RATIO,
|
|
23
|
+
RETURN_TAP_GUARD_MS: () => RETURN_TAP_GUARD_MS,
|
|
24
|
+
createSheetChrome: () => createSheetChrome,
|
|
25
|
+
sheetTopPx: () => sheetTopPx
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(chrome_exports);
|
|
28
|
+
const SHEET_RADIUS_PX = 28;
|
|
29
|
+
const SHEET_MAX_WIDTH_PX = 480;
|
|
30
|
+
const HANDLE_STRIP_PX = 20;
|
|
31
|
+
const INITIAL_CONTENT_PX = 320;
|
|
32
|
+
const TRANSITION_MS = 280;
|
|
33
|
+
const SHEET_BACKGROUND = "#f4f4f4";
|
|
34
|
+
const DRAG_CLOSE_PX = 120;
|
|
35
|
+
const MAX_SHEET_RATIO = 0.92;
|
|
36
|
+
const STYLES = `
|
|
37
|
+
:host { all: initial; }
|
|
38
|
+
.scrim {
|
|
39
|
+
position: fixed; inset: 0; z-index: 2147483646;
|
|
40
|
+
background: rgb(0 0 0 / 0.4);
|
|
41
|
+
opacity: 0; transition: opacity ${TRANSITION_MS}ms ease;
|
|
42
|
+
}
|
|
43
|
+
.sheet {
|
|
44
|
+
position: fixed; left: 0; right: 0; z-index: 2147483647;
|
|
45
|
+
margin: 0 auto; max-width: ${SHEET_MAX_WIDTH_PX}px;
|
|
46
|
+
top: 100vh;
|
|
47
|
+
transition: top ${TRANSITION_MS}ms cubic-bezier(0.32, 0.72, 0, 1);
|
|
48
|
+
}
|
|
49
|
+
.sheet.dragging { transition: none; }
|
|
50
|
+
.handle {
|
|
51
|
+
height: ${HANDLE_STRIP_PX}px;
|
|
52
|
+
display: flex; justify-content: center; align-items: center;
|
|
53
|
+
background: ${SHEET_BACKGROUND};
|
|
54
|
+
border-radius: ${SHEET_RADIUS_PX}px ${SHEET_RADIUS_PX}px 0 0;
|
|
55
|
+
touch-action: none; cursor: grab;
|
|
56
|
+
}
|
|
57
|
+
.handle::before {
|
|
58
|
+
content: ""; width: 36px; height: 5px; border-radius: 3px;
|
|
59
|
+
background: rgb(0 0 0 / 0.18);
|
|
60
|
+
}
|
|
61
|
+
iframe {
|
|
62
|
+
display: block; width: 100%; border: 0;
|
|
63
|
+
background: ${SHEET_BACKGROUND};
|
|
64
|
+
}
|
|
65
|
+
.open .scrim { opacity: 1; }
|
|
66
|
+
@media (prefers-reduced-motion: reduce) {
|
|
67
|
+
.scrim, .sheet { transition: none; }
|
|
68
|
+
}
|
|
69
|
+
`;
|
|
70
|
+
function viewportPx() {
|
|
71
|
+
return window.visualViewport?.height ?? window.innerHeight;
|
|
72
|
+
}
|
|
73
|
+
function capPx() {
|
|
74
|
+
return Math.round(viewportPx() * MAX_SHEET_RATIO);
|
|
75
|
+
}
|
|
76
|
+
function placeSheet(sheet, content) {
|
|
77
|
+
sheet.style.top = `${sheetTopPx(viewportPx(), content)}px`;
|
|
78
|
+
}
|
|
79
|
+
function sheetTopPx(viewport, content) {
|
|
80
|
+
const cap = Math.round(viewport * MAX_SHEET_RATIO);
|
|
81
|
+
const visible = Math.min(content, cap - HANDLE_STRIP_PX);
|
|
82
|
+
return Math.round(viewport - HANDLE_STRIP_PX - visible);
|
|
83
|
+
}
|
|
84
|
+
const RETURN_TAP_GUARD_MS = 500;
|
|
85
|
+
function installReturnGuard(sheet, iframe, state) {
|
|
86
|
+
const fitIframe = () => {
|
|
87
|
+
iframe.style.height = `${capPx() - HANDLE_STRIP_PX}px`;
|
|
88
|
+
if (state.shown) {
|
|
89
|
+
placeSheet(sheet, state.lastHeight ?? INITIAL_CONTENT_PX);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
const onReturn = () => {
|
|
93
|
+
if (document.visibilityState !== "visible") {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
state.returnedAt = performance.now();
|
|
97
|
+
fitIframe();
|
|
98
|
+
};
|
|
99
|
+
fitIframe();
|
|
100
|
+
window.addEventListener("resize", fitIframe);
|
|
101
|
+
window.visualViewport?.addEventListener("resize", fitIframe);
|
|
102
|
+
document.addEventListener("visibilitychange", onReturn);
|
|
103
|
+
window.addEventListener("focus", onReturn);
|
|
104
|
+
return () => {
|
|
105
|
+
window.removeEventListener("resize", fitIframe);
|
|
106
|
+
window.visualViewport?.removeEventListener("resize", fitIframe);
|
|
107
|
+
document.removeEventListener("visibilitychange", onReturn);
|
|
108
|
+
window.removeEventListener("focus", onReturn);
|
|
109
|
+
sheet.style.transform = "";
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function installDragToClose(sheet, handle, onClose) {
|
|
113
|
+
let startY = null;
|
|
114
|
+
handle.addEventListener("pointerdown", (event) => {
|
|
115
|
+
startY = event.clientY;
|
|
116
|
+
handle.setPointerCapture?.(event.pointerId);
|
|
117
|
+
sheet.classList.add("dragging");
|
|
118
|
+
});
|
|
119
|
+
handle.addEventListener("pointermove", (event) => {
|
|
120
|
+
if (startY === null) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const dy = Math.max(0, event.clientY - startY);
|
|
124
|
+
sheet.style.transform = `translateY(${dy}px)`;
|
|
125
|
+
});
|
|
126
|
+
const end = (event) => {
|
|
127
|
+
if (startY === null) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const dy = event.clientY - startY;
|
|
131
|
+
startY = null;
|
|
132
|
+
sheet.classList.remove("dragging");
|
|
133
|
+
sheet.style.transform = "";
|
|
134
|
+
if (dy > DRAG_CLOSE_PX) {
|
|
135
|
+
onClose("drag");
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
handle.addEventListener("pointerup", end);
|
|
139
|
+
handle.addEventListener("pointercancel", end);
|
|
140
|
+
}
|
|
141
|
+
function createSheetChrome(input) {
|
|
142
|
+
const returnState = {
|
|
143
|
+
lastHeight: null,
|
|
144
|
+
returnedAt: 0,
|
|
145
|
+
shown: false
|
|
146
|
+
};
|
|
147
|
+
const host = document.createElement("div");
|
|
148
|
+
host.setAttribute("data-vana-connect", "");
|
|
149
|
+
const root = host.attachShadow({ mode: "open" });
|
|
150
|
+
const style = document.createElement("style");
|
|
151
|
+
style.textContent = STYLES;
|
|
152
|
+
const frame = document.createElement("div");
|
|
153
|
+
const scrim = document.createElement("div");
|
|
154
|
+
scrim.className = "scrim";
|
|
155
|
+
scrim.setAttribute("data-slot", "scrim");
|
|
156
|
+
scrim.addEventListener("click", () => {
|
|
157
|
+
if (performance.now() - returnState.returnedAt < RETURN_TAP_GUARD_MS) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
input.onClose("scrim");
|
|
161
|
+
});
|
|
162
|
+
const sheet = document.createElement("div");
|
|
163
|
+
sheet.className = "sheet";
|
|
164
|
+
sheet.setAttribute("role", "dialog");
|
|
165
|
+
sheet.setAttribute("aria-modal", "true");
|
|
166
|
+
sheet.setAttribute("aria-label", input.ariaLabel);
|
|
167
|
+
const iframe = document.createElement("iframe");
|
|
168
|
+
iframe.setAttribute(
|
|
169
|
+
"sandbox",
|
|
170
|
+
"allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox"
|
|
171
|
+
);
|
|
172
|
+
iframe.setAttribute(
|
|
173
|
+
"allow",
|
|
174
|
+
"otp-credentials; publickey-credentials-get; clipboard-write"
|
|
175
|
+
);
|
|
176
|
+
iframe.setAttribute("title", "Vana");
|
|
177
|
+
iframe.referrerPolicy = "strict-origin-when-cross-origin";
|
|
178
|
+
iframe.src = input.src;
|
|
179
|
+
const handle = document.createElement("div");
|
|
180
|
+
handle.className = "handle";
|
|
181
|
+
handle.setAttribute("aria-hidden", "true");
|
|
182
|
+
sheet.append(handle, iframe);
|
|
183
|
+
frame.append(scrim, sheet);
|
|
184
|
+
root.append(style, frame);
|
|
185
|
+
input.container.append(host);
|
|
186
|
+
const removeReturnGuard = installReturnGuard(sheet, iframe, returnState);
|
|
187
|
+
installDragToClose(sheet, handle, input.onClose);
|
|
188
|
+
return {
|
|
189
|
+
host,
|
|
190
|
+
iframe,
|
|
191
|
+
remove() {
|
|
192
|
+
removeReturnGuard();
|
|
193
|
+
host.remove();
|
|
194
|
+
},
|
|
195
|
+
setContentHeight(px) {
|
|
196
|
+
if (document.visibilityState !== "visible") {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
returnState.lastHeight = px;
|
|
200
|
+
if (returnState.shown) {
|
|
201
|
+
placeSheet(sheet, px);
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
show() {
|
|
205
|
+
requestAnimationFrame(() => {
|
|
206
|
+
returnState.shown = true;
|
|
207
|
+
frame.classList.add("open");
|
|
208
|
+
placeSheet(sheet, returnState.lastHeight ?? INITIAL_CONTENT_PX);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
214
|
+
0 && (module.exports = {
|
|
215
|
+
DRAG_CLOSE_PX,
|
|
216
|
+
MAX_SHEET_RATIO,
|
|
217
|
+
RETURN_TAP_GUARD_MS,
|
|
218
|
+
createSheetChrome,
|
|
219
|
+
sheetTopPx
|
|
220
|
+
});
|
|
221
|
+
//# sourceMappingURL=chrome.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/connect/chrome.ts"],"sourcesContent":["/**\n * Sheet chrome for the Vana connect sheet: scrim, bottom sheet and drag\n * handle, in a Shadow DOM root.\n *\n * @remarks\n * Ported unchanged from `@vana-unity/connect-embed`. See the layout notes\n * below.\n *\n * @category Connect\n * @module connect/chrome\n */\n\n// Sheet chrome: scrim, bottom sheet, drag handle. Lives in a Shadow DOM root\n// so partner CSS cannot restyle it, and the partner cannot reach the iframe's\n// document (cross-origin) through it.\n//\n// host (partner DOM)\n// └─ #shadow-root\n// ├─ .scrim click closes\n// └─ .sheet top = viewport − (handle + content)\n// ├─ .handle drag down to close (above the frame, never over it)\n// └─ iframe fixed height = cap; the unused part sits below\n// the viewport\n//\n// Nothing overlaps or clips the iframe. IntersectionObserver v2 inside the\n// sheet reports it \"not visible\" when anything covers or clips it, and the\n// consent step waits for real visibility. The part of the frame below the\n// viewport is simply off screen, which does not count as occlusion.\n\nconst SHEET_RADIUS_PX = 28;\nconst SHEET_MAX_WIDTH_PX = 480;\nconst HANDLE_STRIP_PX = 20;\nconst INITIAL_CONTENT_PX = 320;\nconst TRANSITION_MS = 280;\nconst SHEET_BACKGROUND = \"#f4f4f4\";\n\n/** Drag the handle this far down to dismiss. */\nexport const DRAG_CLOSE_PX = 120;\n\n/** The sheet never covers more of the viewport than this. */\nexport const MAX_SHEET_RATIO = 0.92;\n\nconst STYLES = `\n:host { all: initial; }\n.scrim {\n position: fixed; inset: 0; z-index: 2147483646;\n background: rgb(0 0 0 / 0.4);\n opacity: 0; transition: opacity ${TRANSITION_MS}ms ease;\n}\n.sheet {\n position: fixed; left: 0; right: 0; z-index: 2147483647;\n margin: 0 auto; max-width: ${SHEET_MAX_WIDTH_PX}px;\n top: 100vh;\n transition: top ${TRANSITION_MS}ms cubic-bezier(0.32, 0.72, 0, 1);\n}\n.sheet.dragging { transition: none; }\n.handle {\n height: ${HANDLE_STRIP_PX}px;\n display: flex; justify-content: center; align-items: center;\n background: ${SHEET_BACKGROUND};\n border-radius: ${SHEET_RADIUS_PX}px ${SHEET_RADIUS_PX}px 0 0;\n touch-action: none; cursor: grab;\n}\n.handle::before {\n content: \"\"; width: 36px; height: 5px; border-radius: 3px;\n background: rgb(0 0 0 / 0.18);\n}\niframe {\n display: block; width: 100%; border: 0;\n background: ${SHEET_BACKGROUND};\n}\n.open .scrim { opacity: 1; }\n@media (prefers-reduced-motion: reduce) {\n .scrim, .sheet { transition: none; }\n}\n`;\n\nexport type CloseReason = \"drag\" | \"scrim\";\n\nexport interface SheetChrome {\n host: HTMLElement;\n iframe: HTMLIFrameElement;\n remove(): void;\n setContentHeight(px: number): void;\n show(): void;\n}\n\nfunction viewportPx(): number {\n return window.visualViewport?.height ?? window.innerHeight;\n}\n\nfunction capPx(): number {\n return Math.round(viewportPx() * MAX_SHEET_RATIO);\n}\n\n/** Places the sheet so exactly `content` px of the frame show above the fold. */\nfunction placeSheet(sheet: HTMLElement, content: number): void {\n sheet.style.top = `${sheetTopPx(viewportPx(), content)}px`;\n}\n\n/**\n * Where the sheet's top edge goes so `content` px of the frame show, capped\n * at {@link MAX_SHEET_RATIO} of the viewport.\n *\n * @param viewport - Viewport height in px.\n * @param content - Content height the sheet reported, in px.\n * @returns The sheet's `top`, in whole px.\n */\nexport function sheetTopPx(viewport: number, content: number): number {\n const cap = Math.round(viewport * MAX_SHEET_RATIO);\n const visible = Math.min(content, cap - HANDLE_STRIP_PX);\n // Whole pixels: a fractional offset blurs the framed text.\n return Math.round(viewport - HANDLE_STRIP_PX - visible);\n}\n\n/** Scrim taps this soon after the page returns to view are ignored. */\nexport const RETURN_TAP_GUARD_MS = 500;\n\n/**\n * Return-to-view handling, isolated so it can change on its own.\n *\n * Root cause of the spike's \"blank sheet\" (scratchpad blank-render notes): not\n * a paint failure. Returning from the Google popup tab, a stray tap landed on\n * the scrim and silently closed the sheet; a headless-Chrome harness artefact\n * looked similar. So: taps on the scrim right after the page regains\n * visibility or focus are ignored, heights reported while hidden are dropped,\n * and the last good height is re-applied on return. The iframe keeps a fixed\n * viewport (the cap) and only the sheet's position animates, so measuring\n * content inside the frame never resizes the frame itself.\n */\nfunction installReturnGuard(\n sheet: HTMLElement,\n iframe: HTMLIFrameElement,\n state: { lastHeight: number | null; returnedAt: number; shown: boolean },\n): () => void {\n const fitIframe = () => {\n iframe.style.height = `${capPx() - HANDLE_STRIP_PX}px`;\n if (state.shown) {\n placeSheet(sheet, state.lastHeight ?? INITIAL_CONTENT_PX);\n }\n };\n const onReturn = () => {\n if (document.visibilityState !== \"visible\") {\n return;\n }\n state.returnedAt = performance.now();\n fitIframe();\n };\n fitIframe();\n window.addEventListener(\"resize\", fitIframe);\n window.visualViewport?.addEventListener(\"resize\", fitIframe);\n document.addEventListener(\"visibilitychange\", onReturn);\n window.addEventListener(\"focus\", onReturn);\n return () => {\n window.removeEventListener(\"resize\", fitIframe);\n window.visualViewport?.removeEventListener(\"resize\", fitIframe);\n document.removeEventListener(\"visibilitychange\", onReturn);\n window.removeEventListener(\"focus\", onReturn);\n sheet.style.transform = \"\";\n };\n}\n\nfunction installDragToClose(\n sheet: HTMLElement,\n handle: HTMLElement,\n onClose: (reason: CloseReason) => void,\n): void {\n let startY: number | null = null;\n handle.addEventListener(\"pointerdown\", (event) => {\n startY = event.clientY;\n handle.setPointerCapture?.(event.pointerId);\n sheet.classList.add(\"dragging\");\n });\n handle.addEventListener(\"pointermove\", (event) => {\n if (startY === null) {\n return;\n }\n const dy = Math.max(0, event.clientY - startY);\n sheet.style.transform = `translateY(${dy}px)`;\n });\n const end = (event: PointerEvent) => {\n if (startY === null) {\n return;\n }\n const dy = event.clientY - startY;\n startY = null;\n sheet.classList.remove(\"dragging\");\n sheet.style.transform = \"\";\n if (dy > DRAG_CLOSE_PX) {\n onClose(\"drag\");\n }\n };\n handle.addEventListener(\"pointerup\", end);\n handle.addEventListener(\"pointercancel\", end);\n}\n\nexport function createSheetChrome(input: {\n ariaLabel: string;\n container: HTMLElement;\n onClose: (reason: CloseReason) => void;\n src: string;\n}): SheetChrome {\n const returnState = {\n lastHeight: null as number | null,\n returnedAt: 0,\n shown: false,\n };\n const host = document.createElement(\"div\");\n host.setAttribute(\"data-vana-connect\", \"\");\n const root = host.attachShadow({ mode: \"open\" });\n\n const style = document.createElement(\"style\");\n style.textContent = STYLES;\n\n const frame = document.createElement(\"div\");\n const scrim = document.createElement(\"div\");\n scrim.className = \"scrim\";\n scrim.setAttribute(\"data-slot\", \"scrim\");\n scrim.addEventListener(\"click\", () => {\n if (performance.now() - returnState.returnedAt < RETURN_TAP_GUARD_MS) {\n return;\n }\n input.onClose(\"scrim\");\n });\n\n const sheet = document.createElement(\"div\");\n sheet.className = \"sheet\";\n sheet.setAttribute(\"role\", \"dialog\");\n sheet.setAttribute(\"aria-modal\", \"true\");\n sheet.setAttribute(\"aria-label\", input.ariaLabel);\n\n const iframe = document.createElement(\"iframe\");\n iframe.setAttribute(\n \"sandbox\",\n \"allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox\",\n );\n iframe.setAttribute(\n \"allow\",\n \"otp-credentials; publickey-credentials-get; clipboard-write\",\n );\n iframe.setAttribute(\"title\", \"Vana\");\n iframe.referrerPolicy = \"strict-origin-when-cross-origin\";\n iframe.src = input.src;\n\n const handle = document.createElement(\"div\");\n handle.className = \"handle\";\n handle.setAttribute(\"aria-hidden\", \"true\");\n\n sheet.append(handle, iframe);\n frame.append(scrim, sheet);\n root.append(style, frame);\n input.container.append(host);\n\n const removeReturnGuard = installReturnGuard(sheet, iframe, returnState);\n installDragToClose(sheet, handle, input.onClose);\n\n return {\n host,\n iframe,\n remove() {\n removeReturnGuard();\n host.remove();\n },\n setContentHeight(px) {\n // A hidden page can report a throttled frame's height; keep the last good one.\n if (document.visibilityState !== \"visible\") {\n return;\n }\n returnState.lastHeight = px;\n if (returnState.shown) {\n placeSheet(sheet, px);\n }\n },\n show() {\n // Next frame, so the closed state paints first and the sheet slides in.\n requestAnimationFrame(() => {\n returnState.shown = true;\n frame.classList.add(\"open\");\n placeSheet(sheet, returnState.lastHeight ?? INITIAL_CONTENT_PX);\n });\n },\n };\n}\n"],"mappings":"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","names":[]}
|