@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
package/README.md
CHANGED
|
@@ -261,6 +261,49 @@ accepts write entries in `scopes`, but its scope part must be a concrete
|
|
|
261
261
|
scope: this flow reads approved scopes back one at a time, so wildcards are
|
|
262
262
|
rejected there for read and write alike.
|
|
263
263
|
|
|
264
|
+
### PDPP grants
|
|
265
|
+
|
|
266
|
+
Ask for PDPP data as **grants**: a registry `source_id` and the streams you
|
|
267
|
+
want from it. The controller turns each stream into one scope key,
|
|
268
|
+
`pdpp.<source>.<stream>`, and that key is what the request, the signed grant
|
|
269
|
+
and your reads carry.
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
export const vana = createDirectDataController({
|
|
273
|
+
// ...
|
|
274
|
+
source: "chatgpt",
|
|
275
|
+
grants: [
|
|
276
|
+
{
|
|
277
|
+
source_id: "https://registry.pdpp.dev/sources/chatgpt",
|
|
278
|
+
streams: ["conversations", "messages"],
|
|
279
|
+
},
|
|
280
|
+
],
|
|
281
|
+
});
|
|
282
|
+
// Requests ["pdpp.chatgpt.conversations", "pdpp.chatgpt.messages"].
|
|
283
|
+
|
|
284
|
+
await vana.readApprovedData({ requestId, scope: "pdpp.chatgpt.messages" });
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
- `grants` and `scopes` can be combined. The request is the union: your
|
|
288
|
+
`scopes` verbatim, then the derived keys that are not already in it.
|
|
289
|
+
Existing apps that pass only `scopes` are unchanged.
|
|
290
|
+
- The `pdpp.` namespace is separate from legacy scopes. PDPP's ChatGPT
|
|
291
|
+
`conversations` stream is `pdpp.chatgpt.conversations`, not the legacy
|
|
292
|
+
`chatgpt.conversations`, and `pdpp.chatgpt.*` covers only PDPP streams.
|
|
293
|
+
A legacy `scopes` entry under `pdpp.` that is not a valid key is rejected.
|
|
294
|
+
- The mapping is strict and never slugifies. `source_id` must be
|
|
295
|
+
`https://registry.pdpp.dev/sources/<source>`, and `<source>` and each stream
|
|
296
|
+
must be lowercase letters, digits and underscores, not starting with an
|
|
297
|
+
underscore. Anything else throws `InvalidPdppGrantError` when the controller
|
|
298
|
+
is created.
|
|
299
|
+
- `pdppScopeKey(source_id, stream)`, `parsePdppScopeKey(key)` (`{ source_id,
|
|
300
|
+
stream }` or `null`), `isPdppScopeKey(key)`, `pdppGrantsToScopes(grants)`
|
|
301
|
+
and `grantRequestScopes({ grants, scopes })` are exported from `/server`,
|
|
302
|
+
the root entry points and `@opendatalabs/vana-sdk/protocol/scopes`. Use them
|
|
303
|
+
rather than building keys by hand. Other implementations can check
|
|
304
|
+
themselves against the vectors in
|
|
305
|
+
[`src/protocol/fixtures/pdpp-scope-keys.json`](src/protocol/fixtures/pdpp-scope-keys.json).
|
|
306
|
+
|
|
264
307
|
### Backend controller
|
|
265
308
|
|
|
266
309
|
```typescript
|
|
@@ -457,6 +500,139 @@ pass the same explicit `idempotencyKey` on the retry to avoid a duplicate DCR.
|
|
|
457
500
|
`react` is an optional peer dependency. The underlying
|
|
458
501
|
`createDirectConnectFlow` store is also exported for non-React frontends.
|
|
459
502
|
|
|
503
|
+
### Connect in Vana's in-page sheet
|
|
504
|
+
|
|
505
|
+
Instead of a Vana tab, the person can approve in a sheet Vana draws over your
|
|
506
|
+
page (it signs them in, shows the request, and finishes inside the sheet). The
|
|
507
|
+
sheet is `presentation: "sheet"` of the same flow: the request, the status poll
|
|
508
|
+
and the read are unchanged. The integration is two files.
|
|
509
|
+
|
|
510
|
+
Backend: one catch-all route serves `config`, `request`, `status` and `data`,
|
|
511
|
+
binding each request to the browser that created it. The controller decides
|
|
512
|
+
what is requested (its `grants` and `scopes`, see [PDPP grants](#pdpp-grants));
|
|
513
|
+
the browser never chooses.
|
|
514
|
+
|
|
515
|
+
```ts
|
|
516
|
+
// app/api/vana/[action]/route.ts
|
|
517
|
+
import { createVanaConnectHandler } from "@opendatalabs/vana-sdk/server";
|
|
518
|
+
import { vana } from "@/lib/vana"; // your createDirectDataController({ grants, ... })
|
|
519
|
+
|
|
520
|
+
const handler = createVanaConnectHandler({
|
|
521
|
+
controller: vana,
|
|
522
|
+
returnUrl: `${process.env.APP_URL}/connect/return`,
|
|
523
|
+
clientId: "my-app", // the embed client id Vana Account registered for your origin
|
|
524
|
+
// Recommended in production: only your signed-in users create requests.
|
|
525
|
+
session: async (request) => (await getSession(request))?.userId ?? null,
|
|
526
|
+
});
|
|
527
|
+
export { handler as GET, handler as POST };
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
The handler reports the Vana Account origin from the controller's `env`
|
|
531
|
+
(`account.vana.org` for `production`, `account-dev.vana.org` for `dev`), so a
|
|
532
|
+
dev app never frames production.
|
|
533
|
+
|
|
534
|
+
Frontend:
|
|
535
|
+
|
|
536
|
+
```tsx
|
|
537
|
+
"use client";
|
|
538
|
+
import { useVanaConnect } from "@opendatalabs/vana-sdk/react";
|
|
539
|
+
|
|
540
|
+
export function ConnectButton() {
|
|
541
|
+
const { state, start } = useVanaConnect<MyData>({ endpoint: "/api/vana" });
|
|
542
|
+
if (state.type === "done") return <Result data={state.result.data} />;
|
|
543
|
+
return (
|
|
544
|
+
<>
|
|
545
|
+
<button
|
|
546
|
+
disabled={
|
|
547
|
+
state.type === "creating" || state.type === "awaiting_approval"
|
|
548
|
+
}
|
|
549
|
+
onClick={start}
|
|
550
|
+
>
|
|
551
|
+
Connect with Vana
|
|
552
|
+
</button>
|
|
553
|
+
{state.type === "awaiting_approval" && state.popupBlocked && (
|
|
554
|
+
// The sheet could not run here (state.sheetUnavailable says why).
|
|
555
|
+
<a href={state.request.approvalUrl} target="_blank" rel="noreferrer">
|
|
556
|
+
Open Vana
|
|
557
|
+
</a>
|
|
558
|
+
)}
|
|
559
|
+
</>
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
- While the sheet is up, `state.sheet` reports `completion` (`in_browser` or
|
|
565
|
+
`remote_collect`) once the person approves, coarse `progress`
|
|
566
|
+
(`collecting`, `writing`), and any `error` it is showing.
|
|
567
|
+
- **Approval is not delivery.** The sheet's `connected` event (and
|
|
568
|
+
`state.sheet.completion`) means the person approved; the data may still be
|
|
569
|
+
collecting. The flow reaches `done` only when your backend's status poll
|
|
570
|
+
says the request is readable and the read succeeds, and `error` when the
|
|
571
|
+
poll reports `denied`, `expired` or `completed` or the read fails. The sheet
|
|
572
|
+
sends no terminal event of its own; its events only make the flow poll
|
|
573
|
+
sooner.
|
|
574
|
+
- Closing the sheet before approving returns to `idle`. Closing it after
|
|
575
|
+
approving keeps polling, since the data may still be readable.
|
|
576
|
+
- If the frame cannot run (`frame_blocked`, `origin_not_registered`,
|
|
577
|
+
`incompatible_version`), the flow falls back to the tab presentation with
|
|
578
|
+
`popupBlocked: true`, so the "Open approval" link above still finishes it.
|
|
579
|
+
`origin_not_registered` arrives at once: the launcher asks Vana
|
|
580
|
+
(`GET <account>/api/embed/connect/client`, no credentials) alongside
|
|
581
|
+
framing. `vana_unavailable` means Vana's own side is down or misconfigured;
|
|
582
|
+
the sheet stays open and says so.
|
|
583
|
+
- The partner reads; it cannot write through the sheet. Pass
|
|
584
|
+
`sheet: { closeAfterDoneMs: null }` to leave the sheet open after the read.
|
|
585
|
+
- If your page sets a CSP, allow the frame and the client check:
|
|
586
|
+
`vanaConnectCsp(env)` from `/server` (or `/connect/protocol`) returns
|
|
587
|
+
`{ "frame-src": [accountOrigin], "connect-src": [accountOrigin] }`. It has
|
|
588
|
+
no default environment.
|
|
589
|
+
- `createVanaConnectHandler` takes `session` to bind requests to your own
|
|
590
|
+
signed-in user (recommended: without it anyone who loads your page can
|
|
591
|
+
create requests as your app), `createOptions` for per-request create input
|
|
592
|
+
(questions, owner), and `read` to replace the default `readApprovedData`.
|
|
593
|
+
Use `toNodeHandler(handler)` for Express or plain `http`.
|
|
594
|
+
- Creating a request is rate limited. The default, `createMemoryRateLimit()`,
|
|
595
|
+
allows 10 creates a minute per client IP and per session, in memory. It is
|
|
596
|
+
not production-grade (each instance counts alone, and the IP comes from
|
|
597
|
+
proxy headers): pass `rateLimit` backed by a shared store, or `false` when
|
|
598
|
+
a limiter already sits in front of the route. A refusal is
|
|
599
|
+
`429 rate_limited` with `Retry-After`.
|
|
600
|
+
|
|
601
|
+
Choosing the Vana Account origin yourself (a custom flow, or preview deploys
|
|
602
|
+
of Vana Account) goes through `resolveVanaAccountOrigin`; never match origins
|
|
603
|
+
with your own pattern. It returns the environment's origin unless overrides
|
|
604
|
+
are switched on for this build and the override is exactly an `https` origin
|
|
605
|
+
on your allowlist:
|
|
606
|
+
|
|
607
|
+
```ts
|
|
608
|
+
import {
|
|
609
|
+
parseOriginAllowlist,
|
|
610
|
+
resolveVanaAccountOrigin,
|
|
611
|
+
} from "@opendatalabs/vana-sdk/connect";
|
|
612
|
+
|
|
613
|
+
const accountOrigin = resolveVanaAccountOrigin({
|
|
614
|
+
env: "dev",
|
|
615
|
+
override: new URLSearchParams(location.search).get("vana_account"),
|
|
616
|
+
previewOrigins: parseOriginAllowlist(
|
|
617
|
+
process.env.NEXT_PUBLIC_VANA_ACCOUNT_PREVIEW_ORIGINS,
|
|
618
|
+
),
|
|
619
|
+
// From build-time config, never the request. Off in production.
|
|
620
|
+
allowOverride: process.env.NEXT_PUBLIC_DEPLOY_ENV === "preview",
|
|
621
|
+
});
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
`createDirectConnectFlow` and the launcher take that origin (or an `env`)
|
|
625
|
+
explicitly; neither falls back to production.
|
|
626
|
+
|
|
627
|
+
Lower-level pieces: `createDirectConnectFlow(transports, { presentation:
|
|
628
|
+
"sheet", sheet: { clientId, appName, accountOrigin } })` with an
|
|
629
|
+
`authorizeRequest` transport backed by
|
|
630
|
+
`controller.authorizeRequestRead(requestId)`; the launcher itself
|
|
631
|
+
(`@opendatalabs/vana-sdk/connect`); and the zero-dependency message protocol
|
|
632
|
+
(`@opendatalabs/vana-sdk/connect/protocol`). A backend in another language
|
|
633
|
+
implements the [connect wire contract](./src/connect/WIRE_CONTRACT.md), which has test
|
|
634
|
+
vectors.
|
|
635
|
+
|
|
460
636
|
### Test with large sample data
|
|
461
637
|
|
|
462
638
|
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 {};
|