@opendatalabs/vana-sdk 3.14.1 → 3.16.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 +52 -5
- package/dist/direct/access-request-client.cjs +31 -3
- package/dist/direct/access-request-client.cjs.map +1 -1
- package/dist/direct/access-request-client.d.ts +12 -1
- package/dist/direct/access-request-client.js +31 -3
- package/dist/direct/access-request-client.js.map +1 -1
- package/dist/direct/connect-flow.cjs +63 -6
- package/dist/direct/connect-flow.cjs.map +1 -1
- package/dist/direct/connect-flow.d.ts +30 -4
- package/dist/direct/connect-flow.js +63 -6
- package/dist/direct/connect-flow.js.map +1 -1
- package/dist/direct/controller.cjs +4 -1
- package/dist/direct/controller.cjs.map +1 -1
- package/dist/direct/controller.d.ts +11 -3
- package/dist/direct/controller.js +4 -1
- package/dist/direct/controller.js.map +1 -1
- package/dist/direct/types.cjs +28 -2
- package/dist/direct/types.cjs.map +1 -1
- package/dist/direct/types.d.ts +51 -0
- package/dist/direct/types.js +26 -1
- package/dist/direct/types.js.map +1 -1
- package/dist/direct/use-direct-vana-connect.cjs +3 -0
- package/dist/direct/use-direct-vana-connect.cjs.map +1 -1
- package/dist/direct/use-direct-vana-connect.js +3 -0
- package/dist/direct/use-direct-vana-connect.js.map +1 -1
- package/dist/direct/use-direct-vana-connect.test.d.ts +1 -0
- package/dist/index.browser.d.ts +1 -0
- package/dist/index.browser.js +167 -2
- package/dist/index.browser.js.map +3 -3
- package/dist/index.node.cjs +175 -2
- package/dist/index.node.cjs.map +3 -3
- package/dist/index.node.d.ts +2 -1
- package/dist/index.node.js +167 -2
- package/dist/index.node.js.map +3 -3
- package/dist/protocol/gateway.cjs +16 -2
- package/dist/protocol/gateway.cjs.map +1 -1
- package/dist/protocol/gateway.d.ts +2 -0
- package/dist/protocol/gateway.js +16 -2
- package/dist/protocol/gateway.js.map +1 -1
- package/dist/protocol/scope-actions.cjs +185 -0
- package/dist/protocol/scope-actions.cjs.map +1 -0
- package/dist/protocol/scope-actions.d.ts +145 -0
- package/dist/protocol/scope-actions.js +154 -0
- package/dist/protocol/scope-actions.js.map +1 -0
- package/dist/protocol/scope-actions.test.d.ts +1 -0
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.ts +9 -2
- package/dist/react.js.map +1 -1
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,185 @@
|
|
|
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 scope_actions_exports = {};
|
|
20
|
+
__export(scope_actions_exports, {
|
|
21
|
+
InvalidScopeEntryError: () => InvalidScopeEntryError,
|
|
22
|
+
SCOPE_ACTIONS: () => SCOPE_ACTIONS,
|
|
23
|
+
formatScopeEntry: () => formatScopeEntry,
|
|
24
|
+
grantPermissions: () => grantPermissions,
|
|
25
|
+
hasAction: () => hasAction,
|
|
26
|
+
parseScopeEntry: () => parseScopeEntry,
|
|
27
|
+
permissionsToScopes: () => permissionsToScopes,
|
|
28
|
+
tryGrantPermissions: () => tryGrantPermissions
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(scope_actions_exports);
|
|
31
|
+
var import_scopes = require("./scopes");
|
|
32
|
+
const SCOPE_ACTIONS = ["read", "write"];
|
|
33
|
+
class InvalidScopeEntryError extends Error {
|
|
34
|
+
/** The offending entry, verbatim (unknown because it may not be a string). */
|
|
35
|
+
entry;
|
|
36
|
+
constructor(entry, reason) {
|
|
37
|
+
super(`Invalid scope entry ${describeValue(entry)}: ${reason}`);
|
|
38
|
+
this.name = "InvalidScopeEntryError";
|
|
39
|
+
this.entry = entry;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const OPERATION_SEPARATOR = ":";
|
|
43
|
+
function describeValue(value) {
|
|
44
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
45
|
+
if (value === null) return "null";
|
|
46
|
+
return `[${typeof value}]`;
|
|
47
|
+
}
|
|
48
|
+
const OPERATION_BY_PREFIX = {
|
|
49
|
+
write: "write"
|
|
50
|
+
};
|
|
51
|
+
function assertScopePart(entry, scope) {
|
|
52
|
+
if (scope.length === 0) {
|
|
53
|
+
throw new InvalidScopeEntryError(entry, "scope part is empty");
|
|
54
|
+
}
|
|
55
|
+
if (scope.includes(OPERATION_SEPARATOR)) {
|
|
56
|
+
throw new InvalidScopeEntryError(
|
|
57
|
+
entry,
|
|
58
|
+
`scope part must not contain "${OPERATION_SEPARATOR}"`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function parseScopeEntry(entry) {
|
|
63
|
+
const raw = entry;
|
|
64
|
+
if (typeof raw !== "string") {
|
|
65
|
+
throw new InvalidScopeEntryError(raw, "entry must be a string");
|
|
66
|
+
}
|
|
67
|
+
const separatorIndex = entry.indexOf(OPERATION_SEPARATOR);
|
|
68
|
+
if (separatorIndex === -1) {
|
|
69
|
+
assertScopePart(entry, entry);
|
|
70
|
+
return { scope: entry, action: "read" };
|
|
71
|
+
}
|
|
72
|
+
const prefix = entry.slice(0, separatorIndex);
|
|
73
|
+
const scope = entry.slice(separatorIndex + 1);
|
|
74
|
+
const action = Object.hasOwn(OPERATION_BY_PREFIX, prefix) ? OPERATION_BY_PREFIX[prefix] : void 0;
|
|
75
|
+
if (action === void 0) {
|
|
76
|
+
throw new InvalidScopeEntryError(
|
|
77
|
+
entry,
|
|
78
|
+
`unknown operation "${prefix}" (known: ${Object.keys(OPERATION_BY_PREFIX).join(", ")}; read has no prefix)`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
assertScopePart(entry, scope);
|
|
82
|
+
return { scope, action };
|
|
83
|
+
}
|
|
84
|
+
function formatScopeEntry(parsed) {
|
|
85
|
+
const { scope, action } = parsed;
|
|
86
|
+
assertScopePart(scope, scope);
|
|
87
|
+
if (action === "read") return scope;
|
|
88
|
+
const prefix = Object.entries(OPERATION_BY_PREFIX).find(
|
|
89
|
+
([, candidate]) => candidate === action
|
|
90
|
+
)?.[0];
|
|
91
|
+
if (prefix === void 0) {
|
|
92
|
+
throw new InvalidScopeEntryError(
|
|
93
|
+
scope,
|
|
94
|
+
`unknown action ${describeValue(action)} (known: ${SCOPE_ACTIONS.join(", ")})`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
return `${prefix}${OPERATION_SEPARATOR}${scope}`;
|
|
98
|
+
}
|
|
99
|
+
function compareScopes(a, b) {
|
|
100
|
+
if (a < b) return -1;
|
|
101
|
+
if (a > b) return 1;
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
function sortActions(actions) {
|
|
105
|
+
const present = new Set(actions);
|
|
106
|
+
return SCOPE_ACTIONS.filter((action) => present.has(action));
|
|
107
|
+
}
|
|
108
|
+
function grantPermissions(scopes) {
|
|
109
|
+
const byScope = /* @__PURE__ */ new Map();
|
|
110
|
+
for (const entry of scopes) {
|
|
111
|
+
const { scope, action } = parseScopeEntry(entry);
|
|
112
|
+
let actions = byScope.get(scope);
|
|
113
|
+
if (actions === void 0) {
|
|
114
|
+
actions = /* @__PURE__ */ new Set();
|
|
115
|
+
byScope.set(scope, actions);
|
|
116
|
+
}
|
|
117
|
+
actions.add(action);
|
|
118
|
+
}
|
|
119
|
+
return [...byScope.keys()].sort(compareScopes).map((scope) => ({
|
|
120
|
+
scope,
|
|
121
|
+
actions: sortActions(byScope.get(scope) ?? [])
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
function permissionsToScopes(permissions) {
|
|
125
|
+
const byScope = /* @__PURE__ */ new Map();
|
|
126
|
+
for (const { scope, actions } of permissions) {
|
|
127
|
+
let merged = byScope.get(scope);
|
|
128
|
+
if (merged === void 0) {
|
|
129
|
+
merged = /* @__PURE__ */ new Set();
|
|
130
|
+
byScope.set(scope, merged);
|
|
131
|
+
}
|
|
132
|
+
for (const action of actions) {
|
|
133
|
+
if (!SCOPE_ACTIONS.includes(action)) {
|
|
134
|
+
throw new InvalidScopeEntryError(
|
|
135
|
+
scope,
|
|
136
|
+
`unknown action ${describeValue(action)} (known: ${SCOPE_ACTIONS.join(", ")})`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
merged.add(action);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const entries = [];
|
|
143
|
+
for (const scope of [...byScope.keys()].sort(compareScopes)) {
|
|
144
|
+
for (const action of sortActions(byScope.get(scope) ?? [])) {
|
|
145
|
+
entries.push(formatScopeEntry({ scope, action }));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return entries;
|
|
149
|
+
}
|
|
150
|
+
function hasAction(scopes, scope, action) {
|
|
151
|
+
if (scope.includes(OPERATION_SEPARATOR)) return false;
|
|
152
|
+
for (const entry of scopes) {
|
|
153
|
+
let parsed;
|
|
154
|
+
try {
|
|
155
|
+
parsed = parseScopeEntry(entry);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (error instanceof InvalidScopeEntryError) continue;
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
if (parsed.action === action && (0, import_scopes.scopeMatchesPattern)(scope, parsed.scope)) {
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
function tryGrantPermissions(scopes) {
|
|
167
|
+
try {
|
|
168
|
+
return grantPermissions(scopes);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
if (error instanceof InvalidScopeEntryError) return void 0;
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
175
|
+
0 && (module.exports = {
|
|
176
|
+
InvalidScopeEntryError,
|
|
177
|
+
SCOPE_ACTIONS,
|
|
178
|
+
formatScopeEntry,
|
|
179
|
+
grantPermissions,
|
|
180
|
+
hasAction,
|
|
181
|
+
parseScopeEntry,
|
|
182
|
+
permissionsToScopes,
|
|
183
|
+
tryGrantPermissions
|
|
184
|
+
});
|
|
185
|
+
//# sourceMappingURL=scope-actions.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/protocol/scope-actions.ts"],"sourcesContent":["import { scopeMatchesPattern } from \"./scopes\";\n\n/**\n * Grant scope-entry grammar.\n *\n * A signed grant carries `scopes: string[]`. Each entry is\n * `[operation:]scope` - an optional lowercase ASCII operation prefix before\n * the first `:`, then a scope pattern (`*`, `{prefix}.*`, or an exact scope).\n * A missing prefix means read. `write:notes.entries` authorizes writing\n * `notes.entries` and nothing else; `notes.entries` authorizes reading it.\n *\n * The string form is the wire and storage detail: it is what the grantor\n * signs (EIP-712 `GrantRegistration.scopes`) and what the gateway stores\n * verbatim. The Personal Server is the sole interpreter, and this module is\n * the SDK-side mirror of that interpretation. Builders and consent UIs should\n * work with the grouped `{ scope, actions }` view (see\n * {@link grantPermissions}) and never construct or parse the strings by hand.\n *\n * Matching rules, pinned by the Personal Server policy\n * (personal-server-ts `packages/core/src/policy/data-write.ts` and\n * `data-read.ts`):\n * - the operation is compared exactly, case-sensitively;\n * - wildcards apply to the scope part only, via {@link scopeMatchesPattern};\n * - an entry whose operation is not recognised never authorizes anything.\n * The parser fails closed on it (throws) rather than treating it as read;\n * the matcher ({@link hasAction}) skips it, which is how the Personal\n * Server treats an entry it does not understand.\n */\n\n/** Operations the grammar defines today, in canonical (output) order. */\nexport const SCOPE_ACTIONS = [\"read\", \"write\"] as const;\n\n/** An operation a grant entry can authorize over a scope. */\nexport type ScopeAction = (typeof SCOPE_ACTIONS)[number];\n\n/** One grant entry, split into its operation and scope pattern. */\nexport interface ParsedScopeEntry {\n scope: string;\n action: ScopeAction;\n}\n\n/**\n * The grouped view of a grant's scope entries: one row per scope pattern\n * with every operation the grant authorizes over it.\n */\nexport interface GrantPermission {\n scope: string;\n actions: ScopeAction[];\n}\n\n/**\n * Thrown when a scope entry does not fit the grammar - an unknown or\n * malformed operation prefix, or an empty scope part.\n */\nexport class InvalidScopeEntryError extends Error {\n /** The offending entry, verbatim (unknown because it may not be a string). */\n readonly entry: unknown;\n\n constructor(entry: unknown, reason: string) {\n super(`Invalid scope entry ${describeValue(entry)}: ${reason}`);\n this.name = \"InvalidScopeEntryError\";\n this.entry = entry;\n }\n}\n\nconst OPERATION_SEPARATOR = \":\";\n\n// Render an untrusted value for an error message without ever throwing:\n// String() and JSON.stringify() both defer to the value's own toString /\n// toJSON, which a hostile JSON body can make throw.\nfunction describeValue(value: unknown): string {\n if (typeof value === \"string\") return JSON.stringify(value);\n if (value === null) return \"null\";\n return `[${typeof value}]`;\n}\n\n// The only operation that is ever written out. Read has no prefix, and\n// `read:` is NOT an alias for it: the Personal Server's read policy matches\n// entries verbatim, so a `read:x` entry would authorize nothing there, and\n// the parser must reject it for the same reason.\nconst OPERATION_BY_PREFIX: Readonly<Record<string, ScopeAction>> = {\n write: \"write\",\n};\n\nfunction assertScopePart(entry: string, scope: string): void {\n if (scope.length === 0) {\n throw new InvalidScopeEntryError(entry, \"scope part is empty\");\n }\n if (scope.includes(OPERATION_SEPARATOR)) {\n throw new InvalidScopeEntryError(\n entry,\n `scope part must not contain \"${OPERATION_SEPARATOR}\"`,\n );\n }\n}\n\n/**\n * Split one grant scope entry into its operation and scope pattern.\n *\n * - `notes.entries` parses as `{ scope: \"notes.entries\", action: \"read\" }`\n * - `write:notes.*` parses as `{ scope: \"notes.*\", action: \"write\" }`\n *\n * Fails closed: a non-string entry, or an entry whose operation prefix is\n * not recognised (including\n * `read:`, any uppercase or non-ASCII prefix, or a wildcard in the operation\n * position) throws {@link InvalidScopeEntryError} and is never treated as a\n * read entry. An empty scope part (`write:`) throws as well.\n *\n * @param entry - A single element of a grant's `scopes` array.\n * @returns The operation and the scope pattern it applies to.\n * @throws InvalidScopeEntryError when the entry does not fit the grammar.\n */\nexport function parseScopeEntry(entry: string): ParsedScopeEntry {\n // Grant bodies arrive from the network; a non-string element is a grammar\n // violation like any other, not a TypeError from indexOf.\n const raw: unknown = entry;\n if (typeof raw !== \"string\") {\n throw new InvalidScopeEntryError(raw, \"entry must be a string\");\n }\n const separatorIndex = entry.indexOf(OPERATION_SEPARATOR);\n if (separatorIndex === -1) {\n assertScopePart(entry, entry);\n return { scope: entry, action: \"read\" };\n }\n\n const prefix = entry.slice(0, separatorIndex);\n const scope = entry.slice(separatorIndex + 1);\n const action = Object.hasOwn(OPERATION_BY_PREFIX, prefix)\n ? OPERATION_BY_PREFIX[prefix]\n : undefined;\n if (action === undefined) {\n throw new InvalidScopeEntryError(\n entry,\n `unknown operation \"${prefix}\" (known: ${Object.keys(OPERATION_BY_PREFIX).join(\", \")}; read has no prefix)`,\n );\n }\n assertScopePart(entry, scope);\n return { scope, action };\n}\n\n/**\n * Inverse of {@link parseScopeEntry}: render one operation over one scope\n * pattern as a grant scope entry. Read has no prefix.\n *\n * @param parsed - The operation and scope pattern to encode.\n * @returns The wire-form entry, e.g. `write:notes.entries` or `notes.entries`.\n * @throws InvalidScopeEntryError when the action is unknown or the scope part\n * is empty or contains `:`.\n */\nexport function formatScopeEntry(parsed: ParsedScopeEntry): string {\n const { scope, action } = parsed;\n assertScopePart(scope, scope);\n if (action === \"read\") return scope;\n // Looked up rather than hard-coded so an action can never be emitted\n // without a prefix the parser accepts (and JS callers passing an unknown\n // action fail closed instead of producing a read entry).\n const prefix = Object.entries(OPERATION_BY_PREFIX).find(\n ([, candidate]) => candidate === action,\n )?.[0];\n if (prefix === undefined) {\n throw new InvalidScopeEntryError(\n scope,\n `unknown action ${describeValue(action)} (known: ${SCOPE_ACTIONS.join(\", \")})`,\n );\n }\n return `${prefix}${OPERATION_SEPARATOR}${scope}`;\n}\n\nfunction compareScopes(a: string, b: string): number {\n // Plain code-unit order: locale-independent, so the grouping is identical\n // on every runtime.\n if (a < b) return -1;\n if (a > b) return 1;\n return 0;\n}\n\nfunction sortActions(actions: Iterable<ScopeAction>): ScopeAction[] {\n const present = new Set(actions);\n return SCOPE_ACTIONS.filter((action) => present.has(action));\n}\n\n/**\n * Group a grant's scope entries into one `{ scope, actions }` row per scope\n * pattern - the view builders and consent UIs should render instead of the\n * raw strings.\n *\n * The result is canonical: rows are ordered by scope (code-unit order),\n * actions within a row follow {@link SCOPE_ACTIONS} order, and neither rows\n * nor actions repeat, whatever order or duplication the input had.\n *\n * Fails closed: if any entry does not fit the grammar this throws\n * {@link InvalidScopeEntryError} rather than silently dropping it, so a grant\n * carrying an operation this SDK does not know is never shown as narrower\n * than it is.\n *\n * @param scopes - A grant's `scopes` array, verbatim.\n * @returns The grouped, canonically ordered permissions.\n * @throws InvalidScopeEntryError when any entry does not fit the grammar.\n */\nexport function grantPermissions(scopes: readonly string[]): GrantPermission[] {\n const byScope = new Map<string, Set<ScopeAction>>();\n for (const entry of scopes) {\n const { scope, action } = parseScopeEntry(entry);\n let actions = byScope.get(scope);\n if (actions === undefined) {\n actions = new Set<ScopeAction>();\n byScope.set(scope, actions);\n }\n actions.add(action);\n }\n return [...byScope.keys()].sort(compareScopes).map((scope) => ({\n scope,\n actions: sortActions(byScope.get(scope) ?? []),\n }));\n}\n\n/**\n * Inverse of {@link grantPermissions}: flatten grouped permissions back into\n * the `string[]` form a grant is signed with.\n *\n * Output is canonical (scopes in code-unit order, read before write, no\n * duplicates), so `permissionsToScopes(grantPermissions(scopes))` is the\n * canonical form of `scopes`, and `grantPermissions(permissionsToScopes(p))`\n * is the canonical form of `p`. Rows with no actions contribute nothing; a\n * row with an action the grammar does not define throws rather than being\n * dropped.\n *\n * @param permissions - Grouped permissions, in any order, possibly repeating\n * a scope.\n * @returns The scope entries, one per (scope, action) pair.\n * @throws InvalidScopeEntryError when a scope or action does not fit the\n * grammar.\n */\nexport function permissionsToScopes(\n permissions: readonly GrantPermission[],\n): string[] {\n const byScope = new Map<string, Set<ScopeAction>>();\n for (const { scope, actions } of permissions) {\n let merged = byScope.get(scope);\n if (merged === undefined) {\n merged = new Set<ScopeAction>();\n byScope.set(scope, merged);\n }\n for (const action of actions) {\n if (!(SCOPE_ACTIONS as readonly string[]).includes(action)) {\n throw new InvalidScopeEntryError(\n scope,\n `unknown action ${describeValue(action)} (known: ${SCOPE_ACTIONS.join(\", \")})`,\n );\n }\n merged.add(action);\n }\n }\n const entries: string[] = [];\n for (const scope of [...byScope.keys()].sort(compareScopes)) {\n for (const action of sortActions(byScope.get(scope) ?? [])) {\n entries.push(formatScopeEntry({ scope, action }));\n }\n }\n return entries;\n}\n\n/**\n * Does this grant authorize `action` over `scope`?\n *\n * The scope part is matched with the SDK's scope wildcard matcher\n * ({@link scopeMatchesPattern}: `*`, `{prefix}.*`, or exact), the action\n * exactly. Entries that do not fit the grammar are skipped - they authorize\n * nothing, which is exactly how the Personal Server treats them - so a grant\n * that carries an operation this SDK does not know still answers correctly\n * for the operations it does.\n *\n * @param scopes - A grant's `scopes` array, verbatim.\n * @param scope - The concrete scope being requested. Never prefixed: a value\n * containing `:` is not a scope id and yields `false`.\n * @param action - The operation being requested.\n * @returns `true` if some entry grants `action` over a pattern covering\n * `scope`.\n */\nexport function hasAction(\n scopes: readonly string[],\n scope: string,\n action: ScopeAction,\n): boolean {\n // A requested scope is a concrete scope id and never carries a prefix; the\n // Personal Server rejects anything else with ScopeSchema before it ever\n // reaches its matcher, so answer the same way here instead of letting\n // `write:x` fall through to a `*` entry.\n if (scope.includes(OPERATION_SEPARATOR)) return false;\n for (const entry of scopes) {\n let parsed: ParsedScopeEntry;\n try {\n parsed = parseScopeEntry(entry);\n } catch (error) {\n if (error instanceof InvalidScopeEntryError) continue;\n throw error;\n }\n if (parsed.action === action && scopeMatchesPattern(scope, parsed.scope)) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * {@link grantPermissions} for a grant record read back from the gateway:\n * returns `undefined` instead of throwing when the scope list carries an\n * entry this SDK version cannot interpret, so a grant with a newer operation\n * still loads (with `scopes` intact) rather than failing the whole read.\n *\n * @param scopes - A grant's `scopes` array, verbatim.\n * @returns The grouped permissions, or `undefined` if any entry is\n * uninterpretable.\n */\nexport function tryGrantPermissions(\n scopes: readonly string[],\n): GrantPermission[] | undefined {\n try {\n return grantPermissions(scopes);\n } catch (error) {\n if (error instanceof InvalidScopeEntryError) return undefined;\n throw error;\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grant scope-entry grammar.
|
|
3
|
+
*
|
|
4
|
+
* A signed grant carries `scopes: string[]`. Each entry is
|
|
5
|
+
* `[operation:]scope` - an optional lowercase ASCII operation prefix before
|
|
6
|
+
* the first `:`, then a scope pattern (`*`, `{prefix}.*`, or an exact scope).
|
|
7
|
+
* A missing prefix means read. `write:notes.entries` authorizes writing
|
|
8
|
+
* `notes.entries` and nothing else; `notes.entries` authorizes reading it.
|
|
9
|
+
*
|
|
10
|
+
* The string form is the wire and storage detail: it is what the grantor
|
|
11
|
+
* signs (EIP-712 `GrantRegistration.scopes`) and what the gateway stores
|
|
12
|
+
* verbatim. The Personal Server is the sole interpreter, and this module is
|
|
13
|
+
* the SDK-side mirror of that interpretation. Builders and consent UIs should
|
|
14
|
+
* work with the grouped `{ scope, actions }` view (see
|
|
15
|
+
* {@link grantPermissions}) and never construct or parse the strings by hand.
|
|
16
|
+
*
|
|
17
|
+
* Matching rules, pinned by the Personal Server policy
|
|
18
|
+
* (personal-server-ts `packages/core/src/policy/data-write.ts` and
|
|
19
|
+
* `data-read.ts`):
|
|
20
|
+
* - the operation is compared exactly, case-sensitively;
|
|
21
|
+
* - wildcards apply to the scope part only, via {@link scopeMatchesPattern};
|
|
22
|
+
* - an entry whose operation is not recognised never authorizes anything.
|
|
23
|
+
* The parser fails closed on it (throws) rather than treating it as read;
|
|
24
|
+
* the matcher ({@link hasAction}) skips it, which is how the Personal
|
|
25
|
+
* Server treats an entry it does not understand.
|
|
26
|
+
*/
|
|
27
|
+
/** Operations the grammar defines today, in canonical (output) order. */
|
|
28
|
+
export declare const SCOPE_ACTIONS: readonly ["read", "write"];
|
|
29
|
+
/** An operation a grant entry can authorize over a scope. */
|
|
30
|
+
export type ScopeAction = (typeof SCOPE_ACTIONS)[number];
|
|
31
|
+
/** One grant entry, split into its operation and scope pattern. */
|
|
32
|
+
export interface ParsedScopeEntry {
|
|
33
|
+
scope: string;
|
|
34
|
+
action: ScopeAction;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The grouped view of a grant's scope entries: one row per scope pattern
|
|
38
|
+
* with every operation the grant authorizes over it.
|
|
39
|
+
*/
|
|
40
|
+
export interface GrantPermission {
|
|
41
|
+
scope: string;
|
|
42
|
+
actions: ScopeAction[];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Thrown when a scope entry does not fit the grammar - an unknown or
|
|
46
|
+
* malformed operation prefix, or an empty scope part.
|
|
47
|
+
*/
|
|
48
|
+
export declare class InvalidScopeEntryError extends Error {
|
|
49
|
+
/** The offending entry, verbatim (unknown because it may not be a string). */
|
|
50
|
+
readonly entry: unknown;
|
|
51
|
+
constructor(entry: unknown, reason: string);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Split one grant scope entry into its operation and scope pattern.
|
|
55
|
+
*
|
|
56
|
+
* - `notes.entries` parses as `{ scope: "notes.entries", action: "read" }`
|
|
57
|
+
* - `write:notes.*` parses as `{ scope: "notes.*", action: "write" }`
|
|
58
|
+
*
|
|
59
|
+
* Fails closed: a non-string entry, or an entry whose operation prefix is
|
|
60
|
+
* not recognised (including
|
|
61
|
+
* `read:`, any uppercase or non-ASCII prefix, or a wildcard in the operation
|
|
62
|
+
* position) throws {@link InvalidScopeEntryError} and is never treated as a
|
|
63
|
+
* read entry. An empty scope part (`write:`) throws as well.
|
|
64
|
+
*
|
|
65
|
+
* @param entry - A single element of a grant's `scopes` array.
|
|
66
|
+
* @returns The operation and the scope pattern it applies to.
|
|
67
|
+
* @throws InvalidScopeEntryError when the entry does not fit the grammar.
|
|
68
|
+
*/
|
|
69
|
+
export declare function parseScopeEntry(entry: string): ParsedScopeEntry;
|
|
70
|
+
/**
|
|
71
|
+
* Inverse of {@link parseScopeEntry}: render one operation over one scope
|
|
72
|
+
* pattern as a grant scope entry. Read has no prefix.
|
|
73
|
+
*
|
|
74
|
+
* @param parsed - The operation and scope pattern to encode.
|
|
75
|
+
* @returns The wire-form entry, e.g. `write:notes.entries` or `notes.entries`.
|
|
76
|
+
* @throws InvalidScopeEntryError when the action is unknown or the scope part
|
|
77
|
+
* is empty or contains `:`.
|
|
78
|
+
*/
|
|
79
|
+
export declare function formatScopeEntry(parsed: ParsedScopeEntry): string;
|
|
80
|
+
/**
|
|
81
|
+
* Group a grant's scope entries into one `{ scope, actions }` row per scope
|
|
82
|
+
* pattern - the view builders and consent UIs should render instead of the
|
|
83
|
+
* raw strings.
|
|
84
|
+
*
|
|
85
|
+
* The result is canonical: rows are ordered by scope (code-unit order),
|
|
86
|
+
* actions within a row follow {@link SCOPE_ACTIONS} order, and neither rows
|
|
87
|
+
* nor actions repeat, whatever order or duplication the input had.
|
|
88
|
+
*
|
|
89
|
+
* Fails closed: if any entry does not fit the grammar this throws
|
|
90
|
+
* {@link InvalidScopeEntryError} rather than silently dropping it, so a grant
|
|
91
|
+
* carrying an operation this SDK does not know is never shown as narrower
|
|
92
|
+
* than it is.
|
|
93
|
+
*
|
|
94
|
+
* @param scopes - A grant's `scopes` array, verbatim.
|
|
95
|
+
* @returns The grouped, canonically ordered permissions.
|
|
96
|
+
* @throws InvalidScopeEntryError when any entry does not fit the grammar.
|
|
97
|
+
*/
|
|
98
|
+
export declare function grantPermissions(scopes: readonly string[]): GrantPermission[];
|
|
99
|
+
/**
|
|
100
|
+
* Inverse of {@link grantPermissions}: flatten grouped permissions back into
|
|
101
|
+
* the `string[]` form a grant is signed with.
|
|
102
|
+
*
|
|
103
|
+
* Output is canonical (scopes in code-unit order, read before write, no
|
|
104
|
+
* duplicates), so `permissionsToScopes(grantPermissions(scopes))` is the
|
|
105
|
+
* canonical form of `scopes`, and `grantPermissions(permissionsToScopes(p))`
|
|
106
|
+
* is the canonical form of `p`. Rows with no actions contribute nothing; a
|
|
107
|
+
* row with an action the grammar does not define throws rather than being
|
|
108
|
+
* dropped.
|
|
109
|
+
*
|
|
110
|
+
* @param permissions - Grouped permissions, in any order, possibly repeating
|
|
111
|
+
* a scope.
|
|
112
|
+
* @returns The scope entries, one per (scope, action) pair.
|
|
113
|
+
* @throws InvalidScopeEntryError when a scope or action does not fit the
|
|
114
|
+
* grammar.
|
|
115
|
+
*/
|
|
116
|
+
export declare function permissionsToScopes(permissions: readonly GrantPermission[]): string[];
|
|
117
|
+
/**
|
|
118
|
+
* Does this grant authorize `action` over `scope`?
|
|
119
|
+
*
|
|
120
|
+
* The scope part is matched with the SDK's scope wildcard matcher
|
|
121
|
+
* ({@link scopeMatchesPattern}: `*`, `{prefix}.*`, or exact), the action
|
|
122
|
+
* exactly. Entries that do not fit the grammar are skipped - they authorize
|
|
123
|
+
* nothing, which is exactly how the Personal Server treats them - so a grant
|
|
124
|
+
* that carries an operation this SDK does not know still answers correctly
|
|
125
|
+
* for the operations it does.
|
|
126
|
+
*
|
|
127
|
+
* @param scopes - A grant's `scopes` array, verbatim.
|
|
128
|
+
* @param scope - The concrete scope being requested. Never prefixed: a value
|
|
129
|
+
* containing `:` is not a scope id and yields `false`.
|
|
130
|
+
* @param action - The operation being requested.
|
|
131
|
+
* @returns `true` if some entry grants `action` over a pattern covering
|
|
132
|
+
* `scope`.
|
|
133
|
+
*/
|
|
134
|
+
export declare function hasAction(scopes: readonly string[], scope: string, action: ScopeAction): boolean;
|
|
135
|
+
/**
|
|
136
|
+
* {@link grantPermissions} for a grant record read back from the gateway:
|
|
137
|
+
* returns `undefined` instead of throwing when the scope list carries an
|
|
138
|
+
* entry this SDK version cannot interpret, so a grant with a newer operation
|
|
139
|
+
* still loads (with `scopes` intact) rather than failing the whole read.
|
|
140
|
+
*
|
|
141
|
+
* @param scopes - A grant's `scopes` array, verbatim.
|
|
142
|
+
* @returns The grouped permissions, or `undefined` if any entry is
|
|
143
|
+
* uninterpretable.
|
|
144
|
+
*/
|
|
145
|
+
export declare function tryGrantPermissions(scopes: readonly string[]): GrantPermission[] | undefined;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { scopeMatchesPattern } from "./scopes.js";
|
|
2
|
+
const SCOPE_ACTIONS = ["read", "write"];
|
|
3
|
+
class InvalidScopeEntryError extends Error {
|
|
4
|
+
/** The offending entry, verbatim (unknown because it may not be a string). */
|
|
5
|
+
entry;
|
|
6
|
+
constructor(entry, reason) {
|
|
7
|
+
super(`Invalid scope entry ${describeValue(entry)}: ${reason}`);
|
|
8
|
+
this.name = "InvalidScopeEntryError";
|
|
9
|
+
this.entry = entry;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const OPERATION_SEPARATOR = ":";
|
|
13
|
+
function describeValue(value) {
|
|
14
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
15
|
+
if (value === null) return "null";
|
|
16
|
+
return `[${typeof value}]`;
|
|
17
|
+
}
|
|
18
|
+
const OPERATION_BY_PREFIX = {
|
|
19
|
+
write: "write"
|
|
20
|
+
};
|
|
21
|
+
function assertScopePart(entry, scope) {
|
|
22
|
+
if (scope.length === 0) {
|
|
23
|
+
throw new InvalidScopeEntryError(entry, "scope part is empty");
|
|
24
|
+
}
|
|
25
|
+
if (scope.includes(OPERATION_SEPARATOR)) {
|
|
26
|
+
throw new InvalidScopeEntryError(
|
|
27
|
+
entry,
|
|
28
|
+
`scope part must not contain "${OPERATION_SEPARATOR}"`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function parseScopeEntry(entry) {
|
|
33
|
+
const raw = entry;
|
|
34
|
+
if (typeof raw !== "string") {
|
|
35
|
+
throw new InvalidScopeEntryError(raw, "entry must be a string");
|
|
36
|
+
}
|
|
37
|
+
const separatorIndex = entry.indexOf(OPERATION_SEPARATOR);
|
|
38
|
+
if (separatorIndex === -1) {
|
|
39
|
+
assertScopePart(entry, entry);
|
|
40
|
+
return { scope: entry, action: "read" };
|
|
41
|
+
}
|
|
42
|
+
const prefix = entry.slice(0, separatorIndex);
|
|
43
|
+
const scope = entry.slice(separatorIndex + 1);
|
|
44
|
+
const action = Object.hasOwn(OPERATION_BY_PREFIX, prefix) ? OPERATION_BY_PREFIX[prefix] : void 0;
|
|
45
|
+
if (action === void 0) {
|
|
46
|
+
throw new InvalidScopeEntryError(
|
|
47
|
+
entry,
|
|
48
|
+
`unknown operation "${prefix}" (known: ${Object.keys(OPERATION_BY_PREFIX).join(", ")}; read has no prefix)`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
assertScopePart(entry, scope);
|
|
52
|
+
return { scope, action };
|
|
53
|
+
}
|
|
54
|
+
function formatScopeEntry(parsed) {
|
|
55
|
+
const { scope, action } = parsed;
|
|
56
|
+
assertScopePart(scope, scope);
|
|
57
|
+
if (action === "read") return scope;
|
|
58
|
+
const prefix = Object.entries(OPERATION_BY_PREFIX).find(
|
|
59
|
+
([, candidate]) => candidate === action
|
|
60
|
+
)?.[0];
|
|
61
|
+
if (prefix === void 0) {
|
|
62
|
+
throw new InvalidScopeEntryError(
|
|
63
|
+
scope,
|
|
64
|
+
`unknown action ${describeValue(action)} (known: ${SCOPE_ACTIONS.join(", ")})`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
return `${prefix}${OPERATION_SEPARATOR}${scope}`;
|
|
68
|
+
}
|
|
69
|
+
function compareScopes(a, b) {
|
|
70
|
+
if (a < b) return -1;
|
|
71
|
+
if (a > b) return 1;
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
function sortActions(actions) {
|
|
75
|
+
const present = new Set(actions);
|
|
76
|
+
return SCOPE_ACTIONS.filter((action) => present.has(action));
|
|
77
|
+
}
|
|
78
|
+
function grantPermissions(scopes) {
|
|
79
|
+
const byScope = /* @__PURE__ */ new Map();
|
|
80
|
+
for (const entry of scopes) {
|
|
81
|
+
const { scope, action } = parseScopeEntry(entry);
|
|
82
|
+
let actions = byScope.get(scope);
|
|
83
|
+
if (actions === void 0) {
|
|
84
|
+
actions = /* @__PURE__ */ new Set();
|
|
85
|
+
byScope.set(scope, actions);
|
|
86
|
+
}
|
|
87
|
+
actions.add(action);
|
|
88
|
+
}
|
|
89
|
+
return [...byScope.keys()].sort(compareScopes).map((scope) => ({
|
|
90
|
+
scope,
|
|
91
|
+
actions: sortActions(byScope.get(scope) ?? [])
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
function permissionsToScopes(permissions) {
|
|
95
|
+
const byScope = /* @__PURE__ */ new Map();
|
|
96
|
+
for (const { scope, actions } of permissions) {
|
|
97
|
+
let merged = byScope.get(scope);
|
|
98
|
+
if (merged === void 0) {
|
|
99
|
+
merged = /* @__PURE__ */ new Set();
|
|
100
|
+
byScope.set(scope, merged);
|
|
101
|
+
}
|
|
102
|
+
for (const action of actions) {
|
|
103
|
+
if (!SCOPE_ACTIONS.includes(action)) {
|
|
104
|
+
throw new InvalidScopeEntryError(
|
|
105
|
+
scope,
|
|
106
|
+
`unknown action ${describeValue(action)} (known: ${SCOPE_ACTIONS.join(", ")})`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
merged.add(action);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const entries = [];
|
|
113
|
+
for (const scope of [...byScope.keys()].sort(compareScopes)) {
|
|
114
|
+
for (const action of sortActions(byScope.get(scope) ?? [])) {
|
|
115
|
+
entries.push(formatScopeEntry({ scope, action }));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return entries;
|
|
119
|
+
}
|
|
120
|
+
function hasAction(scopes, scope, action) {
|
|
121
|
+
if (scope.includes(OPERATION_SEPARATOR)) return false;
|
|
122
|
+
for (const entry of scopes) {
|
|
123
|
+
let parsed;
|
|
124
|
+
try {
|
|
125
|
+
parsed = parseScopeEntry(entry);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (error instanceof InvalidScopeEntryError) continue;
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
if (parsed.action === action && scopeMatchesPattern(scope, parsed.scope)) {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
function tryGrantPermissions(scopes) {
|
|
137
|
+
try {
|
|
138
|
+
return grantPermissions(scopes);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
if (error instanceof InvalidScopeEntryError) return void 0;
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
export {
|
|
145
|
+
InvalidScopeEntryError,
|
|
146
|
+
SCOPE_ACTIONS,
|
|
147
|
+
formatScopeEntry,
|
|
148
|
+
grantPermissions,
|
|
149
|
+
hasAction,
|
|
150
|
+
parseScopeEntry,
|
|
151
|
+
permissionsToScopes,
|
|
152
|
+
tryGrantPermissions
|
|
153
|
+
};
|
|
154
|
+
//# sourceMappingURL=scope-actions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/protocol/scope-actions.ts"],"sourcesContent":["import { scopeMatchesPattern } from \"./scopes\";\n\n/**\n * Grant scope-entry grammar.\n *\n * A signed grant carries `scopes: string[]`. Each entry is\n * `[operation:]scope` - an optional lowercase ASCII operation prefix before\n * the first `:`, then a scope pattern (`*`, `{prefix}.*`, or an exact scope).\n * A missing prefix means read. `write:notes.entries` authorizes writing\n * `notes.entries` and nothing else; `notes.entries` authorizes reading it.\n *\n * The string form is the wire and storage detail: it is what the grantor\n * signs (EIP-712 `GrantRegistration.scopes`) and what the gateway stores\n * verbatim. The Personal Server is the sole interpreter, and this module is\n * the SDK-side mirror of that interpretation. Builders and consent UIs should\n * work with the grouped `{ scope, actions }` view (see\n * {@link grantPermissions}) and never construct or parse the strings by hand.\n *\n * Matching rules, pinned by the Personal Server policy\n * (personal-server-ts `packages/core/src/policy/data-write.ts` and\n * `data-read.ts`):\n * - the operation is compared exactly, case-sensitively;\n * - wildcards apply to the scope part only, via {@link scopeMatchesPattern};\n * - an entry whose operation is not recognised never authorizes anything.\n * The parser fails closed on it (throws) rather than treating it as read;\n * the matcher ({@link hasAction}) skips it, which is how the Personal\n * Server treats an entry it does not understand.\n */\n\n/** Operations the grammar defines today, in canonical (output) order. */\nexport const SCOPE_ACTIONS = [\"read\", \"write\"] as const;\n\n/** An operation a grant entry can authorize over a scope. */\nexport type ScopeAction = (typeof SCOPE_ACTIONS)[number];\n\n/** One grant entry, split into its operation and scope pattern. */\nexport interface ParsedScopeEntry {\n scope: string;\n action: ScopeAction;\n}\n\n/**\n * The grouped view of a grant's scope entries: one row per scope pattern\n * with every operation the grant authorizes over it.\n */\nexport interface GrantPermission {\n scope: string;\n actions: ScopeAction[];\n}\n\n/**\n * Thrown when a scope entry does not fit the grammar - an unknown or\n * malformed operation prefix, or an empty scope part.\n */\nexport class InvalidScopeEntryError extends Error {\n /** The offending entry, verbatim (unknown because it may not be a string). */\n readonly entry: unknown;\n\n constructor(entry: unknown, reason: string) {\n super(`Invalid scope entry ${describeValue(entry)}: ${reason}`);\n this.name = \"InvalidScopeEntryError\";\n this.entry = entry;\n }\n}\n\nconst OPERATION_SEPARATOR = \":\";\n\n// Render an untrusted value for an error message without ever throwing:\n// String() and JSON.stringify() both defer to the value's own toString /\n// toJSON, which a hostile JSON body can make throw.\nfunction describeValue(value: unknown): string {\n if (typeof value === \"string\") return JSON.stringify(value);\n if (value === null) return \"null\";\n return `[${typeof value}]`;\n}\n\n// The only operation that is ever written out. Read has no prefix, and\n// `read:` is NOT an alias for it: the Personal Server's read policy matches\n// entries verbatim, so a `read:x` entry would authorize nothing there, and\n// the parser must reject it for the same reason.\nconst OPERATION_BY_PREFIX: Readonly<Record<string, ScopeAction>> = {\n write: \"write\",\n};\n\nfunction assertScopePart(entry: string, scope: string): void {\n if (scope.length === 0) {\n throw new InvalidScopeEntryError(entry, \"scope part is empty\");\n }\n if (scope.includes(OPERATION_SEPARATOR)) {\n throw new InvalidScopeEntryError(\n entry,\n `scope part must not contain \"${OPERATION_SEPARATOR}\"`,\n );\n }\n}\n\n/**\n * Split one grant scope entry into its operation and scope pattern.\n *\n * - `notes.entries` parses as `{ scope: \"notes.entries\", action: \"read\" }`\n * - `write:notes.*` parses as `{ scope: \"notes.*\", action: \"write\" }`\n *\n * Fails closed: a non-string entry, or an entry whose operation prefix is\n * not recognised (including\n * `read:`, any uppercase or non-ASCII prefix, or a wildcard in the operation\n * position) throws {@link InvalidScopeEntryError} and is never treated as a\n * read entry. An empty scope part (`write:`) throws as well.\n *\n * @param entry - A single element of a grant's `scopes` array.\n * @returns The operation and the scope pattern it applies to.\n * @throws InvalidScopeEntryError when the entry does not fit the grammar.\n */\nexport function parseScopeEntry(entry: string): ParsedScopeEntry {\n // Grant bodies arrive from the network; a non-string element is a grammar\n // violation like any other, not a TypeError from indexOf.\n const raw: unknown = entry;\n if (typeof raw !== \"string\") {\n throw new InvalidScopeEntryError(raw, \"entry must be a string\");\n }\n const separatorIndex = entry.indexOf(OPERATION_SEPARATOR);\n if (separatorIndex === -1) {\n assertScopePart(entry, entry);\n return { scope: entry, action: \"read\" };\n }\n\n const prefix = entry.slice(0, separatorIndex);\n const scope = entry.slice(separatorIndex + 1);\n const action = Object.hasOwn(OPERATION_BY_PREFIX, prefix)\n ? OPERATION_BY_PREFIX[prefix]\n : undefined;\n if (action === undefined) {\n throw new InvalidScopeEntryError(\n entry,\n `unknown operation \"${prefix}\" (known: ${Object.keys(OPERATION_BY_PREFIX).join(\", \")}; read has no prefix)`,\n );\n }\n assertScopePart(entry, scope);\n return { scope, action };\n}\n\n/**\n * Inverse of {@link parseScopeEntry}: render one operation over one scope\n * pattern as a grant scope entry. Read has no prefix.\n *\n * @param parsed - The operation and scope pattern to encode.\n * @returns The wire-form entry, e.g. `write:notes.entries` or `notes.entries`.\n * @throws InvalidScopeEntryError when the action is unknown or the scope part\n * is empty or contains `:`.\n */\nexport function formatScopeEntry(parsed: ParsedScopeEntry): string {\n const { scope, action } = parsed;\n assertScopePart(scope, scope);\n if (action === \"read\") return scope;\n // Looked up rather than hard-coded so an action can never be emitted\n // without a prefix the parser accepts (and JS callers passing an unknown\n // action fail closed instead of producing a read entry).\n const prefix = Object.entries(OPERATION_BY_PREFIX).find(\n ([, candidate]) => candidate === action,\n )?.[0];\n if (prefix === undefined) {\n throw new InvalidScopeEntryError(\n scope,\n `unknown action ${describeValue(action)} (known: ${SCOPE_ACTIONS.join(\", \")})`,\n );\n }\n return `${prefix}${OPERATION_SEPARATOR}${scope}`;\n}\n\nfunction compareScopes(a: string, b: string): number {\n // Plain code-unit order: locale-independent, so the grouping is identical\n // on every runtime.\n if (a < b) return -1;\n if (a > b) return 1;\n return 0;\n}\n\nfunction sortActions(actions: Iterable<ScopeAction>): ScopeAction[] {\n const present = new Set(actions);\n return SCOPE_ACTIONS.filter((action) => present.has(action));\n}\n\n/**\n * Group a grant's scope entries into one `{ scope, actions }` row per scope\n * pattern - the view builders and consent UIs should render instead of the\n * raw strings.\n *\n * The result is canonical: rows are ordered by scope (code-unit order),\n * actions within a row follow {@link SCOPE_ACTIONS} order, and neither rows\n * nor actions repeat, whatever order or duplication the input had.\n *\n * Fails closed: if any entry does not fit the grammar this throws\n * {@link InvalidScopeEntryError} rather than silently dropping it, so a grant\n * carrying an operation this SDK does not know is never shown as narrower\n * than it is.\n *\n * @param scopes - A grant's `scopes` array, verbatim.\n * @returns The grouped, canonically ordered permissions.\n * @throws InvalidScopeEntryError when any entry does not fit the grammar.\n */\nexport function grantPermissions(scopes: readonly string[]): GrantPermission[] {\n const byScope = new Map<string, Set<ScopeAction>>();\n for (const entry of scopes) {\n const { scope, action } = parseScopeEntry(entry);\n let actions = byScope.get(scope);\n if (actions === undefined) {\n actions = new Set<ScopeAction>();\n byScope.set(scope, actions);\n }\n actions.add(action);\n }\n return [...byScope.keys()].sort(compareScopes).map((scope) => ({\n scope,\n actions: sortActions(byScope.get(scope) ?? []),\n }));\n}\n\n/**\n * Inverse of {@link grantPermissions}: flatten grouped permissions back into\n * the `string[]` form a grant is signed with.\n *\n * Output is canonical (scopes in code-unit order, read before write, no\n * duplicates), so `permissionsToScopes(grantPermissions(scopes))` is the\n * canonical form of `scopes`, and `grantPermissions(permissionsToScopes(p))`\n * is the canonical form of `p`. Rows with no actions contribute nothing; a\n * row with an action the grammar does not define throws rather than being\n * dropped.\n *\n * @param permissions - Grouped permissions, in any order, possibly repeating\n * a scope.\n * @returns The scope entries, one per (scope, action) pair.\n * @throws InvalidScopeEntryError when a scope or action does not fit the\n * grammar.\n */\nexport function permissionsToScopes(\n permissions: readonly GrantPermission[],\n): string[] {\n const byScope = new Map<string, Set<ScopeAction>>();\n for (const { scope, actions } of permissions) {\n let merged = byScope.get(scope);\n if (merged === undefined) {\n merged = new Set<ScopeAction>();\n byScope.set(scope, merged);\n }\n for (const action of actions) {\n if (!(SCOPE_ACTIONS as readonly string[]).includes(action)) {\n throw new InvalidScopeEntryError(\n scope,\n `unknown action ${describeValue(action)} (known: ${SCOPE_ACTIONS.join(\", \")})`,\n );\n }\n merged.add(action);\n }\n }\n const entries: string[] = [];\n for (const scope of [...byScope.keys()].sort(compareScopes)) {\n for (const action of sortActions(byScope.get(scope) ?? [])) {\n entries.push(formatScopeEntry({ scope, action }));\n }\n }\n return entries;\n}\n\n/**\n * Does this grant authorize `action` over `scope`?\n *\n * The scope part is matched with the SDK's scope wildcard matcher\n * ({@link scopeMatchesPattern}: `*`, `{prefix}.*`, or exact), the action\n * exactly. Entries that do not fit the grammar are skipped - they authorize\n * nothing, which is exactly how the Personal Server treats them - so a grant\n * that carries an operation this SDK does not know still answers correctly\n * for the operations it does.\n *\n * @param scopes - A grant's `scopes` array, verbatim.\n * @param scope - The concrete scope being requested. Never prefixed: a value\n * containing `:` is not a scope id and yields `false`.\n * @param action - The operation being requested.\n * @returns `true` if some entry grants `action` over a pattern covering\n * `scope`.\n */\nexport function hasAction(\n scopes: readonly string[],\n scope: string,\n action: ScopeAction,\n): boolean {\n // A requested scope is a concrete scope id and never carries a prefix; the\n // Personal Server rejects anything else with ScopeSchema before it ever\n // reaches its matcher, so answer the same way here instead of letting\n // `write:x` fall through to a `*` entry.\n if (scope.includes(OPERATION_SEPARATOR)) return false;\n for (const entry of scopes) {\n let parsed: ParsedScopeEntry;\n try {\n parsed = parseScopeEntry(entry);\n } catch (error) {\n if (error instanceof InvalidScopeEntryError) continue;\n throw error;\n }\n if (parsed.action === action && scopeMatchesPattern(scope, parsed.scope)) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * {@link grantPermissions} for a grant record read back from the gateway:\n * returns `undefined` instead of throwing when the scope list carries an\n * entry this SDK version cannot interpret, so a grant with a newer operation\n * still loads (with `scopes` intact) rather than failing the whole read.\n *\n * @param scopes - A grant's `scopes` array, verbatim.\n * @returns The grouped permissions, or `undefined` if any entry is\n * uninterpretable.\n */\nexport function tryGrantPermissions(\n scopes: readonly string[],\n): GrantPermission[] | undefined {\n try {\n return grantPermissions(scopes);\n } catch (error) {\n if (error instanceof InvalidScopeEntryError) return undefined;\n throw error;\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/react.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/react.ts"],"sourcesContent":["/**\n * React entry point for the Vana SDK direct Data Portability flow.\n *\n * @remarks\n * Exposes {@link useDirectVanaConnect} (and the underlying framework-agnostic\n * connect-flow store) for browser apps. This entry point is browser-safe and\n * imports nothing Node-only. `react` is a peer dependency.\n *\n * @example\n * ```tsx\n * \"use client\";\n * import { useDirectVanaConnect } from \"@opendatalabs/vana-sdk/react\";\n *\n * export function ConnectNotesButton() {\n * const connect = useDirectVanaConnect({\n * createRequest: () => fetch(\"/api/vana/request\", { method: \"POST\" }).then((r) => r.json()),\n * getStatus: (id) => fetch(`/api/vana/status?requestId=${id}`).then((r) => r.json()),\n * readResult: (id) => fetch(`/api/vana/data?requestId=${id}`).then((r) => r.json()),\n * });\n * const { state, start } = connect;\n * return (\n * <div>\n * <button disabled={state.type !== \"idle\"} onClick={start} type=\"button\">\n * {state.type === \"idle\" ? \"Connect Apple Notes\" : \"Connecting...\"}\n * </button>\n * {state.type === \"awaiting_approval\" && (\n * // Fallback link: if the browser blocked the approval popup, the user\n * // can still open it manually instead of the flow silently hanging.\n * <a href={state.request.approvalUrl} target=\"_blank\" rel=\"noreferrer\">\n * {state.popupBlocked ? \"Popup blocked — open approval\" : \"Open approval\"}\n * </a>\n * )}\n * </div>\n * );\n * }\n * ```\n *\n * @category Direct\n * @module react\n */\n\nexport {\n useDirectVanaConnect,\n type UseDirectVanaConnectOptions,\n type UseDirectVanaConnectResult,\n} from \"./direct/use-direct-vana-connect\";\n\n// Framework-agnostic store (usable without React).\nexport {\n createDirectConnectFlow,\n type ConnectWindow,\n type DirectConnectFlow,\n type DirectConnectState,\n type DirectConnectOptions,\n type DirectConnectTransports,\n} from \"./direct/connect-flow\";\n\n// Shared types useful when typing the transports.\nexport type {\n AccessRequest,\n AccessRequestStatus,\n AccessRequestStatusValue,\n ApprovedDataResult,\n} from \"./direct/types\";\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;
|
|
1
|
+
{"version":3,"sources":["../src/react.ts"],"sourcesContent":["/**\n * React entry point for the Vana SDK direct Data Portability flow.\n *\n * @remarks\n * Exposes {@link useDirectVanaConnect} (and the underlying framework-agnostic\n * connect-flow store) for browser apps. This entry point is browser-safe and\n * imports nothing Node-only. `react` is a peer dependency.\n *\n * @example\n * ```tsx\n * \"use client\";\n * import { useDirectVanaConnect } from \"@opendatalabs/vana-sdk/react\";\n *\n * export function ConnectNotesButton() {\n * const connect = useDirectVanaConnect({\n * createRequest: () => fetch(\"/api/vana/request\", { method: \"POST\" }).then((r) => r.json()),\n * getStatus: (id) => fetch(`/api/vana/status?requestId=${id}`).then((r) => r.json()),\n * readResult: (id) => fetch(`/api/vana/data?requestId=${id}`).then((r) => r.json()),\n * });\n * const { state, start } = connect;\n * return (\n * <div>\n * <button disabled={state.type !== \"idle\"} onClick={start} type=\"button\">\n * {state.type === \"idle\" ? \"Connect Apple Notes\" : \"Connecting...\"}\n * </button>\n * {state.type === \"awaiting_approval\" && (\n * // Fallback link: if the browser blocked the approval popup, the user\n * // can still open it manually instead of the flow silently hanging.\n * <a href={state.request.approvalUrl} target=\"_blank\" rel=\"noreferrer\">\n * {state.popupBlocked ? \"Popup blocked — open approval\" : \"Open approval\"}\n * </a>\n * )}\n * {state.type === \"ready_to_open\" && (\n * // Mobile deep Direct: render the HTTPS continuation as a primary tap.\n * // The SDK keeps polling in this tab; if the tab is lost, restart.\n * <a href={state.mobileContinuationUrl} target=\"_blank\" rel=\"noreferrer\">\n * Open Vana\n * </a>\n * )}\n * </div>\n * );\n * }\n * ```\n *\n * @category Direct\n * @module react\n */\n\nexport {\n useDirectVanaConnect,\n type UseDirectVanaConnectOptions,\n type UseDirectVanaConnectResult,\n} from \"./direct/use-direct-vana-connect\";\n\n// Framework-agnostic store (usable without React).\nexport {\n createDirectConnectFlow,\n type ConnectWindow,\n type DirectBrowserPlatform,\n type DirectBrowserPlatformPolicy,\n type DirectConnectFlow,\n type DirectConnectState,\n type DirectConnectOptions,\n type DirectConnectTransports,\n} from \"./direct/connect-flow\";\n\n// Shared types useful when typing the transports.\nexport type {\n ForegroundDelivery,\n AccessRequest,\n AccessRequestStatus,\n AccessRequestStatusValue,\n ApprovedDataResult,\n} from \"./direct/types\";\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgDA,qCAIO;AAGP,0BASO;","names":[]}
|
package/dist/react.d.ts
CHANGED
|
@@ -30,6 +30,13 @@
|
|
|
30
30
|
* {state.popupBlocked ? "Popup blocked — open approval" : "Open approval"}
|
|
31
31
|
* </a>
|
|
32
32
|
* )}
|
|
33
|
+
* {state.type === "ready_to_open" && (
|
|
34
|
+
* // Mobile deep Direct: render the HTTPS continuation as a primary tap.
|
|
35
|
+
* // The SDK keeps polling in this tab; if the tab is lost, restart.
|
|
36
|
+
* <a href={state.mobileContinuationUrl} target="_blank" rel="noreferrer">
|
|
37
|
+
* Open Vana
|
|
38
|
+
* </a>
|
|
39
|
+
* )}
|
|
33
40
|
* </div>
|
|
34
41
|
* );
|
|
35
42
|
* }
|
|
@@ -39,5 +46,5 @@
|
|
|
39
46
|
* @module react
|
|
40
47
|
*/
|
|
41
48
|
export { useDirectVanaConnect, type UseDirectVanaConnectOptions, type UseDirectVanaConnectResult, } from "./direct/use-direct-vana-connect.js";
|
|
42
|
-
export { createDirectConnectFlow, type ConnectWindow, type DirectConnectFlow, type DirectConnectState, type DirectConnectOptions, type DirectConnectTransports, } from "./direct/connect-flow.js";
|
|
43
|
-
export type { AccessRequest, AccessRequestStatus, AccessRequestStatusValue, ApprovedDataResult, } from "./direct/types.js";
|
|
49
|
+
export { createDirectConnectFlow, type ConnectWindow, type DirectBrowserPlatform, type DirectBrowserPlatformPolicy, type DirectConnectFlow, type DirectConnectState, type DirectConnectOptions, type DirectConnectTransports, } from "./direct/connect-flow.js";
|
|
50
|
+
export type { ForegroundDelivery, AccessRequest, AccessRequestStatus, AccessRequestStatusValue, ApprovedDataResult, } from "./direct/types.js";
|
package/dist/react.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/react.ts"],"sourcesContent":["/**\n * React entry point for the Vana SDK direct Data Portability flow.\n *\n * @remarks\n * Exposes {@link useDirectVanaConnect} (and the underlying framework-agnostic\n * connect-flow store) for browser apps. This entry point is browser-safe and\n * imports nothing Node-only. `react` is a peer dependency.\n *\n * @example\n * ```tsx\n * \"use client\";\n * import { useDirectVanaConnect } from \"@opendatalabs/vana-sdk/react\";\n *\n * export function ConnectNotesButton() {\n * const connect = useDirectVanaConnect({\n * createRequest: () => fetch(\"/api/vana/request\", { method: \"POST\" }).then((r) => r.json()),\n * getStatus: (id) => fetch(`/api/vana/status?requestId=${id}`).then((r) => r.json()),\n * readResult: (id) => fetch(`/api/vana/data?requestId=${id}`).then((r) => r.json()),\n * });\n * const { state, start } = connect;\n * return (\n * <div>\n * <button disabled={state.type !== \"idle\"} onClick={start} type=\"button\">\n * {state.type === \"idle\" ? \"Connect Apple Notes\" : \"Connecting...\"}\n * </button>\n * {state.type === \"awaiting_approval\" && (\n * // Fallback link: if the browser blocked the approval popup, the user\n * // can still open it manually instead of the flow silently hanging.\n * <a href={state.request.approvalUrl} target=\"_blank\" rel=\"noreferrer\">\n * {state.popupBlocked ? \"Popup blocked — open approval\" : \"Open approval\"}\n * </a>\n * )}\n * </div>\n * );\n * }\n * ```\n *\n * @category Direct\n * @module react\n */\n\nexport {\n useDirectVanaConnect,\n type UseDirectVanaConnectOptions,\n type UseDirectVanaConnectResult,\n} from \"./direct/use-direct-vana-connect\";\n\n// Framework-agnostic store (usable without React).\nexport {\n createDirectConnectFlow,\n type ConnectWindow,\n type DirectConnectFlow,\n type DirectConnectState,\n type DirectConnectOptions,\n type DirectConnectTransports,\n} from \"./direct/connect-flow\";\n\n// Shared types useful when typing the transports.\nexport type {\n AccessRequest,\n AccessRequestStatus,\n AccessRequestStatusValue,\n ApprovedDataResult,\n} from \"./direct/types\";\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../src/react.ts"],"sourcesContent":["/**\n * React entry point for the Vana SDK direct Data Portability flow.\n *\n * @remarks\n * Exposes {@link useDirectVanaConnect} (and the underlying framework-agnostic\n * connect-flow store) for browser apps. This entry point is browser-safe and\n * imports nothing Node-only. `react` is a peer dependency.\n *\n * @example\n * ```tsx\n * \"use client\";\n * import { useDirectVanaConnect } from \"@opendatalabs/vana-sdk/react\";\n *\n * export function ConnectNotesButton() {\n * const connect = useDirectVanaConnect({\n * createRequest: () => fetch(\"/api/vana/request\", { method: \"POST\" }).then((r) => r.json()),\n * getStatus: (id) => fetch(`/api/vana/status?requestId=${id}`).then((r) => r.json()),\n * readResult: (id) => fetch(`/api/vana/data?requestId=${id}`).then((r) => r.json()),\n * });\n * const { state, start } = connect;\n * return (\n * <div>\n * <button disabled={state.type !== \"idle\"} onClick={start} type=\"button\">\n * {state.type === \"idle\" ? \"Connect Apple Notes\" : \"Connecting...\"}\n * </button>\n * {state.type === \"awaiting_approval\" && (\n * // Fallback link: if the browser blocked the approval popup, the user\n * // can still open it manually instead of the flow silently hanging.\n * <a href={state.request.approvalUrl} target=\"_blank\" rel=\"noreferrer\">\n * {state.popupBlocked ? \"Popup blocked — open approval\" : \"Open approval\"}\n * </a>\n * )}\n * {state.type === \"ready_to_open\" && (\n * // Mobile deep Direct: render the HTTPS continuation as a primary tap.\n * // The SDK keeps polling in this tab; if the tab is lost, restart.\n * <a href={state.mobileContinuationUrl} target=\"_blank\" rel=\"noreferrer\">\n * Open Vana\n * </a>\n * )}\n * </div>\n * );\n * }\n * ```\n *\n * @category Direct\n * @module react\n */\n\nexport {\n useDirectVanaConnect,\n type UseDirectVanaConnectOptions,\n type UseDirectVanaConnectResult,\n} from \"./direct/use-direct-vana-connect\";\n\n// Framework-agnostic store (usable without React).\nexport {\n createDirectConnectFlow,\n type ConnectWindow,\n type DirectBrowserPlatform,\n type DirectBrowserPlatformPolicy,\n type DirectConnectFlow,\n type DirectConnectState,\n type DirectConnectOptions,\n type DirectConnectTransports,\n} from \"./direct/connect-flow\";\n\n// Shared types useful when typing the transports.\nexport type {\n ForegroundDelivery,\n AccessRequest,\n AccessRequestStatus,\n AccessRequestStatusValue,\n ApprovedDataResult,\n} from \"./direct/types\";\n"],"mappings":"AAgDA;AAAA,EACE;AAAA,OAGK;AAGP;AAAA,EACE;AAAA,OAQK;","names":[]}
|
package/dist/server.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/server.ts"],"sourcesContent":["/**\n * Server entry point for the Vana SDK direct Data Portability flow.\n *\n * @remarks\n * Exposes {@link createDirectDataController} and its supporting types/errors.\n * This is a Node/server entry point — it owns the app private key and must never\n * be imported into browser code.\n *\n * @example\n * ```typescript\n * import { createDirectDataController } from \"@opendatalabs/vana-sdk/server\";\n *\n * export const vana = createDirectDataController({\n * env: process.env.VANA_ENV === \"dev\" ? \"dev\" : \"production\",\n * appPrivateKey: process.env.VANA_APP_PRIVATE_KEY!,\n * app: { id: \"notes-lens\", name: \"Notes Lens\", homepageUrl: process.env.VANA_APP_URL! },\n * source: \"icloud_notes\",\n * scopes: [\"icloud_notes.notes\"],\n * });\n * ```\n *\n * @category Direct\n * @module server\n */\n\nexport {\n createDirectDataController,\n type DirectDataController,\n type DirectDataControllerConfig,\n type DirectEscrowConfig,\n} from \"./direct/controller\";\n\n// Lower-level building blocks (advanced use / custom transports).\nexport {\n createDefaultAccessRequestClient,\n buildApprovalUrl,\n type DefaultAccessRequestClientOptions,\n type FetchLike,\n} from \"./direct/access-request-client\";\nexport {\n buildPersonalServerDataReadRequest,\n readPersonalServerData,\n parsePersonalServerPaymentRequired,\n dataPathForScope,\n type PersonalServerDataReadRequest,\n type PersonalServerReadResult,\n type PersonalServerFetch,\n type PersonalServerTransportRetryOptions,\n type FetchResponseLike,\n} from \"./direct/personal-server-read\";\n// Escrow-backed payment (built on protocol/escrow).\nexport {\n authorizeEscrowPayment,\n authorizeGrantPayment,\n buildEscrowPaymentHeader,\n buildGrantPaymentHeader,\n paymentResponseMetadataFromHeader,\n toDirectPaymentReceipt,\n toDirectFeeBreakdown,\n createDefaultNonceSource,\n DATA_ACCESS_OP_TYPE,\n GRANT_OP_TYPE,\n type EscrowPaymentConfig,\n type EscrowPaymentHeaderConfig,\n type SignTypedDataFn,\n type PaymentNonceSource,\n} from \"./direct/escrow-payment\";\nexport {\n getDirectEndpoints,\n PRODUCTION_ENDPOINTS,\n DEV_ENDPOINTS,\n} from \"./direct/endpoints\";\n\n// Errors\nexport {\n DirectConfigError,\n AccessNotApprovedError,\n ScopeNotApprovedError,\n PersonalServerReadError,\n PaymentRequiredError,\n} from \"./direct/errors\";\n\n// Shared types\nexport type {\n DirectEnv,\n DirectNetwork,\n DirectAppConfig,\n AppIdentity,\n DirectServiceEndpoints,\n AccessRequest,\n AccessRequestStatus,\n AccessRequestStatusValue,\n ApprovedDataResult,\n MultiScopeDataResult,\n AccessRequestClient,\n DirectOpTypeValue,\n PersonalServerDataAccessPaymentOperation,\n PersonalServerGrantPaymentOperation,\n PersonalServerPaymentOperation,\n PersonalServerPaymentRequired,\n DirectPaymentReceipt,\n DirectPaymentResponseMetadata,\n DirectFeeBreakdown,\n} from \"./direct/types\";\n\n// Op-type vocabulary constant.\nexport { DirectOpType } from \"./direct/types\";\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAyBA,wBAKO;AAGP,mCAKO;AACP,kCAUO;AAEP,4BAeO;AACP,uBAIO;AAGP,oBAMO;
|
|
1
|
+
{"version":3,"sources":["../src/server.ts"],"sourcesContent":["/**\n * Server entry point for the Vana SDK direct Data Portability flow.\n *\n * @remarks\n * Exposes {@link createDirectDataController} and its supporting types/errors.\n * This is a Node/server entry point — it owns the app private key and must never\n * be imported into browser code.\n *\n * @example\n * ```typescript\n * import { createDirectDataController } from \"@opendatalabs/vana-sdk/server\";\n *\n * export const vana = createDirectDataController({\n * env: process.env.VANA_ENV === \"dev\" ? \"dev\" : \"production\",\n * appPrivateKey: process.env.VANA_APP_PRIVATE_KEY!,\n * app: { id: \"notes-lens\", name: \"Notes Lens\", homepageUrl: process.env.VANA_APP_URL! },\n * source: \"icloud_notes\",\n * scopes: [\"icloud_notes.notes\"],\n * });\n * ```\n *\n * @category Direct\n * @module server\n */\n\nexport {\n createDirectDataController,\n type DirectDataController,\n type DirectDataControllerConfig,\n type DirectEscrowConfig,\n} from \"./direct/controller\";\n\n// Lower-level building blocks (advanced use / custom transports).\nexport {\n createDefaultAccessRequestClient,\n buildApprovalUrl,\n type DefaultAccessRequestClientOptions,\n type FetchLike,\n} from \"./direct/access-request-client\";\nexport {\n buildPersonalServerDataReadRequest,\n readPersonalServerData,\n parsePersonalServerPaymentRequired,\n dataPathForScope,\n type PersonalServerDataReadRequest,\n type PersonalServerReadResult,\n type PersonalServerFetch,\n type PersonalServerTransportRetryOptions,\n type FetchResponseLike,\n} from \"./direct/personal-server-read\";\n// Escrow-backed payment (built on protocol/escrow).\nexport {\n authorizeEscrowPayment,\n authorizeGrantPayment,\n buildEscrowPaymentHeader,\n buildGrantPaymentHeader,\n paymentResponseMetadataFromHeader,\n toDirectPaymentReceipt,\n toDirectFeeBreakdown,\n createDefaultNonceSource,\n DATA_ACCESS_OP_TYPE,\n GRANT_OP_TYPE,\n type EscrowPaymentConfig,\n type EscrowPaymentHeaderConfig,\n type SignTypedDataFn,\n type PaymentNonceSource,\n} from \"./direct/escrow-payment\";\nexport {\n getDirectEndpoints,\n PRODUCTION_ENDPOINTS,\n DEV_ENDPOINTS,\n} from \"./direct/endpoints\";\n\n// Errors\nexport {\n DirectConfigError,\n AccessNotApprovedError,\n ScopeNotApprovedError,\n PersonalServerReadError,\n PaymentRequiredError,\n} from \"./direct/errors\";\n\n// Shared types\nexport type {\n DirectEnv,\n DirectNetwork,\n DirectAppConfig,\n ForegroundDelivery,\n AppIdentity,\n DirectServiceEndpoints,\n AccessRequest,\n AccessRequestStatus,\n AccessRequestStatusValue,\n ApprovedDataResult,\n MultiScopeDataResult,\n AccessRequestClient,\n DirectOpTypeValue,\n PersonalServerDataAccessPaymentOperation,\n PersonalServerGrantPaymentOperation,\n PersonalServerPaymentOperation,\n PersonalServerPaymentRequired,\n DirectPaymentReceipt,\n DirectPaymentResponseMetadata,\n DirectFeeBreakdown,\n} from \"./direct/types\";\n\n// Op-type vocabulary constant.\nexport { DirectOpType } from \"./direct/types\";\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAyBA,wBAKO;AAGP,mCAKO;AACP,kCAUO;AAEP,4BAeO;AACP,uBAIO;AAGP,oBAMO;AA2BP,mBAA6B;","names":[]}
|
package/dist/server.d.ts
CHANGED
|
@@ -28,5 +28,5 @@ export { buildPersonalServerDataReadRequest, readPersonalServerData, parsePerson
|
|
|
28
28
|
export { authorizeEscrowPayment, authorizeGrantPayment, buildEscrowPaymentHeader, buildGrantPaymentHeader, paymentResponseMetadataFromHeader, toDirectPaymentReceipt, toDirectFeeBreakdown, createDefaultNonceSource, DATA_ACCESS_OP_TYPE, GRANT_OP_TYPE, type EscrowPaymentConfig, type EscrowPaymentHeaderConfig, type SignTypedDataFn, type PaymentNonceSource, } from "./direct/escrow-payment.js";
|
|
29
29
|
export { getDirectEndpoints, PRODUCTION_ENDPOINTS, DEV_ENDPOINTS, } from "./direct/endpoints.js";
|
|
30
30
|
export { DirectConfigError, AccessNotApprovedError, ScopeNotApprovedError, PersonalServerReadError, PaymentRequiredError, } from "./direct/errors.js";
|
|
31
|
-
export type { DirectEnv, DirectNetwork, DirectAppConfig, AppIdentity, DirectServiceEndpoints, AccessRequest, AccessRequestStatus, AccessRequestStatusValue, ApprovedDataResult, MultiScopeDataResult, AccessRequestClient, DirectOpTypeValue, PersonalServerDataAccessPaymentOperation, PersonalServerGrantPaymentOperation, PersonalServerPaymentOperation, PersonalServerPaymentRequired, DirectPaymentReceipt, DirectPaymentResponseMetadata, DirectFeeBreakdown, } from "./direct/types.js";
|
|
31
|
+
export type { DirectEnv, DirectNetwork, DirectAppConfig, ForegroundDelivery, AppIdentity, DirectServiceEndpoints, AccessRequest, AccessRequestStatus, AccessRequestStatusValue, ApprovedDataResult, MultiScopeDataResult, AccessRequestClient, DirectOpTypeValue, PersonalServerDataAccessPaymentOperation, PersonalServerGrantPaymentOperation, PersonalServerPaymentOperation, PersonalServerPaymentRequired, DirectPaymentReceipt, DirectPaymentResponseMetadata, DirectFeeBreakdown, } from "./direct/types.js";
|
|
32
32
|
export { DirectOpType } from "./direct/types.js";
|