@opendatalabs/vana-sdk 4.4.0 → 4.5.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 +176 -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 +284 -0
- package/dist/connect/chrome.cjs.map +1 -0
- package/dist/connect/chrome.d.ts +42 -0
- package/dist/connect/chrome.js +256 -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 +232 -0
- package/dist/connect/launcher.cjs.map +1 -0
- package/dist/connect/launcher.d.ts +129 -0
- package/dist/connect/launcher.js +220 -0
- package/dist/connect/launcher.js.map +1 -0
- package/dist/connect/launcher.test.d.ts +1 -0
- package/dist/connect/protocol.cjs +259 -0
- package/dist/connect/protocol.cjs.map +1 -0
- package/dist/connect/protocol.d.ts +331 -0
- package/dist/connect/protocol.js +215 -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 +246 -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 +86 -17
- package/dist/direct/controller.cjs.map +1 -1
- package/dist/direct/controller.d.ts +101 -6
- package/dist/direct/controller.js +86 -16
- 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/index.browser.d.ts +1 -0
- package/dist/index.browser.js +104 -0
- package/dist/index.browser.js.map +4 -4
- package/dist/index.node.cjs +112 -0
- package/dist/index.node.cjs.map +3 -3
- package/dist/index.node.d.ts +1 -0
- package/dist/index.node.js +104 -0
- package/dist/index.node.js.map +4 -4
- package/dist/protocol/fixtures/pdpp-scope-keys.json +133 -0
- package/dist/protocol/pdpp-scopes.cjs +134 -0
- package/dist/protocol/pdpp-scopes.cjs.map +1 -0
- package/dist/protocol/pdpp-scopes.d.ts +123 -0
- package/dist/protocol/pdpp-scopes.js +103 -0
- package/dist/protocol/pdpp-scopes.js.map +1 -0
- package/dist/protocol/pdpp-scopes.test.d.ts +1 -0
- package/dist/protocol/scopes.cjs +17 -0
- package/dist/protocol/scopes.cjs.map +1 -1
- package/dist/protocol/scopes.d.ts +1 -0
- package/dist/protocol/scopes.js +18 -0
- package/dist/protocol/scopes.js.map +1 -1
- 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 +61 -2
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.ts +16 -4
- package/dist/server.js +67 -3
- package/dist/server.js.map +1 -1
- package/package.json +11 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/connect/protocol.ts"],"sourcesContent":["/**\n * Message protocol for the Vana connect sheet, shared by the partner-side\n * launcher and the Vana-hosted sheet.\n *\n * @remarks\n * Zero-dependency and browser-safe: it imports nothing, so a non-SDK\n * consumer (Vana Account's sheet, a backend validating a request) can take\n * it alone from `@opendatalabs/vana-sdk/connect/protocol`.\n *\n * ```\n * partner page Vana sheet (iframe)\n * ──────────── ───────────────────\n * GET clientCheckUrl(...) ───────▶ Vana's server: { registered }\n * (alongside framing; false ends it\n * with origin_not_registered)\n * iframe src = sheetUrl(...) ───────▶ renders, bound to partner origin,\n * laid out for its `display`\n * ◀─────── vana:ready\n * ◀─────── vana:resize { height }\n * ◀─────── vana:signed-in\n * ◀─────── vana:request-needed\n * vana:request { request } ───────▶ loads the request via Vana's server\n * ◀─────── vana:connected { status, completion }\n * (approved; data follows in the sheet)\n * ◀─────── vana:progress { phase }\n * (remote_collect only; coarse, no source detail)\n * vana:request-status { status } ───────▶ shows the partner-observed outcome\n * ◀─────── vana:close | vana:error { code }\n * ```\n *\n * Every message carries `v`. Both sides post with an exact `targetOrigin` and\n * check `event.origin` and `event.source` before parsing. Tokens never cross\n * this boundary: the sheet only reports outcomes.\n *\n * The partner reads; it never writes. Approving a read request grants no\n * write access, so there is no partner-to-sheet message that saves data.\n *\n * `vana:connected` means the person approved. It is not delivery: the data\n * is ready only once the partner's backend reads it (the status poll and the\n * read of the Direct flow say so).\n *\n * Versioning: the sheet URL carries the highest version the launcher speaks\n * (`v`). Parsers accept any version from {@link MIN_PROTOCOL_VERSION} to\n * {@link PROTOCOL_VERSION} and drop fields they do not know, so a sheet may\n * add fields without a version bump. A `vana:ready` or `vana:error` at a\n * version outside that range is reported as `incompatible_version` (see\n * {@link readIncompatibleVersion}).\n *\n * @category Connect\n * @module connect/protocol\n */\n\n/** The protocol version this SDK posts and puts in the sheet URL. */\nexport const PROTOCOL_VERSION = 1;\n\n/** The oldest protocol version this SDK still accepts from the other side. */\nexport const MIN_PROTOCOL_VERSION = 1;\n\n/** A protocol version this SDK accepts. */\nexport type ProtocolVersion = typeof PROTOCOL_VERSION;\n\n/** Path of the sheet page on the Vana Account origin. */\nexport const SHEET_PATH = \"/embed/connect\";\n\n/**\n * Says whether a (client id, partner origin) pair may frame the sheet.\n *\n * @remarks\n * The browser refuses an unregistered frame silently, so the launcher asks\n * here (without credentials) to name that failure instead of waiting out\n * `frame_blocked`.\n */\nexport const CLIENT_CHECK_PATH = \"/api/embed/connect/client\";\n\n/** Query parameters the sheet reads. The origin is re-checked server side. */\nexport const SHEET_QUERY = {\n clientId: \"client_id\",\n display: \"display\",\n origin: \"origin\",\n version: \"v\",\n} as const;\n\n/**\n * How the sheet is presented in the partner's window:\n * `sheet` a bottom sheet over the page, sized to its content;\n * `fullscreen` fills the whole window, for a window that holds nothing else,\n * such as a small popup the partner opened.\n * It is presentation only: it authorizes nothing and the sheet's steps,\n * messages and events are the same in both.\n */\nexport const CONNECT_DISPLAYS = [\"sheet\", \"fullscreen\"] as const;\n\n/** One of {@link CONNECT_DISPLAYS}. */\nexport type ConnectDisplay = (typeof CONNECT_DISPLAYS)[number];\n\n/** The display `connect.open` uses when none is asked for. */\nexport const DEFAULT_CONNECT_DISPLAY: ConnectDisplay = \"sheet\";\n\n/**\n * Read a display from untrusted input.\n *\n * @param value - Anything.\n * @returns A known display, or `null` for anything else.\n */\nexport function readConnectDisplay(value: unknown): ConnectDisplay | null {\n return CONNECT_DISPLAYS.find((known) => known === value) ?? null;\n}\n\n/** Vana Account origin per deployment environment. */\nexport const VANA_ACCOUNT_ORIGINS = {\n production: \"https://account.vana.org\",\n dev: \"https://account-dev.vana.org\",\n} as const;\n\n/** Environment names {@link VANA_ACCOUNT_ORIGINS} is keyed by. */\nexport type VanaConnectEnv = keyof typeof VANA_ACCOUNT_ORIGINS;\n\n/**\n * The production Vana Account origin.\n *\n * @remarks\n * Never used as an implicit default: the launcher, the flow and the CSP\n * helper all need an explicit environment or origin, so a dev app cannot\n * silently talk to production. Resolve the origin with\n * `resolveVanaAccountOrigin` instead.\n */\nexport const DEFAULT_VANA_ORIGIN = VANA_ACCOUNT_ORIGINS.production;\n\n/**\n * How an approved request gets its data, always inside this sheet.\n *\n * @remarks\n * `in_browser`: the person's Personal Server already holds every scope and\n * serves them from the sheet until the partner reads.\n * `remote_collect`: some scopes are missing, so the sheet collects them (a\n * remote browser, driven with the person) and then serves.\n */\nexport type ConnectCompletion = \"in_browser\" | \"remote_collect\";\n\nconst CONNECT_COMPLETIONS: readonly ConnectCompletion[] = [\n \"in_browser\",\n \"remote_collect\",\n];\n\n/**\n * Where a `remote_collect` request stands once approved. Deliberately coarse:\n * it says nothing of which source, what was asked of the person, or how it is\n * going. The partner shows \"Vana is collecting your data\", nothing more.\n */\nexport type ConnectProgressPhase = \"collecting\" | \"writing\";\n\nconst PROGRESS_PHASES: readonly ConnectProgressPhase[] = [\n \"collecting\",\n \"writing\",\n];\n\n/** Height reports below this are a hidden or throttled frame, never content. */\nexport const MIN_SHEET_HEIGHT_PX = 80;\n\n/** Direct Data Controller status vocabulary, as the partner backend sees it. */\nexport type RequestStatus =\n | \"pending\"\n | \"approved\"\n | \"completed\"\n | \"denied\"\n | \"expired\";\n\n/**\n * A partner-signed, short-lived read capability for one request.\n *\n * @remarks\n * The sheet asks for it (`vana:request-needed`) once the person has signed\n * in, so the partner signs it fresh; it expires within minutes. It is the\n * Direct SDK request signature for `GET /api/data-connection-requests/<id>`,\n * minted by the partner's backend with its app key. Vana's server replays it\n * to the request authority, so the sheet shows the request's real scopes and\n * the partner cannot claim someone else's app.\n */\nexport interface RequestAuthorization {\n appAddress: `0x${string}`;\n signature: `0x${string}`;\n timestamp: string;\n}\n\n/** What the partner hands the sheet: a request id and its read capability. */\nexport interface ConnectRequest {\n authorization: RequestAuthorization;\n id: string;\n}\n\n/**\n * Error codes the sheet reports.\n *\n * @remarks\n * `frame_blocked`: no `vana:ready` in time, usually the page's own CSP.\n * `incompatible_version`: the sheet speaks a protocol version this SDK does\n * not (detected by the launcher).\n * `origin_not_registered`: this page's origin is not registered for the\n * client id.\n * `vana_unavailable`: Vana itself is misconfigured or down here. Nothing the\n * partner did, and nothing the partner can fix; the sheet stays open to say\n * so.\n * `sheet_failed`: the sheet could not finish this request.\n */\nexport type SheetErrorCode =\n | \"frame_blocked\"\n | \"incompatible_version\"\n | \"origin_not_registered\"\n | \"personal_server_unsupported\"\n | \"request_unavailable\"\n | \"sheet_failed\"\n | \"vana_unavailable\";\n\n/** Sheet to partner. */\nexport type SheetMessage =\n | { type: \"vana:ready\"; v: ProtocolVersion }\n | { height: number; type: \"vana:resize\"; v: ProtocolVersion }\n | { type: \"vana:signed-in\"; v: ProtocolVersion }\n | { type: \"vana:request-needed\"; v: ProtocolVersion }\n | {\n completion: ConnectCompletion;\n status: \"approved\";\n type: \"vana:connected\";\n v: ProtocolVersion;\n }\n | {\n phase: ConnectProgressPhase;\n type: \"vana:progress\";\n v: ProtocolVersion;\n }\n | { type: \"vana:close\"; v: ProtocolVersion }\n | { code: SheetErrorCode; type: \"vana:error\"; v: ProtocolVersion };\n\n/** Partner to sheet. */\nexport type PartnerMessage =\n | {\n request: ConnectRequest;\n type: \"vana:request\";\n v: ProtocolVersion;\n }\n | {\n status: RequestStatus;\n type: \"vana:request-status\";\n v: ProtocolVersion;\n };\n\nconst REQUEST_STATUSES: readonly RequestStatus[] = [\n \"pending\",\n \"approved\",\n \"completed\",\n \"denied\",\n \"expired\",\n];\n\nconst SHEET_ERROR_CODES: readonly SheetErrorCode[] = [\n \"frame_blocked\",\n \"incompatible_version\",\n \"origin_not_registered\",\n \"personal_server_unsupported\",\n \"request_unavailable\",\n \"sheet_failed\",\n \"vana_unavailable\",\n];\n\nconst HEX_ADDRESS = /^0x[0-9a-fA-F]{40}$/;\nconst HEX_SIGNATURE = /^0x[0-9a-fA-F]{130}$/;\nconst REQUEST_ID = /^[A-Za-z0-9_-]{1,128}$/;\nconst TIMESTAMP = /^[0-9]{10,16}$/;\n\nfunction record(value: unknown): Record<string, unknown> | null {\n return value && typeof value === \"object\" && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : null;\n}\n\nfunction isSupportedVersion(value: unknown): value is ProtocolVersion {\n return (\n typeof value === \"number\" &&\n Number.isInteger(value) &&\n value >= MIN_PROTOCOL_VERSION &&\n value <= PROTOCOL_VERSION\n );\n}\n\n/**\n * Whether a request id has the shape the sheet accepts.\n *\n * @remarks\n * The id is signed into a request path, so anything outside this shape could\n * name a different resource. Signers refuse it.\n *\n * @param value - The candidate request id.\n * @returns `true` for 1 to 128 characters of `[A-Za-z0-9_-]`.\n */\nexport function isRequestId(value: unknown): value is string {\n return typeof value === \"string\" && REQUEST_ID.test(value);\n}\n\nfunction readAuthorization(value: unknown): RequestAuthorization | null {\n const auth = record(value);\n if (!auth) {\n return null;\n }\n const { appAddress, signature, timestamp } = auth;\n if (\n typeof appAddress !== \"string\" ||\n !HEX_ADDRESS.test(appAddress) ||\n typeof signature !== \"string\" ||\n !HEX_SIGNATURE.test(signature) ||\n typeof timestamp !== \"string\" ||\n !TIMESTAMP.test(timestamp)\n ) {\n return null;\n }\n return {\n appAddress: appAddress as `0x${string}`,\n signature: signature as `0x${string}`,\n timestamp,\n };\n}\n\n/**\n * Parse a {@link ConnectRequest}. Extra fields are dropped.\n *\n * @param value - Untrusted input.\n * @returns The request, or `null` when malformed.\n */\nexport function readConnectRequest(value: unknown): ConnectRequest | null {\n const request = record(value);\n if (!request || !isRequestId(request.id)) {\n return null;\n }\n const authorization = readAuthorization(request.authorization);\n return authorization ? { authorization, id: request.id } : null;\n}\n\n/**\n * Detect a sheet that speaks a protocol version this SDK does not.\n *\n * @remarks\n * Only `vana:ready` and `vana:error` are read at any version: they are the\n * handshake, so the launcher can tell \"the sheet is up but incompatible\" from\n * \"the sheet never started\". Everything else at an unknown version is ignored.\n *\n * @param data - A message posted by the sheet.\n * @returns The sheet's version when it is outside the supported range,\n * otherwise `null`.\n */\nexport function readIncompatibleVersion(data: unknown): number | null {\n const message = record(data);\n if (\n !message ||\n (message.type !== \"vana:ready\" && message.type !== \"vana:error\")\n ) {\n return null;\n }\n const { v } = message;\n if (typeof v !== \"number\" || !Number.isInteger(v) || isSupportedVersion(v)) {\n return null;\n }\n return v;\n}\n\n/**\n * Parse a message the sheet posted.\n *\n * @remarks\n * Unknown fields are dropped. Unknown message types are ignored. An error\n * code this SDK does not know is read as `sheet_failed`, so a newer sheet's\n * failure is never silently lost.\n *\n * @param data - Untrusted `MessageEvent.data`.\n * @returns The message, or `null` for unknown or malformed data.\n */\nexport function parseSheetMessage(data: unknown): SheetMessage | null {\n const message = record(data);\n if (!(message && isSupportedVersion(message.v))) {\n return null;\n }\n const v = message.v;\n switch (message.type) {\n case \"vana:ready\":\n case \"vana:signed-in\":\n case \"vana:request-needed\":\n case \"vana:close\":\n return { type: message.type, v };\n case \"vana:resize\": {\n const { height } = message;\n if (typeof height !== \"number\" || !Number.isFinite(height)) {\n return null;\n }\n return { height, type: \"vana:resize\", v };\n }\n case \"vana:connected\": {\n const completion = CONNECT_COMPLETIONS.find(\n (known) => known === message.completion,\n );\n return message.status === \"approved\" && completion\n ? { completion, status: \"approved\", type: \"vana:connected\", v }\n : null;\n }\n case \"vana:progress\": {\n const phase = PROGRESS_PHASES.find((known) => known === message.phase);\n return phase ? { phase, type: \"vana:progress\", v } : null;\n }\n case \"vana:error\": {\n if (typeof message.code !== \"string\") {\n return null;\n }\n const code =\n SHEET_ERROR_CODES.find((known) => known === message.code) ??\n \"sheet_failed\";\n return { code, type: \"vana:error\", v };\n }\n default:\n return null;\n }\n}\n\n/**\n * Parse a message the partner posted.\n *\n * @param data - Untrusted `MessageEvent.data`.\n * @returns The message, or `null` for unknown or malformed data.\n */\nexport function parsePartnerMessage(data: unknown): PartnerMessage | null {\n const message = record(data);\n if (!(message && isSupportedVersion(message.v))) {\n return null;\n }\n const v = message.v;\n switch (message.type) {\n case \"vana:request\": {\n const request = readConnectRequest(message.request);\n return request ? { request, type: \"vana:request\", v } : null;\n }\n case \"vana:request-status\": {\n const status = REQUEST_STATUSES.find((known) => known === message.status);\n return status ? { status, type: \"vana:request-status\", v } : null;\n }\n default:\n return null;\n }\n}\n\n/**\n * Normalize to a bare origin.\n *\n * @param value - A URL or origin.\n * @returns `scheme://host[:port]` for http(s), or `null` for anything else.\n */\nexport function normalizeOrigin(value: string): string | null {\n try {\n const url = new URL(value);\n if (url.protocol !== \"https:\" && url.protocol !== \"http:\") {\n return null;\n }\n return url.origin;\n } catch {\n return null;\n }\n}\n\n/**\n * Build the client-check URL the launcher asks before the sheet is ready.\n *\n * @param input - The registered client id, the partner page origin, and the\n * Vana Account origin.\n * @returns `<vanaOrigin>/api/embed/connect/client?client_id&origin`.\n */\nexport function clientCheckUrl(input: {\n clientId: string;\n partnerOrigin: string;\n vanaOrigin: string;\n}): string {\n const url = new URL(CLIENT_CHECK_PATH, input.vanaOrigin);\n url.searchParams.set(SHEET_QUERY.clientId, input.clientId);\n url.searchParams.set(SHEET_QUERY.origin, input.partnerOrigin);\n return url.toString();\n}\n\n/**\n * Parse the client-check response body.\n *\n * @param data - Untrusted JSON.\n * @returns `{ registered }`, or `null` for anything else.\n */\nexport function parseClientCheck(\n data: unknown,\n): { registered: boolean } | null {\n const body = record(data);\n return body && typeof body.registered === \"boolean\"\n ? { registered: body.registered }\n : null;\n}\n\n/**\n * Build the sheet's iframe URL.\n *\n * @param input - The registered client id, the partner page origin, the Vana\n * Account origin, and optionally the display.\n * @returns `<vanaOrigin>/embed/connect?client_id&origin&v`, plus `display` when\n * it is not the default.\n */\nexport function sheetUrl(input: {\n clientId: string;\n display?: ConnectDisplay;\n partnerOrigin: string;\n vanaOrigin: string;\n}): string {\n const url = new URL(SHEET_PATH, input.vanaOrigin);\n url.searchParams.set(SHEET_QUERY.clientId, input.clientId);\n url.searchParams.set(SHEET_QUERY.origin, input.partnerOrigin);\n url.searchParams.set(SHEET_QUERY.version, String(PROTOCOL_VERSION));\n // The default is left out: a bottom sheet's URL is what it has always been.\n if (input.display && input.display !== DEFAULT_CONNECT_DISPLAY) {\n url.searchParams.set(SHEET_QUERY.display, input.display);\n }\n return url.toString();\n}\n\n/**\n * Content-Security-Policy sources a partner page needs for the sheet.\n *\n * @remarks\n * The launcher is bundled into the partner's own code, so only the frame\n * and the launcher's client check (a credential-less `fetch`) need\n * allowing. Merge the result into your existing policy; if the page sets\n * `child-src` but not `frame-src`, add the origin there instead. There is no\n * default target, so a dev app never allows production by accident.\n *\n * @example\n * ```typescript\n * const csp = vanaConnectCsp(\"production\");\n * // { \"frame-src\": [\"https://account.vana.org\"],\n * // \"connect-src\": [\"https://account.vana.org\"] }\n * const header = Object.entries(csp)\n * .map(([directive, sources]) => `${directive} 'self' ${sources.join(\" \")}`)\n * .join(\"; \");\n * ```\n *\n * @param target - `\"production\"`, `\"dev\"`, or `{ accountOrigin }` for a\n * custom Vana Account deployment.\n * @returns The directives to add, each with the sources it needs.\n */\nexport function vanaConnectCsp(\n target: VanaConnectEnv | { accountOrigin: string },\n): { \"connect-src\": string[]; \"frame-src\": string[] } {\n const origin =\n target === \"production\" || target === \"dev\"\n ? VANA_ACCOUNT_ORIGINS[target]\n : typeof target === \"object\" &&\n target !== null &&\n typeof target.accountOrigin === \"string\"\n ? normalizeOrigin(target.accountOrigin)\n : null;\n if (!origin) {\n throw new TypeError(\n 'vanaConnectCsp needs \"production\", \"dev\" or an http(s) accountOrigin',\n );\n }\n return { \"connect-src\": [origin], \"frame-src\": [origin] };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message protocol for the Vana connect sheet, shared by the partner-side
|
|
3
|
+
* launcher and the Vana-hosted sheet.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* Zero-dependency and browser-safe: it imports nothing, so a non-SDK
|
|
7
|
+
* consumer (Vana Account's sheet, a backend validating a request) can take
|
|
8
|
+
* it alone from `@opendatalabs/vana-sdk/connect/protocol`.
|
|
9
|
+
*
|
|
10
|
+
* ```
|
|
11
|
+
* partner page Vana sheet (iframe)
|
|
12
|
+
* ──────────── ───────────────────
|
|
13
|
+
* GET clientCheckUrl(...) ───────▶ Vana's server: { registered }
|
|
14
|
+
* (alongside framing; false ends it
|
|
15
|
+
* with origin_not_registered)
|
|
16
|
+
* iframe src = sheetUrl(...) ───────▶ renders, bound to partner origin,
|
|
17
|
+
* laid out for its `display`
|
|
18
|
+
* ◀─────── vana:ready
|
|
19
|
+
* ◀─────── vana:resize { height }
|
|
20
|
+
* ◀─────── vana:signed-in
|
|
21
|
+
* ◀─────── vana:request-needed
|
|
22
|
+
* vana:request { request } ───────▶ loads the request via Vana's server
|
|
23
|
+
* ◀─────── vana:connected { status, completion }
|
|
24
|
+
* (approved; data follows in the sheet)
|
|
25
|
+
* ◀─────── vana:progress { phase }
|
|
26
|
+
* (remote_collect only; coarse, no source detail)
|
|
27
|
+
* vana:request-status { status } ───────▶ shows the partner-observed outcome
|
|
28
|
+
* ◀─────── vana:close | vana:error { code }
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* Every message carries `v`. Both sides post with an exact `targetOrigin` and
|
|
32
|
+
* check `event.origin` and `event.source` before parsing. Tokens never cross
|
|
33
|
+
* this boundary: the sheet only reports outcomes.
|
|
34
|
+
*
|
|
35
|
+
* The partner reads; it never writes. Approving a read request grants no
|
|
36
|
+
* write access, so there is no partner-to-sheet message that saves data.
|
|
37
|
+
*
|
|
38
|
+
* `vana:connected` means the person approved. It is not delivery: the data
|
|
39
|
+
* is ready only once the partner's backend reads it (the status poll and the
|
|
40
|
+
* read of the Direct flow say so).
|
|
41
|
+
*
|
|
42
|
+
* Versioning: the sheet URL carries the highest version the launcher speaks
|
|
43
|
+
* (`v`). Parsers accept any version from {@link MIN_PROTOCOL_VERSION} to
|
|
44
|
+
* {@link PROTOCOL_VERSION} and drop fields they do not know, so a sheet may
|
|
45
|
+
* add fields without a version bump. A `vana:ready` or `vana:error` at a
|
|
46
|
+
* version outside that range is reported as `incompatible_version` (see
|
|
47
|
+
* {@link readIncompatibleVersion}).
|
|
48
|
+
*
|
|
49
|
+
* @category Connect
|
|
50
|
+
* @module connect/protocol
|
|
51
|
+
*/
|
|
52
|
+
/** The protocol version this SDK posts and puts in the sheet URL. */
|
|
53
|
+
export declare const PROTOCOL_VERSION = 1;
|
|
54
|
+
/** The oldest protocol version this SDK still accepts from the other side. */
|
|
55
|
+
export declare const MIN_PROTOCOL_VERSION = 1;
|
|
56
|
+
/** A protocol version this SDK accepts. */
|
|
57
|
+
export type ProtocolVersion = typeof PROTOCOL_VERSION;
|
|
58
|
+
/** Path of the sheet page on the Vana Account origin. */
|
|
59
|
+
export declare const SHEET_PATH = "/embed/connect";
|
|
60
|
+
/**
|
|
61
|
+
* Says whether a (client id, partner origin) pair may frame the sheet.
|
|
62
|
+
*
|
|
63
|
+
* @remarks
|
|
64
|
+
* The browser refuses an unregistered frame silently, so the launcher asks
|
|
65
|
+
* here (without credentials) to name that failure instead of waiting out
|
|
66
|
+
* `frame_blocked`.
|
|
67
|
+
*/
|
|
68
|
+
export declare const CLIENT_CHECK_PATH = "/api/embed/connect/client";
|
|
69
|
+
/** Query parameters the sheet reads. The origin is re-checked server side. */
|
|
70
|
+
export declare const SHEET_QUERY: {
|
|
71
|
+
readonly clientId: "client_id";
|
|
72
|
+
readonly display: "display";
|
|
73
|
+
readonly origin: "origin";
|
|
74
|
+
readonly version: "v";
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* How the sheet is presented in the partner's window:
|
|
78
|
+
* `sheet` a bottom sheet over the page, sized to its content;
|
|
79
|
+
* `fullscreen` fills the whole window, for a window that holds nothing else,
|
|
80
|
+
* such as a small popup the partner opened.
|
|
81
|
+
* It is presentation only: it authorizes nothing and the sheet's steps,
|
|
82
|
+
* messages and events are the same in both.
|
|
83
|
+
*/
|
|
84
|
+
export declare const CONNECT_DISPLAYS: readonly ["sheet", "fullscreen"];
|
|
85
|
+
/** One of {@link CONNECT_DISPLAYS}. */
|
|
86
|
+
export type ConnectDisplay = (typeof CONNECT_DISPLAYS)[number];
|
|
87
|
+
/** The display `connect.open` uses when none is asked for. */
|
|
88
|
+
export declare const DEFAULT_CONNECT_DISPLAY: ConnectDisplay;
|
|
89
|
+
/**
|
|
90
|
+
* Read a display from untrusted input.
|
|
91
|
+
*
|
|
92
|
+
* @param value - Anything.
|
|
93
|
+
* @returns A known display, or `null` for anything else.
|
|
94
|
+
*/
|
|
95
|
+
export declare function readConnectDisplay(value: unknown): ConnectDisplay | null;
|
|
96
|
+
/** Vana Account origin per deployment environment. */
|
|
97
|
+
export declare const VANA_ACCOUNT_ORIGINS: {
|
|
98
|
+
readonly production: "https://account.vana.org";
|
|
99
|
+
readonly dev: "https://account-dev.vana.org";
|
|
100
|
+
};
|
|
101
|
+
/** Environment names {@link VANA_ACCOUNT_ORIGINS} is keyed by. */
|
|
102
|
+
export type VanaConnectEnv = keyof typeof VANA_ACCOUNT_ORIGINS;
|
|
103
|
+
/**
|
|
104
|
+
* The production Vana Account origin.
|
|
105
|
+
*
|
|
106
|
+
* @remarks
|
|
107
|
+
* Never used as an implicit default: the launcher, the flow and the CSP
|
|
108
|
+
* helper all need an explicit environment or origin, so a dev app cannot
|
|
109
|
+
* silently talk to production. Resolve the origin with
|
|
110
|
+
* `resolveVanaAccountOrigin` instead.
|
|
111
|
+
*/
|
|
112
|
+
export declare const DEFAULT_VANA_ORIGIN: "https://account.vana.org";
|
|
113
|
+
/**
|
|
114
|
+
* How an approved request gets its data, always inside this sheet.
|
|
115
|
+
*
|
|
116
|
+
* @remarks
|
|
117
|
+
* `in_browser`: the person's Personal Server already holds every scope and
|
|
118
|
+
* serves them from the sheet until the partner reads.
|
|
119
|
+
* `remote_collect`: some scopes are missing, so the sheet collects them (a
|
|
120
|
+
* remote browser, driven with the person) and then serves.
|
|
121
|
+
*/
|
|
122
|
+
export type ConnectCompletion = "in_browser" | "remote_collect";
|
|
123
|
+
/**
|
|
124
|
+
* Where a `remote_collect` request stands once approved. Deliberately coarse:
|
|
125
|
+
* it says nothing of which source, what was asked of the person, or how it is
|
|
126
|
+
* going. The partner shows "Vana is collecting your data", nothing more.
|
|
127
|
+
*/
|
|
128
|
+
export type ConnectProgressPhase = "collecting" | "writing";
|
|
129
|
+
/** Height reports below this are a hidden or throttled frame, never content. */
|
|
130
|
+
export declare const MIN_SHEET_HEIGHT_PX = 80;
|
|
131
|
+
/** Direct Data Controller status vocabulary, as the partner backend sees it. */
|
|
132
|
+
export type RequestStatus = "pending" | "approved" | "completed" | "denied" | "expired";
|
|
133
|
+
/**
|
|
134
|
+
* A partner-signed, short-lived read capability for one request.
|
|
135
|
+
*
|
|
136
|
+
* @remarks
|
|
137
|
+
* The sheet asks for it (`vana:request-needed`) once the person has signed
|
|
138
|
+
* in, so the partner signs it fresh; it expires within minutes. It is the
|
|
139
|
+
* Direct SDK request signature for `GET /api/data-connection-requests/<id>`,
|
|
140
|
+
* minted by the partner's backend with its app key. Vana's server replays it
|
|
141
|
+
* to the request authority, so the sheet shows the request's real scopes and
|
|
142
|
+
* the partner cannot claim someone else's app.
|
|
143
|
+
*/
|
|
144
|
+
export interface RequestAuthorization {
|
|
145
|
+
appAddress: `0x${string}`;
|
|
146
|
+
signature: `0x${string}`;
|
|
147
|
+
timestamp: string;
|
|
148
|
+
}
|
|
149
|
+
/** What the partner hands the sheet: a request id and its read capability. */
|
|
150
|
+
export interface ConnectRequest {
|
|
151
|
+
authorization: RequestAuthorization;
|
|
152
|
+
id: string;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Error codes the sheet reports.
|
|
156
|
+
*
|
|
157
|
+
* @remarks
|
|
158
|
+
* `frame_blocked`: no `vana:ready` in time, usually the page's own CSP.
|
|
159
|
+
* `incompatible_version`: the sheet speaks a protocol version this SDK does
|
|
160
|
+
* not (detected by the launcher).
|
|
161
|
+
* `origin_not_registered`: this page's origin is not registered for the
|
|
162
|
+
* client id.
|
|
163
|
+
* `vana_unavailable`: Vana itself is misconfigured or down here. Nothing the
|
|
164
|
+
* partner did, and nothing the partner can fix; the sheet stays open to say
|
|
165
|
+
* so.
|
|
166
|
+
* `sheet_failed`: the sheet could not finish this request.
|
|
167
|
+
*/
|
|
168
|
+
export type SheetErrorCode = "frame_blocked" | "incompatible_version" | "origin_not_registered" | "personal_server_unsupported" | "request_unavailable" | "sheet_failed" | "vana_unavailable";
|
|
169
|
+
/** Sheet to partner. */
|
|
170
|
+
export type SheetMessage = {
|
|
171
|
+
type: "vana:ready";
|
|
172
|
+
v: ProtocolVersion;
|
|
173
|
+
} | {
|
|
174
|
+
height: number;
|
|
175
|
+
type: "vana:resize";
|
|
176
|
+
v: ProtocolVersion;
|
|
177
|
+
} | {
|
|
178
|
+
type: "vana:signed-in";
|
|
179
|
+
v: ProtocolVersion;
|
|
180
|
+
} | {
|
|
181
|
+
type: "vana:request-needed";
|
|
182
|
+
v: ProtocolVersion;
|
|
183
|
+
} | {
|
|
184
|
+
completion: ConnectCompletion;
|
|
185
|
+
status: "approved";
|
|
186
|
+
type: "vana:connected";
|
|
187
|
+
v: ProtocolVersion;
|
|
188
|
+
} | {
|
|
189
|
+
phase: ConnectProgressPhase;
|
|
190
|
+
type: "vana:progress";
|
|
191
|
+
v: ProtocolVersion;
|
|
192
|
+
} | {
|
|
193
|
+
type: "vana:close";
|
|
194
|
+
v: ProtocolVersion;
|
|
195
|
+
} | {
|
|
196
|
+
code: SheetErrorCode;
|
|
197
|
+
type: "vana:error";
|
|
198
|
+
v: ProtocolVersion;
|
|
199
|
+
};
|
|
200
|
+
/** Partner to sheet. */
|
|
201
|
+
export type PartnerMessage = {
|
|
202
|
+
request: ConnectRequest;
|
|
203
|
+
type: "vana:request";
|
|
204
|
+
v: ProtocolVersion;
|
|
205
|
+
} | {
|
|
206
|
+
status: RequestStatus;
|
|
207
|
+
type: "vana:request-status";
|
|
208
|
+
v: ProtocolVersion;
|
|
209
|
+
};
|
|
210
|
+
/**
|
|
211
|
+
* Whether a request id has the shape the sheet accepts.
|
|
212
|
+
*
|
|
213
|
+
* @remarks
|
|
214
|
+
* The id is signed into a request path, so anything outside this shape could
|
|
215
|
+
* name a different resource. Signers refuse it.
|
|
216
|
+
*
|
|
217
|
+
* @param value - The candidate request id.
|
|
218
|
+
* @returns `true` for 1 to 128 characters of `[A-Za-z0-9_-]`.
|
|
219
|
+
*/
|
|
220
|
+
export declare function isRequestId(value: unknown): value is string;
|
|
221
|
+
/**
|
|
222
|
+
* Parse a {@link ConnectRequest}. Extra fields are dropped.
|
|
223
|
+
*
|
|
224
|
+
* @param value - Untrusted input.
|
|
225
|
+
* @returns The request, or `null` when malformed.
|
|
226
|
+
*/
|
|
227
|
+
export declare function readConnectRequest(value: unknown): ConnectRequest | null;
|
|
228
|
+
/**
|
|
229
|
+
* Detect a sheet that speaks a protocol version this SDK does not.
|
|
230
|
+
*
|
|
231
|
+
* @remarks
|
|
232
|
+
* Only `vana:ready` and `vana:error` are read at any version: they are the
|
|
233
|
+
* handshake, so the launcher can tell "the sheet is up but incompatible" from
|
|
234
|
+
* "the sheet never started". Everything else at an unknown version is ignored.
|
|
235
|
+
*
|
|
236
|
+
* @param data - A message posted by the sheet.
|
|
237
|
+
* @returns The sheet's version when it is outside the supported range,
|
|
238
|
+
* otherwise `null`.
|
|
239
|
+
*/
|
|
240
|
+
export declare function readIncompatibleVersion(data: unknown): number | null;
|
|
241
|
+
/**
|
|
242
|
+
* Parse a message the sheet posted.
|
|
243
|
+
*
|
|
244
|
+
* @remarks
|
|
245
|
+
* Unknown fields are dropped. Unknown message types are ignored. An error
|
|
246
|
+
* code this SDK does not know is read as `sheet_failed`, so a newer sheet's
|
|
247
|
+
* failure is never silently lost.
|
|
248
|
+
*
|
|
249
|
+
* @param data - Untrusted `MessageEvent.data`.
|
|
250
|
+
* @returns The message, or `null` for unknown or malformed data.
|
|
251
|
+
*/
|
|
252
|
+
export declare function parseSheetMessage(data: unknown): SheetMessage | null;
|
|
253
|
+
/**
|
|
254
|
+
* Parse a message the partner posted.
|
|
255
|
+
*
|
|
256
|
+
* @param data - Untrusted `MessageEvent.data`.
|
|
257
|
+
* @returns The message, or `null` for unknown or malformed data.
|
|
258
|
+
*/
|
|
259
|
+
export declare function parsePartnerMessage(data: unknown): PartnerMessage | null;
|
|
260
|
+
/**
|
|
261
|
+
* Normalize to a bare origin.
|
|
262
|
+
*
|
|
263
|
+
* @param value - A URL or origin.
|
|
264
|
+
* @returns `scheme://host[:port]` for http(s), or `null` for anything else.
|
|
265
|
+
*/
|
|
266
|
+
export declare function normalizeOrigin(value: string): string | null;
|
|
267
|
+
/**
|
|
268
|
+
* Build the client-check URL the launcher asks before the sheet is ready.
|
|
269
|
+
*
|
|
270
|
+
* @param input - The registered client id, the partner page origin, and the
|
|
271
|
+
* Vana Account origin.
|
|
272
|
+
* @returns `<vanaOrigin>/api/embed/connect/client?client_id&origin`.
|
|
273
|
+
*/
|
|
274
|
+
export declare function clientCheckUrl(input: {
|
|
275
|
+
clientId: string;
|
|
276
|
+
partnerOrigin: string;
|
|
277
|
+
vanaOrigin: string;
|
|
278
|
+
}): string;
|
|
279
|
+
/**
|
|
280
|
+
* Parse the client-check response body.
|
|
281
|
+
*
|
|
282
|
+
* @param data - Untrusted JSON.
|
|
283
|
+
* @returns `{ registered }`, or `null` for anything else.
|
|
284
|
+
*/
|
|
285
|
+
export declare function parseClientCheck(data: unknown): {
|
|
286
|
+
registered: boolean;
|
|
287
|
+
} | null;
|
|
288
|
+
/**
|
|
289
|
+
* Build the sheet's iframe URL.
|
|
290
|
+
*
|
|
291
|
+
* @param input - The registered client id, the partner page origin, the Vana
|
|
292
|
+
* Account origin, and optionally the display.
|
|
293
|
+
* @returns `<vanaOrigin>/embed/connect?client_id&origin&v`, plus `display` when
|
|
294
|
+
* it is not the default.
|
|
295
|
+
*/
|
|
296
|
+
export declare function sheetUrl(input: {
|
|
297
|
+
clientId: string;
|
|
298
|
+
display?: ConnectDisplay;
|
|
299
|
+
partnerOrigin: string;
|
|
300
|
+
vanaOrigin: string;
|
|
301
|
+
}): string;
|
|
302
|
+
/**
|
|
303
|
+
* Content-Security-Policy sources a partner page needs for the sheet.
|
|
304
|
+
*
|
|
305
|
+
* @remarks
|
|
306
|
+
* The launcher is bundled into the partner's own code, so only the frame
|
|
307
|
+
* and the launcher's client check (a credential-less `fetch`) need
|
|
308
|
+
* allowing. Merge the result into your existing policy; if the page sets
|
|
309
|
+
* `child-src` but not `frame-src`, add the origin there instead. There is no
|
|
310
|
+
* default target, so a dev app never allows production by accident.
|
|
311
|
+
*
|
|
312
|
+
* @example
|
|
313
|
+
* ```typescript
|
|
314
|
+
* const csp = vanaConnectCsp("production");
|
|
315
|
+
* // { "frame-src": ["https://account.vana.org"],
|
|
316
|
+
* // "connect-src": ["https://account.vana.org"] }
|
|
317
|
+
* const header = Object.entries(csp)
|
|
318
|
+
* .map(([directive, sources]) => `${directive} 'self' ${sources.join(" ")}`)
|
|
319
|
+
* .join("; ");
|
|
320
|
+
* ```
|
|
321
|
+
*
|
|
322
|
+
* @param target - `"production"`, `"dev"`, or `{ accountOrigin }` for a
|
|
323
|
+
* custom Vana Account deployment.
|
|
324
|
+
* @returns The directives to add, each with the sources it needs.
|
|
325
|
+
*/
|
|
326
|
+
export declare function vanaConnectCsp(target: VanaConnectEnv | {
|
|
327
|
+
accountOrigin: string;
|
|
328
|
+
}): {
|
|
329
|
+
"connect-src": string[];
|
|
330
|
+
"frame-src": string[];
|
|
331
|
+
};
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
const PROTOCOL_VERSION = 1;
|
|
2
|
+
const MIN_PROTOCOL_VERSION = 1;
|
|
3
|
+
const SHEET_PATH = "/embed/connect";
|
|
4
|
+
const CLIENT_CHECK_PATH = "/api/embed/connect/client";
|
|
5
|
+
const SHEET_QUERY = {
|
|
6
|
+
clientId: "client_id",
|
|
7
|
+
display: "display",
|
|
8
|
+
origin: "origin",
|
|
9
|
+
version: "v"
|
|
10
|
+
};
|
|
11
|
+
const CONNECT_DISPLAYS = ["sheet", "fullscreen"];
|
|
12
|
+
const DEFAULT_CONNECT_DISPLAY = "sheet";
|
|
13
|
+
function readConnectDisplay(value) {
|
|
14
|
+
return CONNECT_DISPLAYS.find((known) => known === value) ?? null;
|
|
15
|
+
}
|
|
16
|
+
const VANA_ACCOUNT_ORIGINS = {
|
|
17
|
+
production: "https://account.vana.org",
|
|
18
|
+
dev: "https://account-dev.vana.org"
|
|
19
|
+
};
|
|
20
|
+
const DEFAULT_VANA_ORIGIN = VANA_ACCOUNT_ORIGINS.production;
|
|
21
|
+
const CONNECT_COMPLETIONS = [
|
|
22
|
+
"in_browser",
|
|
23
|
+
"remote_collect"
|
|
24
|
+
];
|
|
25
|
+
const PROGRESS_PHASES = [
|
|
26
|
+
"collecting",
|
|
27
|
+
"writing"
|
|
28
|
+
];
|
|
29
|
+
const MIN_SHEET_HEIGHT_PX = 80;
|
|
30
|
+
const REQUEST_STATUSES = [
|
|
31
|
+
"pending",
|
|
32
|
+
"approved",
|
|
33
|
+
"completed",
|
|
34
|
+
"denied",
|
|
35
|
+
"expired"
|
|
36
|
+
];
|
|
37
|
+
const SHEET_ERROR_CODES = [
|
|
38
|
+
"frame_blocked",
|
|
39
|
+
"incompatible_version",
|
|
40
|
+
"origin_not_registered",
|
|
41
|
+
"personal_server_unsupported",
|
|
42
|
+
"request_unavailable",
|
|
43
|
+
"sheet_failed",
|
|
44
|
+
"vana_unavailable"
|
|
45
|
+
];
|
|
46
|
+
const HEX_ADDRESS = /^0x[0-9a-fA-F]{40}$/;
|
|
47
|
+
const HEX_SIGNATURE = /^0x[0-9a-fA-F]{130}$/;
|
|
48
|
+
const REQUEST_ID = /^[A-Za-z0-9_-]{1,128}$/;
|
|
49
|
+
const TIMESTAMP = /^[0-9]{10,16}$/;
|
|
50
|
+
function record(value) {
|
|
51
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
52
|
+
}
|
|
53
|
+
function isSupportedVersion(value) {
|
|
54
|
+
return typeof value === "number" && Number.isInteger(value) && value >= MIN_PROTOCOL_VERSION && value <= PROTOCOL_VERSION;
|
|
55
|
+
}
|
|
56
|
+
function isRequestId(value) {
|
|
57
|
+
return typeof value === "string" && REQUEST_ID.test(value);
|
|
58
|
+
}
|
|
59
|
+
function readAuthorization(value) {
|
|
60
|
+
const auth = record(value);
|
|
61
|
+
if (!auth) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const { appAddress, signature, timestamp } = auth;
|
|
65
|
+
if (typeof appAddress !== "string" || !HEX_ADDRESS.test(appAddress) || typeof signature !== "string" || !HEX_SIGNATURE.test(signature) || typeof timestamp !== "string" || !TIMESTAMP.test(timestamp)) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
appAddress,
|
|
70
|
+
signature,
|
|
71
|
+
timestamp
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function readConnectRequest(value) {
|
|
75
|
+
const request = record(value);
|
|
76
|
+
if (!request || !isRequestId(request.id)) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const authorization = readAuthorization(request.authorization);
|
|
80
|
+
return authorization ? { authorization, id: request.id } : null;
|
|
81
|
+
}
|
|
82
|
+
function readIncompatibleVersion(data) {
|
|
83
|
+
const message = record(data);
|
|
84
|
+
if (!message || message.type !== "vana:ready" && message.type !== "vana:error") {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
const { v } = message;
|
|
88
|
+
if (typeof v !== "number" || !Number.isInteger(v) || isSupportedVersion(v)) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
return v;
|
|
92
|
+
}
|
|
93
|
+
function parseSheetMessage(data) {
|
|
94
|
+
const message = record(data);
|
|
95
|
+
if (!(message && isSupportedVersion(message.v))) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const v = message.v;
|
|
99
|
+
switch (message.type) {
|
|
100
|
+
case "vana:ready":
|
|
101
|
+
case "vana:signed-in":
|
|
102
|
+
case "vana:request-needed":
|
|
103
|
+
case "vana:close":
|
|
104
|
+
return { type: message.type, v };
|
|
105
|
+
case "vana:resize": {
|
|
106
|
+
const { height } = message;
|
|
107
|
+
if (typeof height !== "number" || !Number.isFinite(height)) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
return { height, type: "vana:resize", v };
|
|
111
|
+
}
|
|
112
|
+
case "vana:connected": {
|
|
113
|
+
const completion = CONNECT_COMPLETIONS.find(
|
|
114
|
+
(known) => known === message.completion
|
|
115
|
+
);
|
|
116
|
+
return message.status === "approved" && completion ? { completion, status: "approved", type: "vana:connected", v } : null;
|
|
117
|
+
}
|
|
118
|
+
case "vana:progress": {
|
|
119
|
+
const phase = PROGRESS_PHASES.find((known) => known === message.phase);
|
|
120
|
+
return phase ? { phase, type: "vana:progress", v } : null;
|
|
121
|
+
}
|
|
122
|
+
case "vana:error": {
|
|
123
|
+
if (typeof message.code !== "string") {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
const code = SHEET_ERROR_CODES.find((known) => known === message.code) ?? "sheet_failed";
|
|
127
|
+
return { code, type: "vana:error", v };
|
|
128
|
+
}
|
|
129
|
+
default:
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function parsePartnerMessage(data) {
|
|
134
|
+
const message = record(data);
|
|
135
|
+
if (!(message && isSupportedVersion(message.v))) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
const v = message.v;
|
|
139
|
+
switch (message.type) {
|
|
140
|
+
case "vana:request": {
|
|
141
|
+
const request = readConnectRequest(message.request);
|
|
142
|
+
return request ? { request, type: "vana:request", v } : null;
|
|
143
|
+
}
|
|
144
|
+
case "vana:request-status": {
|
|
145
|
+
const status = REQUEST_STATUSES.find((known) => known === message.status);
|
|
146
|
+
return status ? { status, type: "vana:request-status", v } : null;
|
|
147
|
+
}
|
|
148
|
+
default:
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function normalizeOrigin(value) {
|
|
153
|
+
try {
|
|
154
|
+
const url = new URL(value);
|
|
155
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
return url.origin;
|
|
159
|
+
} catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function clientCheckUrl(input) {
|
|
164
|
+
const url = new URL(CLIENT_CHECK_PATH, input.vanaOrigin);
|
|
165
|
+
url.searchParams.set(SHEET_QUERY.clientId, input.clientId);
|
|
166
|
+
url.searchParams.set(SHEET_QUERY.origin, input.partnerOrigin);
|
|
167
|
+
return url.toString();
|
|
168
|
+
}
|
|
169
|
+
function parseClientCheck(data) {
|
|
170
|
+
const body = record(data);
|
|
171
|
+
return body && typeof body.registered === "boolean" ? { registered: body.registered } : null;
|
|
172
|
+
}
|
|
173
|
+
function sheetUrl(input) {
|
|
174
|
+
const url = new URL(SHEET_PATH, input.vanaOrigin);
|
|
175
|
+
url.searchParams.set(SHEET_QUERY.clientId, input.clientId);
|
|
176
|
+
url.searchParams.set(SHEET_QUERY.origin, input.partnerOrigin);
|
|
177
|
+
url.searchParams.set(SHEET_QUERY.version, String(PROTOCOL_VERSION));
|
|
178
|
+
if (input.display && input.display !== DEFAULT_CONNECT_DISPLAY) {
|
|
179
|
+
url.searchParams.set(SHEET_QUERY.display, input.display);
|
|
180
|
+
}
|
|
181
|
+
return url.toString();
|
|
182
|
+
}
|
|
183
|
+
function vanaConnectCsp(target) {
|
|
184
|
+
const origin = target === "production" || target === "dev" ? VANA_ACCOUNT_ORIGINS[target] : typeof target === "object" && target !== null && typeof target.accountOrigin === "string" ? normalizeOrigin(target.accountOrigin) : null;
|
|
185
|
+
if (!origin) {
|
|
186
|
+
throw new TypeError(
|
|
187
|
+
'vanaConnectCsp needs "production", "dev" or an http(s) accountOrigin'
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
return { "connect-src": [origin], "frame-src": [origin] };
|
|
191
|
+
}
|
|
192
|
+
export {
|
|
193
|
+
CLIENT_CHECK_PATH,
|
|
194
|
+
CONNECT_DISPLAYS,
|
|
195
|
+
DEFAULT_CONNECT_DISPLAY,
|
|
196
|
+
DEFAULT_VANA_ORIGIN,
|
|
197
|
+
MIN_PROTOCOL_VERSION,
|
|
198
|
+
MIN_SHEET_HEIGHT_PX,
|
|
199
|
+
PROTOCOL_VERSION,
|
|
200
|
+
SHEET_PATH,
|
|
201
|
+
SHEET_QUERY,
|
|
202
|
+
VANA_ACCOUNT_ORIGINS,
|
|
203
|
+
clientCheckUrl,
|
|
204
|
+
isRequestId,
|
|
205
|
+
normalizeOrigin,
|
|
206
|
+
parseClientCheck,
|
|
207
|
+
parsePartnerMessage,
|
|
208
|
+
parseSheetMessage,
|
|
209
|
+
readConnectDisplay,
|
|
210
|
+
readConnectRequest,
|
|
211
|
+
readIncompatibleVersion,
|
|
212
|
+
sheetUrl,
|
|
213
|
+
vanaConnectCsp
|
|
214
|
+
};
|
|
215
|
+
//# sourceMappingURL=protocol.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/connect/protocol.ts"],"sourcesContent":["/**\n * Message protocol for the Vana connect sheet, shared by the partner-side\n * launcher and the Vana-hosted sheet.\n *\n * @remarks\n * Zero-dependency and browser-safe: it imports nothing, so a non-SDK\n * consumer (Vana Account's sheet, a backend validating a request) can take\n * it alone from `@opendatalabs/vana-sdk/connect/protocol`.\n *\n * ```\n * partner page Vana sheet (iframe)\n * ──────────── ───────────────────\n * GET clientCheckUrl(...) ───────▶ Vana's server: { registered }\n * (alongside framing; false ends it\n * with origin_not_registered)\n * iframe src = sheetUrl(...) ───────▶ renders, bound to partner origin,\n * laid out for its `display`\n * ◀─────── vana:ready\n * ◀─────── vana:resize { height }\n * ◀─────── vana:signed-in\n * ◀─────── vana:request-needed\n * vana:request { request } ───────▶ loads the request via Vana's server\n * ◀─────── vana:connected { status, completion }\n * (approved; data follows in the sheet)\n * ◀─────── vana:progress { phase }\n * (remote_collect only; coarse, no source detail)\n * vana:request-status { status } ───────▶ shows the partner-observed outcome\n * ◀─────── vana:close | vana:error { code }\n * ```\n *\n * Every message carries `v`. Both sides post with an exact `targetOrigin` and\n * check `event.origin` and `event.source` before parsing. Tokens never cross\n * this boundary: the sheet only reports outcomes.\n *\n * The partner reads; it never writes. Approving a read request grants no\n * write access, so there is no partner-to-sheet message that saves data.\n *\n * `vana:connected` means the person approved. It is not delivery: the data\n * is ready only once the partner's backend reads it (the status poll and the\n * read of the Direct flow say so).\n *\n * Versioning: the sheet URL carries the highest version the launcher speaks\n * (`v`). Parsers accept any version from {@link MIN_PROTOCOL_VERSION} to\n * {@link PROTOCOL_VERSION} and drop fields they do not know, so a sheet may\n * add fields without a version bump. A `vana:ready` or `vana:error` at a\n * version outside that range is reported as `incompatible_version` (see\n * {@link readIncompatibleVersion}).\n *\n * @category Connect\n * @module connect/protocol\n */\n\n/** The protocol version this SDK posts and puts in the sheet URL. */\nexport const PROTOCOL_VERSION = 1;\n\n/** The oldest protocol version this SDK still accepts from the other side. */\nexport const MIN_PROTOCOL_VERSION = 1;\n\n/** A protocol version this SDK accepts. */\nexport type ProtocolVersion = typeof PROTOCOL_VERSION;\n\n/** Path of the sheet page on the Vana Account origin. */\nexport const SHEET_PATH = \"/embed/connect\";\n\n/**\n * Says whether a (client id, partner origin) pair may frame the sheet.\n *\n * @remarks\n * The browser refuses an unregistered frame silently, so the launcher asks\n * here (without credentials) to name that failure instead of waiting out\n * `frame_blocked`.\n */\nexport const CLIENT_CHECK_PATH = \"/api/embed/connect/client\";\n\n/** Query parameters the sheet reads. The origin is re-checked server side. */\nexport const SHEET_QUERY = {\n clientId: \"client_id\",\n display: \"display\",\n origin: \"origin\",\n version: \"v\",\n} as const;\n\n/**\n * How the sheet is presented in the partner's window:\n * `sheet` a bottom sheet over the page, sized to its content;\n * `fullscreen` fills the whole window, for a window that holds nothing else,\n * such as a small popup the partner opened.\n * It is presentation only: it authorizes nothing and the sheet's steps,\n * messages and events are the same in both.\n */\nexport const CONNECT_DISPLAYS = [\"sheet\", \"fullscreen\"] as const;\n\n/** One of {@link CONNECT_DISPLAYS}. */\nexport type ConnectDisplay = (typeof CONNECT_DISPLAYS)[number];\n\n/** The display `connect.open` uses when none is asked for. */\nexport const DEFAULT_CONNECT_DISPLAY: ConnectDisplay = \"sheet\";\n\n/**\n * Read a display from untrusted input.\n *\n * @param value - Anything.\n * @returns A known display, or `null` for anything else.\n */\nexport function readConnectDisplay(value: unknown): ConnectDisplay | null {\n return CONNECT_DISPLAYS.find((known) => known === value) ?? null;\n}\n\n/** Vana Account origin per deployment environment. */\nexport const VANA_ACCOUNT_ORIGINS = {\n production: \"https://account.vana.org\",\n dev: \"https://account-dev.vana.org\",\n} as const;\n\n/** Environment names {@link VANA_ACCOUNT_ORIGINS} is keyed by. */\nexport type VanaConnectEnv = keyof typeof VANA_ACCOUNT_ORIGINS;\n\n/**\n * The production Vana Account origin.\n *\n * @remarks\n * Never used as an implicit default: the launcher, the flow and the CSP\n * helper all need an explicit environment or origin, so a dev app cannot\n * silently talk to production. Resolve the origin with\n * `resolveVanaAccountOrigin` instead.\n */\nexport const DEFAULT_VANA_ORIGIN = VANA_ACCOUNT_ORIGINS.production;\n\n/**\n * How an approved request gets its data, always inside this sheet.\n *\n * @remarks\n * `in_browser`: the person's Personal Server already holds every scope and\n * serves them from the sheet until the partner reads.\n * `remote_collect`: some scopes are missing, so the sheet collects them (a\n * remote browser, driven with the person) and then serves.\n */\nexport type ConnectCompletion = \"in_browser\" | \"remote_collect\";\n\nconst CONNECT_COMPLETIONS: readonly ConnectCompletion[] = [\n \"in_browser\",\n \"remote_collect\",\n];\n\n/**\n * Where a `remote_collect` request stands once approved. Deliberately coarse:\n * it says nothing of which source, what was asked of the person, or how it is\n * going. The partner shows \"Vana is collecting your data\", nothing more.\n */\nexport type ConnectProgressPhase = \"collecting\" | \"writing\";\n\nconst PROGRESS_PHASES: readonly ConnectProgressPhase[] = [\n \"collecting\",\n \"writing\",\n];\n\n/** Height reports below this are a hidden or throttled frame, never content. */\nexport const MIN_SHEET_HEIGHT_PX = 80;\n\n/** Direct Data Controller status vocabulary, as the partner backend sees it. */\nexport type RequestStatus =\n | \"pending\"\n | \"approved\"\n | \"completed\"\n | \"denied\"\n | \"expired\";\n\n/**\n * A partner-signed, short-lived read capability for one request.\n *\n * @remarks\n * The sheet asks for it (`vana:request-needed`) once the person has signed\n * in, so the partner signs it fresh; it expires within minutes. It is the\n * Direct SDK request signature for `GET /api/data-connection-requests/<id>`,\n * minted by the partner's backend with its app key. Vana's server replays it\n * to the request authority, so the sheet shows the request's real scopes and\n * the partner cannot claim someone else's app.\n */\nexport interface RequestAuthorization {\n appAddress: `0x${string}`;\n signature: `0x${string}`;\n timestamp: string;\n}\n\n/** What the partner hands the sheet: a request id and its read capability. */\nexport interface ConnectRequest {\n authorization: RequestAuthorization;\n id: string;\n}\n\n/**\n * Error codes the sheet reports.\n *\n * @remarks\n * `frame_blocked`: no `vana:ready` in time, usually the page's own CSP.\n * `incompatible_version`: the sheet speaks a protocol version this SDK does\n * not (detected by the launcher).\n * `origin_not_registered`: this page's origin is not registered for the\n * client id.\n * `vana_unavailable`: Vana itself is misconfigured or down here. Nothing the\n * partner did, and nothing the partner can fix; the sheet stays open to say\n * so.\n * `sheet_failed`: the sheet could not finish this request.\n */\nexport type SheetErrorCode =\n | \"frame_blocked\"\n | \"incompatible_version\"\n | \"origin_not_registered\"\n | \"personal_server_unsupported\"\n | \"request_unavailable\"\n | \"sheet_failed\"\n | \"vana_unavailable\";\n\n/** Sheet to partner. */\nexport type SheetMessage =\n | { type: \"vana:ready\"; v: ProtocolVersion }\n | { height: number; type: \"vana:resize\"; v: ProtocolVersion }\n | { type: \"vana:signed-in\"; v: ProtocolVersion }\n | { type: \"vana:request-needed\"; v: ProtocolVersion }\n | {\n completion: ConnectCompletion;\n status: \"approved\";\n type: \"vana:connected\";\n v: ProtocolVersion;\n }\n | {\n phase: ConnectProgressPhase;\n type: \"vana:progress\";\n v: ProtocolVersion;\n }\n | { type: \"vana:close\"; v: ProtocolVersion }\n | { code: SheetErrorCode; type: \"vana:error\"; v: ProtocolVersion };\n\n/** Partner to sheet. */\nexport type PartnerMessage =\n | {\n request: ConnectRequest;\n type: \"vana:request\";\n v: ProtocolVersion;\n }\n | {\n status: RequestStatus;\n type: \"vana:request-status\";\n v: ProtocolVersion;\n };\n\nconst REQUEST_STATUSES: readonly RequestStatus[] = [\n \"pending\",\n \"approved\",\n \"completed\",\n \"denied\",\n \"expired\",\n];\n\nconst SHEET_ERROR_CODES: readonly SheetErrorCode[] = [\n \"frame_blocked\",\n \"incompatible_version\",\n \"origin_not_registered\",\n \"personal_server_unsupported\",\n \"request_unavailable\",\n \"sheet_failed\",\n \"vana_unavailable\",\n];\n\nconst HEX_ADDRESS = /^0x[0-9a-fA-F]{40}$/;\nconst HEX_SIGNATURE = /^0x[0-9a-fA-F]{130}$/;\nconst REQUEST_ID = /^[A-Za-z0-9_-]{1,128}$/;\nconst TIMESTAMP = /^[0-9]{10,16}$/;\n\nfunction record(value: unknown): Record<string, unknown> | null {\n return value && typeof value === \"object\" && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : null;\n}\n\nfunction isSupportedVersion(value: unknown): value is ProtocolVersion {\n return (\n typeof value === \"number\" &&\n Number.isInteger(value) &&\n value >= MIN_PROTOCOL_VERSION &&\n value <= PROTOCOL_VERSION\n );\n}\n\n/**\n * Whether a request id has the shape the sheet accepts.\n *\n * @remarks\n * The id is signed into a request path, so anything outside this shape could\n * name a different resource. Signers refuse it.\n *\n * @param value - The candidate request id.\n * @returns `true` for 1 to 128 characters of `[A-Za-z0-9_-]`.\n */\nexport function isRequestId(value: unknown): value is string {\n return typeof value === \"string\" && REQUEST_ID.test(value);\n}\n\nfunction readAuthorization(value: unknown): RequestAuthorization | null {\n const auth = record(value);\n if (!auth) {\n return null;\n }\n const { appAddress, signature, timestamp } = auth;\n if (\n typeof appAddress !== \"string\" ||\n !HEX_ADDRESS.test(appAddress) ||\n typeof signature !== \"string\" ||\n !HEX_SIGNATURE.test(signature) ||\n typeof timestamp !== \"string\" ||\n !TIMESTAMP.test(timestamp)\n ) {\n return null;\n }\n return {\n appAddress: appAddress as `0x${string}`,\n signature: signature as `0x${string}`,\n timestamp,\n };\n}\n\n/**\n * Parse a {@link ConnectRequest}. Extra fields are dropped.\n *\n * @param value - Untrusted input.\n * @returns The request, or `null` when malformed.\n */\nexport function readConnectRequest(value: unknown): ConnectRequest | null {\n const request = record(value);\n if (!request || !isRequestId(request.id)) {\n return null;\n }\n const authorization = readAuthorization(request.authorization);\n return authorization ? { authorization, id: request.id } : null;\n}\n\n/**\n * Detect a sheet that speaks a protocol version this SDK does not.\n *\n * @remarks\n * Only `vana:ready` and `vana:error` are read at any version: they are the\n * handshake, so the launcher can tell \"the sheet is up but incompatible\" from\n * \"the sheet never started\". Everything else at an unknown version is ignored.\n *\n * @param data - A message posted by the sheet.\n * @returns The sheet's version when it is outside the supported range,\n * otherwise `null`.\n */\nexport function readIncompatibleVersion(data: unknown): number | null {\n const message = record(data);\n if (\n !message ||\n (message.type !== \"vana:ready\" && message.type !== \"vana:error\")\n ) {\n return null;\n }\n const { v } = message;\n if (typeof v !== \"number\" || !Number.isInteger(v) || isSupportedVersion(v)) {\n return null;\n }\n return v;\n}\n\n/**\n * Parse a message the sheet posted.\n *\n * @remarks\n * Unknown fields are dropped. Unknown message types are ignored. An error\n * code this SDK does not know is read as `sheet_failed`, so a newer sheet's\n * failure is never silently lost.\n *\n * @param data - Untrusted `MessageEvent.data`.\n * @returns The message, or `null` for unknown or malformed data.\n */\nexport function parseSheetMessage(data: unknown): SheetMessage | null {\n const message = record(data);\n if (!(message && isSupportedVersion(message.v))) {\n return null;\n }\n const v = message.v;\n switch (message.type) {\n case \"vana:ready\":\n case \"vana:signed-in\":\n case \"vana:request-needed\":\n case \"vana:close\":\n return { type: message.type, v };\n case \"vana:resize\": {\n const { height } = message;\n if (typeof height !== \"number\" || !Number.isFinite(height)) {\n return null;\n }\n return { height, type: \"vana:resize\", v };\n }\n case \"vana:connected\": {\n const completion = CONNECT_COMPLETIONS.find(\n (known) => known === message.completion,\n );\n return message.status === \"approved\" && completion\n ? { completion, status: \"approved\", type: \"vana:connected\", v }\n : null;\n }\n case \"vana:progress\": {\n const phase = PROGRESS_PHASES.find((known) => known === message.phase);\n return phase ? { phase, type: \"vana:progress\", v } : null;\n }\n case \"vana:error\": {\n if (typeof message.code !== \"string\") {\n return null;\n }\n const code =\n SHEET_ERROR_CODES.find((known) => known === message.code) ??\n \"sheet_failed\";\n return { code, type: \"vana:error\", v };\n }\n default:\n return null;\n }\n}\n\n/**\n * Parse a message the partner posted.\n *\n * @param data - Untrusted `MessageEvent.data`.\n * @returns The message, or `null` for unknown or malformed data.\n */\nexport function parsePartnerMessage(data: unknown): PartnerMessage | null {\n const message = record(data);\n if (!(message && isSupportedVersion(message.v))) {\n return null;\n }\n const v = message.v;\n switch (message.type) {\n case \"vana:request\": {\n const request = readConnectRequest(message.request);\n return request ? { request, type: \"vana:request\", v } : null;\n }\n case \"vana:request-status\": {\n const status = REQUEST_STATUSES.find((known) => known === message.status);\n return status ? { status, type: \"vana:request-status\", v } : null;\n }\n default:\n return null;\n }\n}\n\n/**\n * Normalize to a bare origin.\n *\n * @param value - A URL or origin.\n * @returns `scheme://host[:port]` for http(s), or `null` for anything else.\n */\nexport function normalizeOrigin(value: string): string | null {\n try {\n const url = new URL(value);\n if (url.protocol !== \"https:\" && url.protocol !== \"http:\") {\n return null;\n }\n return url.origin;\n } catch {\n return null;\n }\n}\n\n/**\n * Build the client-check URL the launcher asks before the sheet is ready.\n *\n * @param input - The registered client id, the partner page origin, and the\n * Vana Account origin.\n * @returns `<vanaOrigin>/api/embed/connect/client?client_id&origin`.\n */\nexport function clientCheckUrl(input: {\n clientId: string;\n partnerOrigin: string;\n vanaOrigin: string;\n}): string {\n const url = new URL(CLIENT_CHECK_PATH, input.vanaOrigin);\n url.searchParams.set(SHEET_QUERY.clientId, input.clientId);\n url.searchParams.set(SHEET_QUERY.origin, input.partnerOrigin);\n return url.toString();\n}\n\n/**\n * Parse the client-check response body.\n *\n * @param data - Untrusted JSON.\n * @returns `{ registered }`, or `null` for anything else.\n */\nexport function parseClientCheck(\n data: unknown,\n): { registered: boolean } | null {\n const body = record(data);\n return body && typeof body.registered === \"boolean\"\n ? { registered: body.registered }\n : null;\n}\n\n/**\n * Build the sheet's iframe URL.\n *\n * @param input - The registered client id, the partner page origin, the Vana\n * Account origin, and optionally the display.\n * @returns `<vanaOrigin>/embed/connect?client_id&origin&v`, plus `display` when\n * it is not the default.\n */\nexport function sheetUrl(input: {\n clientId: string;\n display?: ConnectDisplay;\n partnerOrigin: string;\n vanaOrigin: string;\n}): string {\n const url = new URL(SHEET_PATH, input.vanaOrigin);\n url.searchParams.set(SHEET_QUERY.clientId, input.clientId);\n url.searchParams.set(SHEET_QUERY.origin, input.partnerOrigin);\n url.searchParams.set(SHEET_QUERY.version, String(PROTOCOL_VERSION));\n // The default is left out: a bottom sheet's URL is what it has always been.\n if (input.display && input.display !== DEFAULT_CONNECT_DISPLAY) {\n url.searchParams.set(SHEET_QUERY.display, input.display);\n }\n return url.toString();\n}\n\n/**\n * Content-Security-Policy sources a partner page needs for the sheet.\n *\n * @remarks\n * The launcher is bundled into the partner's own code, so only the frame\n * and the launcher's client check (a credential-less `fetch`) need\n * allowing. Merge the result into your existing policy; if the page sets\n * `child-src` but not `frame-src`, add the origin there instead. There is no\n * default target, so a dev app never allows production by accident.\n *\n * @example\n * ```typescript\n * const csp = vanaConnectCsp(\"production\");\n * // { \"frame-src\": [\"https://account.vana.org\"],\n * // \"connect-src\": [\"https://account.vana.org\"] }\n * const header = Object.entries(csp)\n * .map(([directive, sources]) => `${directive} 'self' ${sources.join(\" \")}`)\n * .join(\"; \");\n * ```\n *\n * @param target - `\"production\"`, `\"dev\"`, or `{ accountOrigin }` for a\n * custom Vana Account deployment.\n * @returns The directives to add, each with the sources it needs.\n */\nexport function vanaConnectCsp(\n target: VanaConnectEnv | { accountOrigin: string },\n): { \"connect-src\": string[]; \"frame-src\": string[] } {\n const origin =\n target === \"production\" || target === \"dev\"\n ? VANA_ACCOUNT_ORIGINS[target]\n : typeof target === \"object\" &&\n target !== null &&\n typeof target.accountOrigin === \"string\"\n ? normalizeOrigin(target.accountOrigin)\n : null;\n if (!origin) {\n throw new TypeError(\n 'vanaConnectCsp needs \"production\", \"dev\" or an http(s) accountOrigin',\n );\n }\n return { \"connect-src\": [origin], \"frame-src\": [origin] };\n}\n"],"mappings":"AAqDO,MAAM,mBAAmB;AAGzB,MAAM,uBAAuB;AAM7B,MAAM,aAAa;AAUnB,MAAM,oBAAoB;AAG1B,MAAM,cAAc;AAAA,EACzB,UAAU;AAAA,EACV,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,SAAS;AACX;AAUO,MAAM,mBAAmB,CAAC,SAAS,YAAY;AAM/C,MAAM,0BAA0C;AAQhD,SAAS,mBAAmB,OAAuC;AACxE,SAAO,iBAAiB,KAAK,CAAC,UAAU,UAAU,KAAK,KAAK;AAC9D;AAGO,MAAM,uBAAuB;AAAA,EAClC,YAAY;AAAA,EACZ,KAAK;AACP;AAcO,MAAM,sBAAsB,qBAAqB;AAaxD,MAAM,sBAAoD;AAAA,EACxD;AAAA,EACA;AACF;AASA,MAAM,kBAAmD;AAAA,EACvD;AAAA,EACA;AACF;AAGO,MAAM,sBAAsB;AAyFnC,MAAM,mBAA6C;AAAA,EACjD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,oBAA+C;AAAA,EACnD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,cAAc;AACpB,MAAM,gBAAgB;AACtB,MAAM,aAAa;AACnB,MAAM,YAAY;AAElB,SAAS,OAAO,OAAgD;AAC9D,SAAO,SAAS,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,IAC5D,QACD;AACN;AAEA,SAAS,mBAAmB,OAA0C;AACpE,SACE,OAAO,UAAU,YACjB,OAAO,UAAU,KAAK,KACtB,SAAS,wBACT,SAAS;AAEb;AAYO,SAAS,YAAY,OAAiC;AAC3D,SAAO,OAAO,UAAU,YAAY,WAAW,KAAK,KAAK;AAC3D;AAEA,SAAS,kBAAkB,OAA6C;AACtE,QAAM,OAAO,OAAO,KAAK;AACzB,MAAI,CAAC,MAAM;AACT,WAAO;AAAA,EACT;AACA,QAAM,EAAE,YAAY,WAAW,UAAU,IAAI;AAC7C,MACE,OAAO,eAAe,YACtB,CAAC,YAAY,KAAK,UAAU,KAC5B,OAAO,cAAc,YACrB,CAAC,cAAc,KAAK,SAAS,KAC7B,OAAO,cAAc,YACrB,CAAC,UAAU,KAAK,SAAS,GACzB;AACA,WAAO;AAAA,EACT;AACA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAQO,SAAS,mBAAmB,OAAuC;AACxE,QAAM,UAAU,OAAO,KAAK;AAC5B,MAAI,CAAC,WAAW,CAAC,YAAY,QAAQ,EAAE,GAAG;AACxC,WAAO;AAAA,EACT;AACA,QAAM,gBAAgB,kBAAkB,QAAQ,aAAa;AAC7D,SAAO,gBAAgB,EAAE,eAAe,IAAI,QAAQ,GAAG,IAAI;AAC7D;AAcO,SAAS,wBAAwB,MAA8B;AACpE,QAAM,UAAU,OAAO,IAAI;AAC3B,MACE,CAAC,WACA,QAAQ,SAAS,gBAAgB,QAAQ,SAAS,cACnD;AACA,WAAO;AAAA,EACT;AACA,QAAM,EAAE,EAAE,IAAI;AACd,MAAI,OAAO,MAAM,YAAY,CAAC,OAAO,UAAU,CAAC,KAAK,mBAAmB,CAAC,GAAG;AAC1E,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAaO,SAAS,kBAAkB,MAAoC;AACpE,QAAM,UAAU,OAAO,IAAI;AAC3B,MAAI,EAAE,WAAW,mBAAmB,QAAQ,CAAC,IAAI;AAC/C,WAAO;AAAA,EACT;AACA,QAAM,IAAI,QAAQ;AAClB,UAAQ,QAAQ,MAAM;AAAA,IACpB,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AACH,aAAO,EAAE,MAAM,QAAQ,MAAM,EAAE;AAAA,IACjC,KAAK,eAAe;AAClB,YAAM,EAAE,OAAO,IAAI;AACnB,UAAI,OAAO,WAAW,YAAY,CAAC,OAAO,SAAS,MAAM,GAAG;AAC1D,eAAO;AAAA,MACT;AACA,aAAO,EAAE,QAAQ,MAAM,eAAe,EAAE;AAAA,IAC1C;AAAA,IACA,KAAK,kBAAkB;AACrB,YAAM,aAAa,oBAAoB;AAAA,QACrC,CAAC,UAAU,UAAU,QAAQ;AAAA,MAC/B;AACA,aAAO,QAAQ,WAAW,cAAc,aACpC,EAAE,YAAY,QAAQ,YAAY,MAAM,kBAAkB,EAAE,IAC5D;AAAA,IACN;AAAA,IACA,KAAK,iBAAiB;AACpB,YAAM,QAAQ,gBAAgB,KAAK,CAAC,UAAU,UAAU,QAAQ,KAAK;AACrE,aAAO,QAAQ,EAAE,OAAO,MAAM,iBAAiB,EAAE,IAAI;AAAA,IACvD;AAAA,IACA,KAAK,cAAc;AACjB,UAAI,OAAO,QAAQ,SAAS,UAAU;AACpC,eAAO;AAAA,MACT;AACA,YAAM,OACJ,kBAAkB,KAAK,CAAC,UAAU,UAAU,QAAQ,IAAI,KACxD;AACF,aAAO,EAAE,MAAM,MAAM,cAAc,EAAE;AAAA,IACvC;AAAA,IACA;AACE,aAAO;AAAA,EACX;AACF;AAQO,SAAS,oBAAoB,MAAsC;AACxE,QAAM,UAAU,OAAO,IAAI;AAC3B,MAAI,EAAE,WAAW,mBAAmB,QAAQ,CAAC,IAAI;AAC/C,WAAO;AAAA,EACT;AACA,QAAM,IAAI,QAAQ;AAClB,UAAQ,QAAQ,MAAM;AAAA,IACpB,KAAK,gBAAgB;AACnB,YAAM,UAAU,mBAAmB,QAAQ,OAAO;AAClD,aAAO,UAAU,EAAE,SAAS,MAAM,gBAAgB,EAAE,IAAI;AAAA,IAC1D;AAAA,IACA,KAAK,uBAAuB;AAC1B,YAAM,SAAS,iBAAiB,KAAK,CAAC,UAAU,UAAU,QAAQ,MAAM;AACxE,aAAO,SAAS,EAAE,QAAQ,MAAM,uBAAuB,EAAE,IAAI;AAAA,IAC/D;AAAA,IACA;AACE,aAAO;AAAA,EACX;AACF;AAQO,SAAS,gBAAgB,OAA8B;AAC5D,MAAI;AACF,UAAM,MAAM,IAAI,IAAI,KAAK;AACzB,QAAI,IAAI,aAAa,YAAY,IAAI,aAAa,SAAS;AACzD,aAAO;AAAA,IACT;AACA,WAAO,IAAI;AAAA,EACb,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AASO,SAAS,eAAe,OAIpB;AACT,QAAM,MAAM,IAAI,IAAI,mBAAmB,MAAM,UAAU;AACvD,MAAI,aAAa,IAAI,YAAY,UAAU,MAAM,QAAQ;AACzD,MAAI,aAAa,IAAI,YAAY,QAAQ,MAAM,aAAa;AAC5D,SAAO,IAAI,SAAS;AACtB;AAQO,SAAS,iBACd,MACgC;AAChC,QAAM,OAAO,OAAO,IAAI;AACxB,SAAO,QAAQ,OAAO,KAAK,eAAe,YACtC,EAAE,YAAY,KAAK,WAAW,IAC9B;AACN;AAUO,SAAS,SAAS,OAKd;AACT,QAAM,MAAM,IAAI,IAAI,YAAY,MAAM,UAAU;AAChD,MAAI,aAAa,IAAI,YAAY,UAAU,MAAM,QAAQ;AACzD,MAAI,aAAa,IAAI,YAAY,QAAQ,MAAM,aAAa;AAC5D,MAAI,aAAa,IAAI,YAAY,SAAS,OAAO,gBAAgB,CAAC;AAElE,MAAI,MAAM,WAAW,MAAM,YAAY,yBAAyB;AAC9D,QAAI,aAAa,IAAI,YAAY,SAAS,MAAM,OAAO;AAAA,EACzD;AACA,SAAO,IAAI,SAAS;AACtB;AA0BO,SAAS,eACd,QACoD;AACpD,QAAM,SACJ,WAAW,gBAAgB,WAAW,QAClC,qBAAqB,MAAM,IAC3B,OAAO,WAAW,YAChB,WAAW,QACX,OAAO,OAAO,kBAAkB,WAChC,gBAAgB,OAAO,aAAa,IACpC;AACR,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,SAAO,EAAE,eAAe,CAAC,MAAM,GAAG,aAAa,CAAC,MAAM,EAAE;AAC1D;","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|