@opendatalabs/vana-sdk 3.20.1 → 3.21.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/dist/direct/access-request-client.cjs +100 -2
- package/dist/direct/access-request-client.cjs.map +1 -1
- package/dist/direct/access-request-client.d.ts +23 -1
- package/dist/direct/access-request-client.js +98 -1
- package/dist/direct/access-request-client.js.map +1 -1
- package/dist/direct/controller.cjs +4 -0
- package/dist/direct/controller.cjs.map +1 -1
- package/dist/direct/controller.d.ts +10 -1
- package/dist/direct/controller.js +6 -1
- package/dist/direct/controller.js.map +1 -1
- package/dist/direct/types.cjs.map +1 -1
- package/dist/direct/types.d.ts +45 -0
- package/dist/direct/types.js.map +1 -1
- package/dist/server.cjs +4 -2
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.ts +2 -2
- package/dist/server.js +4 -2
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
|
@@ -20,10 +20,13 @@ var access_request_client_exports = {};
|
|
|
20
20
|
__export(access_request_client_exports, {
|
|
21
21
|
buildApprovalUrl: () => buildApprovalUrl,
|
|
22
22
|
buildDirectAccessRequestAuthMessage: () => buildDirectAccessRequestAuthMessage,
|
|
23
|
-
createDefaultAccessRequestClient: () => createDefaultAccessRequestClient
|
|
23
|
+
createDefaultAccessRequestClient: () => createDefaultAccessRequestClient,
|
|
24
|
+
validateAccessRequestQuestions: () => validateAccessRequestQuestions
|
|
24
25
|
});
|
|
25
26
|
module.exports = __toCommonJS(access_request_client_exports);
|
|
26
27
|
var import_types = require("./types");
|
|
28
|
+
var import_scopes = require("../protocol/scopes");
|
|
29
|
+
var import_errors = require("./errors");
|
|
27
30
|
const VALID_STATUSES = [
|
|
28
31
|
"pending",
|
|
29
32
|
"approved",
|
|
@@ -86,6 +89,96 @@ function buildApprovalUrl(approvalBaseUrl, requestId) {
|
|
|
86
89
|
requestId
|
|
87
90
|
)}?mode=page`;
|
|
88
91
|
}
|
|
92
|
+
const RECOMPUTE_VALUES = ["snapshot", "on-change"];
|
|
93
|
+
function parseConcreteScope(field, value) {
|
|
94
|
+
if (typeof value !== "string") {
|
|
95
|
+
throw new import_errors.DirectConfigError(`${field} must be a string`, { field });
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
return (0, import_scopes.parseScope)(value);
|
|
99
|
+
} catch {
|
|
100
|
+
throw new import_errors.DirectConfigError(
|
|
101
|
+
`${field} "${value}" is not a concrete scope. Use {source}.{category}[.{subcategory}] with no wildcard and no operation prefix.`,
|
|
102
|
+
{ field, value }
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function validateAccessRequestQuestions(questions, scopes) {
|
|
107
|
+
if (questions.length === 0 || questions.length > 4) {
|
|
108
|
+
throw new import_errors.DirectConfigError(
|
|
109
|
+
`questions must contain 1 to 4 entries when present, got ${questions.length}. Omit the field to send no questions.`,
|
|
110
|
+
{ count: questions.length }
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
const seenDerived = /* @__PURE__ */ new Set();
|
|
114
|
+
questions.forEach((question, index) => {
|
|
115
|
+
const label = `questions[${index}]`;
|
|
116
|
+
const derived = parseConcreteScope(
|
|
117
|
+
`${label}.derivedScope`,
|
|
118
|
+
question.derivedScope
|
|
119
|
+
);
|
|
120
|
+
if (seenDerived.has(question.derivedScope)) {
|
|
121
|
+
throw new import_errors.DirectConfigError(
|
|
122
|
+
`${label}.derivedScope "${question.derivedScope}" is already used by an earlier question. Each question must target its own derived scope.`,
|
|
123
|
+
{ derivedScope: question.derivedScope }
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
seenDerived.add(question.derivedScope);
|
|
127
|
+
if (!scopes.includes(question.derivedScope)) {
|
|
128
|
+
throw new import_errors.DirectConfigError(
|
|
129
|
+
`${label}.derivedScope "${question.derivedScope}" must also appear in scopes as a bare read entry, so the app can read the answer it asked for.`,
|
|
130
|
+
{ derivedScope: question.derivedScope, scopes: [...scopes] }
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
if (question.sourceScopes.length === 0 || question.sourceScopes.length > 16) {
|
|
134
|
+
throw new import_errors.DirectConfigError(
|
|
135
|
+
`${label}.sourceScopes must contain 1 to 16 entries, got ${question.sourceScopes.length}.`,
|
|
136
|
+
{ count: question.sourceScopes.length }
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
const seenSources = /* @__PURE__ */ new Set();
|
|
140
|
+
for (const sourceScope of question.sourceScopes) {
|
|
141
|
+
const source = parseConcreteScope(`${label}.sourceScopes`, sourceScope);
|
|
142
|
+
if (seenSources.has(sourceScope)) {
|
|
143
|
+
throw new import_errors.DirectConfigError(
|
|
144
|
+
`${label}.sourceScopes contains "${sourceScope}" more than once. Deduplicate the source scopes.`,
|
|
145
|
+
{ sourceScope }
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
seenSources.add(sourceScope);
|
|
149
|
+
if (sourceScope === question.derivedScope) {
|
|
150
|
+
throw new import_errors.DirectConfigError(
|
|
151
|
+
`${label}.sourceScopes must not contain the derived scope "${question.derivedScope}".`,
|
|
152
|
+
{ sourceScope }
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
if (source.source === derived.source) {
|
|
156
|
+
throw new import_errors.DirectConfigError(
|
|
157
|
+
`${label}.derivedScope "${question.derivedScope}" must not share its first dot-segment "${derived.source}" with source scope "${sourceScope}". Name the derived scope under the app's own namespace.`,
|
|
158
|
+
{ derivedScope: question.derivedScope, sourceScope }
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (typeof question.question !== "string") {
|
|
163
|
+
throw new import_errors.DirectConfigError(`${label}.question must be a string`, {
|
|
164
|
+
field: `${label}.question`
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
const trimmedLength = question.question.trim().length;
|
|
168
|
+
if (trimmedLength === 0 || trimmedLength > 4e3) {
|
|
169
|
+
throw new import_errors.DirectConfigError(
|
|
170
|
+
`${label}.question must be 1 to 4000 characters after trimming, got ${trimmedLength}.`,
|
|
171
|
+
{ length: trimmedLength }
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
if (question.recompute !== void 0 && !RECOMPUTE_VALUES.includes(question.recompute)) {
|
|
175
|
+
throw new import_errors.DirectConfigError(
|
|
176
|
+
`${label}.recompute must be "snapshot" or "on-change" when present, got "${String(question.recompute)}".`,
|
|
177
|
+
{ recompute: question.recompute }
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
89
182
|
function createDefaultAccessRequestClient(options) {
|
|
90
183
|
const fetchFn = options.fetchFn ?? globalThis.fetch;
|
|
91
184
|
if (!fetchFn) {
|
|
@@ -96,6 +189,9 @@ function createDefaultAccessRequestClient(options) {
|
|
|
96
189
|
const base = stripTrailingSlash(options.baseUrl);
|
|
97
190
|
return {
|
|
98
191
|
async createAccessRequest(input) {
|
|
192
|
+
if (input.questions !== void 0) {
|
|
193
|
+
validateAccessRequestQuestions(input.questions, input.scopes);
|
|
194
|
+
}
|
|
99
195
|
const path = "/api/data-connection-requests";
|
|
100
196
|
const idempotencyKey = input.idempotencyKey ?? (options.createIdempotencyKey ?? defaultCreateIdempotencyKey)();
|
|
101
197
|
const body = JSON.stringify({
|
|
@@ -106,6 +202,7 @@ function createDefaultAccessRequestClient(options) {
|
|
|
106
202
|
returnUrl: input.returnUrl,
|
|
107
203
|
network: input.network,
|
|
108
204
|
...input.foregroundDelivery !== void 0 ? { foregroundDelivery: input.foregroundDelivery } : {},
|
|
205
|
+
...input.questions !== void 0 ? { questions: input.questions } : {},
|
|
109
206
|
idempotencyKey
|
|
110
207
|
});
|
|
111
208
|
const res = await fetchFn(`${base}${path}`, {
|
|
@@ -193,6 +290,7 @@ function createDefaultAccessRequestClient(options) {
|
|
|
193
290
|
0 && (module.exports = {
|
|
194
291
|
buildApprovalUrl,
|
|
195
292
|
buildDirectAccessRequestAuthMessage,
|
|
196
|
-
createDefaultAccessRequestClient
|
|
293
|
+
createDefaultAccessRequestClient,
|
|
294
|
+
validateAccessRequestQuestions
|
|
197
295
|
});
|
|
198
296
|
//# sourceMappingURL=access-request-client.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/direct/access-request-client.ts"],"sourcesContent":["/**\n * Default client for the Vana Account access-request API.\n *\n * @remarks\n * Calls the Vana Account endpoints that issue `dcr_*` ids and approval URLs and\n * report request status. Inject a custom {@link AccessRequestClient} on the\n * controller to point at a different deployment; pass `fetchFn` to supply a test\n * double for the HTTP layer.\n *\n * @category Direct\n * @module direct/access-request-client\n */\n\nimport type {\n AccessRequest,\n AccessRequestClient,\n AccessRequestStatus,\n AccessRequestStatusValue,\n DirectEnv,\n} from \"./types\";\nimport { normalizeMobileContinuationUrl } from \"./types\";\nimport type { Web3SignedSignFn } from \"../auth/web3-signed-builder\";\n\n/** Minimal `fetch` signature so the client is testable without a global fetch. */\nexport type FetchLike = (\n input: string,\n init?: {\n method?: string;\n headers?: Record<string, string>;\n body?: string;\n },\n) => Promise<{\n ok: boolean;\n status: number;\n statusText: string;\n json(): Promise<unknown>;\n text(): Promise<string>;\n}>;\n\n/** Options for {@link createDefaultAccessRequestClient}. */\nexport interface DefaultAccessRequestClientOptions {\n /** Base URL of the Vana Account access-request API. */\n baseUrl: string;\n /** Base URL the user is sent to for approval. */\n approvalBaseUrl: string;\n /**\n * Target environment. Pins the allowed mobile continuation link host\n * (`open.vana.org` for production, `open-dev.vana.org` for dev). When omitted,\n * both canonical hosts pass the structural continuation-URL check.\n */\n env?: DirectEnv;\n /** `fetch` implementation. Defaults to the global `fetch`. */\n fetchFn?: FetchLike;\n /** App identity address used for direct access-request authentication. */\n appAddress?: string;\n /** EIP-191 signer for direct access-request authentication. */\n signMessage?: Web3SignedSignFn;\n /** Clock source used for signed request timestamps. */\n now?: () => number;\n /**\n * Create the signed DCR idempotency key used when a create call omits one.\n * Called once per create. Injectable for deterministic tests.\n */\n createIdempotencyKey?: () => string;\n}\n\nconst VALID_STATUSES: readonly AccessRequestStatusValue[] = [\n \"pending\",\n \"approved\",\n \"ready_for_read\",\n \"completed\",\n \"denied\",\n \"expired\",\n];\n\nfunction normalizeStatus(value: unknown): AccessRequestStatusValue {\n return VALID_STATUSES.includes(value as AccessRequestStatusValue)\n ? (value as AccessRequestStatusValue)\n : \"pending\";\n}\n\nfunction normalizeNetwork(value: unknown): AccessRequest[\"network\"] {\n return value === \"mainnet\" || value === \"moksha\" ? value : undefined;\n}\n\nfunction normalizeExpiresAt(value: unknown): string | undefined {\n return typeof value === \"string\" && Number.isFinite(Date.parse(value))\n ? value\n : undefined;\n}\n\nfunction defaultCreateIdempotencyKey(): string {\n if (typeof globalThis.crypto?.randomUUID !== \"function\") {\n throw new Error(\n \"Secure randomUUID is unavailable. Pass createIdempotencyKey to createDefaultAccessRequestClient.\",\n );\n }\n return globalThis.crypto.randomUUID();\n}\n\nfunction stripTrailingSlash(url: string): string {\n return url.replace(/\\/+$/, \"\");\n}\n\nconst DIRECT_ACCESS_REQUEST_MESSAGE_PREFIX = \"Vana Direct Access Request v1\";\n\ninterface DirectAccessRequestAuthInput {\n body: string;\n method: string;\n path: string;\n timestamp: string;\n}\n\nexport function buildDirectAccessRequestAuthMessage(\n input: DirectAccessRequestAuthInput,\n): string {\n return [\n DIRECT_ACCESS_REQUEST_MESSAGE_PREFIX,\n `method:${input.method.toUpperCase()}`,\n `path:${input.path}`,\n `timestamp:${input.timestamp}`,\n `body:${input.body}`,\n ].join(\"\\n\");\n}\n\nasync function buildDirectAccessRequestHeaders(\n options: DefaultAccessRequestClientOptions,\n input: Omit<DirectAccessRequestAuthInput, \"timestamp\">,\n): Promise<Record<string, string>> {\n if (!options.appAddress && !options.signMessage) {\n return {};\n }\n if (!options.appAddress || !options.signMessage) {\n throw new Error(\n \"Direct access-request authentication requires both `appAddress` and `signMessage`.\",\n );\n }\n\n const timestamp = String(options.now?.() ?? Date.now());\n const signature = await options.signMessage(\n buildDirectAccessRequestAuthMessage({ ...input, timestamp }),\n );\n\n return {\n \"X-Vana-App-Address\": options.appAddress,\n \"X-Vana-App-Signature\": signature,\n \"X-Vana-App-Timestamp\": timestamp,\n };\n}\n\n/**\n * Build an approval URL for a request id, matching the documented format\n * (`{app}/data-connection-requests/{requestId}?mode=page`).\n *\n * @param approvalBaseUrl - Base URL of the Vana approval app.\n * @param requestId - The `dcr_*` request id.\n * @returns The full approval URL.\n */\nexport function buildApprovalUrl(\n approvalBaseUrl: string,\n requestId: string,\n): string {\n return `${stripTrailingSlash(approvalBaseUrl)}/data-connection-requests/${encodeURIComponent(\n requestId,\n )}?mode=page`;\n}\n\n/**\n * Create the default {@link AccessRequestClient} for the Vana Account\n * access-request API.\n *\n * @param options - Base URLs and an optional `fetch` implementation.\n * @returns An {@link AccessRequestClient} backed by HTTP calls.\n */\nexport function createDefaultAccessRequestClient(\n options: DefaultAccessRequestClientOptions,\n): AccessRequestClient {\n const fetchFn = options.fetchFn ?? (globalThis.fetch as FetchLike);\n if (!fetchFn) {\n throw new Error(\n \"No fetch implementation available. Pass `fetchFn` to createDefaultAccessRequestClient.\",\n );\n }\n const base = stripTrailingSlash(options.baseUrl);\n\n return {\n async createAccessRequest(input): Promise<AccessRequest> {\n const path = \"/api/data-connection-requests\";\n // Every call is an independent logical create, so it gets its own key.\n // The client cannot tell two look-alike creates apart — one shared backend\n // controller serves many users with the same app, scopes, and returnUrl —\n // so deriving a key from the input would let the service deduplicate two\n // users onto a single DCR. Retrying an uncertain create is the caller's\n // decision: pass the same `idempotencyKey` back in.\n const idempotencyKey =\n input.idempotencyKey ??\n (options.createIdempotencyKey ?? defaultCreateIdempotencyKey)();\n const body = JSON.stringify({\n appAddress: input.appAddress,\n app: input.app,\n source: input.source,\n scopes: input.scopes,\n returnUrl: input.returnUrl,\n network: input.network,\n ...(input.foregroundDelivery !== undefined\n ? { foregroundDelivery: input.foregroundDelivery }\n : {}),\n idempotencyKey,\n });\n const res = await fetchFn(`${base}${path}`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...(await buildDirectAccessRequestHeaders(options, {\n body,\n method: \"POST\",\n path,\n })),\n },\n body,\n });\n if (!res.ok) {\n throw new Error(\n `Access request service error: ${res.status} ${res.statusText}`,\n );\n }\n const responseBody = (await res.json()) as {\n requestId?: string;\n id?: string;\n approvalUrl?: string;\n appAddress?: string;\n network?: unknown;\n expiresAt?: unknown;\n mobileContinuationUrl?: unknown;\n };\n const requestId = responseBody.requestId ?? responseBody.id;\n if (!requestId) {\n throw new Error(\"Access request service returned no requestId\");\n }\n return {\n requestId,\n approvalUrl:\n responseBody.approvalUrl ??\n buildApprovalUrl(options.approvalBaseUrl, requestId),\n appAddress: responseBody.appAddress ?? input.appAddress,\n network: normalizeNetwork(responseBody.network),\n expiresAt: normalizeExpiresAt(responseBody.expiresAt),\n mobileContinuationUrl: normalizeMobileContinuationUrl(\n responseBody.mobileContinuationUrl,\n options.env,\n ),\n };\n },\n\n async getAccessRequestStatus(\n requestId: string,\n ): Promise<AccessRequestStatus> {\n const path = `/api/data-connection-requests/${encodeURIComponent(requestId)}`;\n const res = await fetchFn(`${base}${path}`, {\n method: \"GET\",\n headers: await buildDirectAccessRequestHeaders(options, {\n body: \"\",\n method: \"GET\",\n path,\n }),\n });\n if (!res.ok) {\n throw new Error(\n `Access request service error: ${res.status} ${res.statusText}`,\n );\n }\n const body = (await res.json()) as {\n status?: string;\n personalServerUrl?: string;\n grantId?: string;\n scope?: string;\n mobileContinuationUrl?: unknown;\n scopes?: string[];\n };\n // `scopes` is the full approved set; `scope` is the first of them, kept\n // for callers (and deployments) that predate the array.\n const scopes =\n body.scopes && body.scopes.length > 0\n ? body.scopes\n : body.scope\n ? [body.scope]\n : undefined;\n return {\n status: normalizeStatus(body.status),\n personalServerUrl: body.personalServerUrl,\n grantId: body.grantId,\n scope: body.scope ?? scopes?.[0],\n scopes,\n mobileContinuationUrl: normalizeMobileContinuationUrl(\n body.mobileContinuationUrl,\n options.env,\n ),\n };\n },\n\n async acknowledgeRead(requestId: string): Promise<void> {\n const path = `/api/data-connection-requests/${encodeURIComponent(requestId)}/consumer-ack`;\n const res = await fetchFn(`${base}${path}`, {\n method: \"POST\",\n headers: await buildDirectAccessRequestHeaders(options, {\n body: \"\",\n method: \"POST\",\n path,\n }),\n });\n if (!res.ok) {\n throw new Error(\n `Access request ack service error: ${res.status} ${res.statusText}`,\n );\n }\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAoBA,mBAA+C;AA8C/C,MAAM,iBAAsD;AAAA,EAC1D;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,SAAS,gBAAgB,OAA0C;AACjE,SAAO,eAAe,SAAS,KAAiC,IAC3D,QACD;AACN;AAEA,SAAS,iBAAiB,OAA0C;AAClE,SAAO,UAAU,aAAa,UAAU,WAAW,QAAQ;AAC7D;AAEA,SAAS,mBAAmB,OAAoC;AAC9D,SAAO,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,MAAM,KAAK,CAAC,IACjE,QACA;AACN;AAEA,SAAS,8BAAsC;AAC7C,MAAI,OAAO,WAAW,QAAQ,eAAe,YAAY;AACvD,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,SAAO,WAAW,OAAO,WAAW;AACtC;AAEA,SAAS,mBAAmB,KAAqB;AAC/C,SAAO,IAAI,QAAQ,QAAQ,EAAE;AAC/B;AAEA,MAAM,uCAAuC;AAStC,SAAS,oCACd,OACQ;AACR,SAAO;AAAA,IACL;AAAA,IACA,UAAU,MAAM,OAAO,YAAY,CAAC;AAAA,IACpC,QAAQ,MAAM,IAAI;AAAA,IAClB,aAAa,MAAM,SAAS;AAAA,IAC5B,QAAQ,MAAM,IAAI;AAAA,EACpB,EAAE,KAAK,IAAI;AACb;AAEA,eAAe,gCACb,SACA,OACiC;AACjC,MAAI,CAAC,QAAQ,cAAc,CAAC,QAAQ,aAAa;AAC/C,WAAO,CAAC;AAAA,EACV;AACA,MAAI,CAAC,QAAQ,cAAc,CAAC,QAAQ,aAAa;AAC/C,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAEA,QAAM,YAAY,OAAO,QAAQ,MAAM,KAAK,KAAK,IAAI,CAAC;AACtD,QAAM,YAAY,MAAM,QAAQ;AAAA,IAC9B,oCAAoC,EAAE,GAAG,OAAO,UAAU,CAAC;AAAA,EAC7D;AAEA,SAAO;AAAA,IACL,sBAAsB,QAAQ;AAAA,IAC9B,wBAAwB;AAAA,IACxB,wBAAwB;AAAA,EAC1B;AACF;AAUO,SAAS,iBACd,iBACA,WACQ;AACR,SAAO,GAAG,mBAAmB,eAAe,CAAC,6BAA6B;AAAA,IACxE;AAAA,EACF,CAAC;AACH;AASO,SAAS,iCACd,SACqB;AACrB,QAAM,UAAU,QAAQ,WAAY,WAAW;AAC/C,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,QAAM,OAAO,mBAAmB,QAAQ,OAAO;AAE/C,SAAO;AAAA,IACL,MAAM,oBAAoB,OAA+B;AACvD,YAAM,OAAO;AAOb,YAAM,iBACJ,MAAM,mBACL,QAAQ,wBAAwB,6BAA6B;AAChE,YAAM,OAAO,KAAK,UAAU;AAAA,QAC1B,YAAY,MAAM;AAAA,QAClB,KAAK,MAAM;AAAA,QACX,QAAQ,MAAM;AAAA,QACd,QAAQ,MAAM;AAAA,QACd,WAAW,MAAM;AAAA,QACjB,SAAS,MAAM;AAAA,QACf,GAAI,MAAM,uBAAuB,SAC7B,EAAE,oBAAoB,MAAM,mBAAmB,IAC/C,CAAC;AAAA,QACL;AAAA,MACF,CAAC;AACD,YAAM,MAAM,MAAM,QAAQ,GAAG,IAAI,GAAG,IAAI,IAAI;AAAA,QAC1C,QAAQ;AAAA,QACR,SAAS;AAAA,UACP,gBAAgB;AAAA,UAChB,GAAI,MAAM,gCAAgC,SAAS;AAAA,YACjD;AAAA,YACA,QAAQ;AAAA,YACR;AAAA,UACF,CAAC;AAAA,QACH;AAAA,QACA;AAAA,MACF,CAAC;AACD,UAAI,CAAC,IAAI,IAAI;AACX,cAAM,IAAI;AAAA,UACR,iCAAiC,IAAI,MAAM,IAAI,IAAI,UAAU;AAAA,QAC/D;AAAA,MACF;AACA,YAAM,eAAgB,MAAM,IAAI,KAAK;AASrC,YAAM,YAAY,aAAa,aAAa,aAAa;AACzD,UAAI,CAAC,WAAW;AACd,cAAM,IAAI,MAAM,8CAA8C;AAAA,MAChE;AACA,aAAO;AAAA,QACL;AAAA,QACA,aACE,aAAa,eACb,iBAAiB,QAAQ,iBAAiB,SAAS;AAAA,QACrD,YAAY,aAAa,cAAc,MAAM;AAAA,QAC7C,SAAS,iBAAiB,aAAa,OAAO;AAAA,QAC9C,WAAW,mBAAmB,aAAa,SAAS;AAAA,QACpD,2BAAuB;AAAA,UACrB,aAAa;AAAA,UACb,QAAQ;AAAA,QACV;AAAA,MACF;AAAA,IACF;AAAA,IAEA,MAAM,uBACJ,WAC8B;AAC9B,YAAM,OAAO,iCAAiC,mBAAmB,SAAS,CAAC;AAC3E,YAAM,MAAM,MAAM,QAAQ,GAAG,IAAI,GAAG,IAAI,IAAI;AAAA,QAC1C,QAAQ;AAAA,QACR,SAAS,MAAM,gCAAgC,SAAS;AAAA,UACtD,MAAM;AAAA,UACN,QAAQ;AAAA,UACR;AAAA,QACF,CAAC;AAAA,MACH,CAAC;AACD,UAAI,CAAC,IAAI,IAAI;AACX,cAAM,IAAI;AAAA,UACR,iCAAiC,IAAI,MAAM,IAAI,IAAI,UAAU;AAAA,QAC/D;AAAA,MACF;AACA,YAAM,OAAQ,MAAM,IAAI,KAAK;AAU7B,YAAM,SACJ,KAAK,UAAU,KAAK,OAAO,SAAS,IAChC,KAAK,SACL,KAAK,QACH,CAAC,KAAK,KAAK,IACX;AACR,aAAO;AAAA,QACL,QAAQ,gBAAgB,KAAK,MAAM;AAAA,QACnC,mBAAmB,KAAK;AAAA,QACxB,SAAS,KAAK;AAAA,QACd,OAAO,KAAK,SAAS,SAAS,CAAC;AAAA,QAC/B;AAAA,QACA,2BAAuB;AAAA,UACrB,KAAK;AAAA,UACL,QAAQ;AAAA,QACV;AAAA,MACF;AAAA,IACF;AAAA,IAEA,MAAM,gBAAgB,WAAkC;AACtD,YAAM,OAAO,iCAAiC,mBAAmB,SAAS,CAAC;AAC3E,YAAM,MAAM,MAAM,QAAQ,GAAG,IAAI,GAAG,IAAI,IAAI;AAAA,QAC1C,QAAQ;AAAA,QACR,SAAS,MAAM,gCAAgC,SAAS;AAAA,UACtD,MAAM;AAAA,UACN,QAAQ;AAAA,UACR;AAAA,QACF,CAAC;AAAA,MACH,CAAC;AACD,UAAI,CAAC,IAAI,IAAI;AACX,cAAM,IAAI;AAAA,UACR,qCAAqC,IAAI,MAAM,IAAI,IAAI,UAAU;AAAA,QACnE;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/direct/access-request-client.ts"],"sourcesContent":["/**\n * Default client for the Vana Account access-request API.\n *\n * @remarks\n * Calls the Vana Account endpoints that issue `dcr_*` ids and approval URLs and\n * report request status. Inject a custom {@link AccessRequestClient} on the\n * controller to point at a different deployment; pass `fetchFn` to supply a test\n * double for the HTTP layer.\n *\n * @category Direct\n * @module direct/access-request-client\n */\n\nimport type {\n AccessRequest,\n AccessRequestClient,\n AccessRequestQuestion,\n AccessRequestStatus,\n AccessRequestStatusValue,\n DirectEnv,\n} from \"./types\";\nimport { normalizeMobileContinuationUrl } from \"./types\";\nimport type { Web3SignedSignFn } from \"../auth/web3-signed-builder\";\nimport { parseScope, type ParsedScope } from \"../protocol/scopes\";\nimport { DirectConfigError } from \"./errors\";\n\n/** Minimal `fetch` signature so the client is testable without a global fetch. */\nexport type FetchLike = (\n input: string,\n init?: {\n method?: string;\n headers?: Record<string, string>;\n body?: string;\n },\n) => Promise<{\n ok: boolean;\n status: number;\n statusText: string;\n json(): Promise<unknown>;\n text(): Promise<string>;\n}>;\n\n/** Options for {@link createDefaultAccessRequestClient}. */\nexport interface DefaultAccessRequestClientOptions {\n /** Base URL of the Vana Account access-request API. */\n baseUrl: string;\n /** Base URL the user is sent to for approval. */\n approvalBaseUrl: string;\n /**\n * Target environment. Pins the allowed mobile continuation link host\n * (`open.vana.org` for production, `open-dev.vana.org` for dev). When omitted,\n * both canonical hosts pass the structural continuation-URL check.\n */\n env?: DirectEnv;\n /** `fetch` implementation. Defaults to the global `fetch`. */\n fetchFn?: FetchLike;\n /** App identity address used for direct access-request authentication. */\n appAddress?: string;\n /** EIP-191 signer for direct access-request authentication. */\n signMessage?: Web3SignedSignFn;\n /** Clock source used for signed request timestamps. */\n now?: () => number;\n /**\n * Create the signed DCR idempotency key used when a create call omits one.\n * Called once per create. Injectable for deterministic tests.\n */\n createIdempotencyKey?: () => string;\n}\n\nconst VALID_STATUSES: readonly AccessRequestStatusValue[] = [\n \"pending\",\n \"approved\",\n \"ready_for_read\",\n \"completed\",\n \"denied\",\n \"expired\",\n];\n\nfunction normalizeStatus(value: unknown): AccessRequestStatusValue {\n return VALID_STATUSES.includes(value as AccessRequestStatusValue)\n ? (value as AccessRequestStatusValue)\n : \"pending\";\n}\n\nfunction normalizeNetwork(value: unknown): AccessRequest[\"network\"] {\n return value === \"mainnet\" || value === \"moksha\" ? value : undefined;\n}\n\nfunction normalizeExpiresAt(value: unknown): string | undefined {\n return typeof value === \"string\" && Number.isFinite(Date.parse(value))\n ? value\n : undefined;\n}\n\nfunction defaultCreateIdempotencyKey(): string {\n if (typeof globalThis.crypto?.randomUUID !== \"function\") {\n throw new Error(\n \"Secure randomUUID is unavailable. Pass createIdempotencyKey to createDefaultAccessRequestClient.\",\n );\n }\n return globalThis.crypto.randomUUID();\n}\n\nfunction stripTrailingSlash(url: string): string {\n return url.replace(/\\/+$/, \"\");\n}\n\nconst DIRECT_ACCESS_REQUEST_MESSAGE_PREFIX = \"Vana Direct Access Request v1\";\n\ninterface DirectAccessRequestAuthInput {\n body: string;\n method: string;\n path: string;\n timestamp: string;\n}\n\nexport function buildDirectAccessRequestAuthMessage(\n input: DirectAccessRequestAuthInput,\n): string {\n return [\n DIRECT_ACCESS_REQUEST_MESSAGE_PREFIX,\n `method:${input.method.toUpperCase()}`,\n `path:${input.path}`,\n `timestamp:${input.timestamp}`,\n `body:${input.body}`,\n ].join(\"\\n\");\n}\n\nasync function buildDirectAccessRequestHeaders(\n options: DefaultAccessRequestClientOptions,\n input: Omit<DirectAccessRequestAuthInput, \"timestamp\">,\n): Promise<Record<string, string>> {\n if (!options.appAddress && !options.signMessage) {\n return {};\n }\n if (!options.appAddress || !options.signMessage) {\n throw new Error(\n \"Direct access-request authentication requires both `appAddress` and `signMessage`.\",\n );\n }\n\n const timestamp = String(options.now?.() ?? Date.now());\n const signature = await options.signMessage(\n buildDirectAccessRequestAuthMessage({ ...input, timestamp }),\n );\n\n return {\n \"X-Vana-App-Address\": options.appAddress,\n \"X-Vana-App-Signature\": signature,\n \"X-Vana-App-Timestamp\": timestamp,\n };\n}\n\n/**\n * Build an approval URL for a request id, matching the documented format\n * (`{app}/data-connection-requests/{requestId}?mode=page`).\n *\n * @param approvalBaseUrl - Base URL of the Vana approval app.\n * @param requestId - The `dcr_*` request id.\n * @returns The full approval URL.\n */\nexport function buildApprovalUrl(\n approvalBaseUrl: string,\n requestId: string,\n): string {\n return `${stripTrailingSlash(approvalBaseUrl)}/data-connection-requests/${encodeURIComponent(\n requestId,\n )}?mode=page`;\n}\n\n/** The `recompute` values the question contract defines today. */\nconst RECOMPUTE_VALUES: readonly string[] = [\"snapshot\", \"on-change\"];\n\nfunction parseConcreteScope(field: string, value: unknown): ParsedScope {\n if (typeof value !== \"string\") {\n throw new DirectConfigError(`${field} must be a string`, { field });\n }\n try {\n return parseScope(value);\n } catch {\n throw new DirectConfigError(\n `${field} \"${value}\" is not a concrete scope. Use {source}.{category}[.{subcategory}] with no wildcard and no operation prefix.`,\n { field, value },\n );\n }\n}\n\n/**\n * Validate the derivative questions on a create input against the request\n * scope entries.\n *\n * @remarks\n * Client-side mirror of the access-request service rules so builders fail\n * fast, before the create request is signed and sent; the service remains\n * authoritative. Rules: 1 to 4 questions; every `derivedScope` and every\n * `sourceScope` is a concrete scope (wildcards rejected); 1 to 16 source\n * scopes per question with no duplicates and none equal to the derived scope;\n * the first dot-segment of the derived scope differs from the first\n * dot-segment of every source scope; the derived scope appears verbatim in\n * `scopes` as a bare read entry; no two questions share a derived scope; the\n * question text is 1 to 4000 characters after trimming; `recompute`, when\n * present, is `\"snapshot\"` or `\"on-change\"`.\n *\n * @param questions - The `questions` array from the create input.\n * @param scopes - The request's grant scope entries, verbatim.\n * @throws {DirectConfigError} - When any rule is violated. The message names\n * the offending question index and field.\n */\nexport function validateAccessRequestQuestions(\n questions: readonly AccessRequestQuestion[],\n scopes: readonly string[],\n): void {\n if (questions.length === 0 || questions.length > 4) {\n throw new DirectConfigError(\n `questions must contain 1 to 4 entries when present, got ${questions.length}. Omit the field to send no questions.`,\n { count: questions.length },\n );\n }\n const seenDerived = new Set<string>();\n questions.forEach((question, index) => {\n const label = `questions[${index}]`;\n const derived = parseConcreteScope(\n `${label}.derivedScope`,\n question.derivedScope,\n );\n if (seenDerived.has(question.derivedScope)) {\n throw new DirectConfigError(\n `${label}.derivedScope \"${question.derivedScope}\" is already used by an earlier question. Each question must target its own derived scope.`,\n { derivedScope: question.derivedScope },\n );\n }\n seenDerived.add(question.derivedScope);\n // The bare entry (no operation prefix) is what makes the answer readable\n // by the app: `write:coach.weekly` alone would not grant the read back.\n if (!scopes.includes(question.derivedScope)) {\n throw new DirectConfigError(\n `${label}.derivedScope \"${question.derivedScope}\" must also appear in scopes as a bare read entry, so the app can read the answer it asked for.`,\n { derivedScope: question.derivedScope, scopes: [...scopes] },\n );\n }\n if (\n question.sourceScopes.length === 0 ||\n question.sourceScopes.length > 16\n ) {\n throw new DirectConfigError(\n `${label}.sourceScopes must contain 1 to 16 entries, got ${question.sourceScopes.length}.`,\n { count: question.sourceScopes.length },\n );\n }\n const seenSources = new Set<string>();\n for (const sourceScope of question.sourceScopes) {\n const source = parseConcreteScope(`${label}.sourceScopes`, sourceScope);\n if (seenSources.has(sourceScope)) {\n throw new DirectConfigError(\n `${label}.sourceScopes contains \"${sourceScope}\" more than once. Deduplicate the source scopes.`,\n { sourceScope },\n );\n }\n seenSources.add(sourceScope);\n if (sourceScope === question.derivedScope) {\n throw new DirectConfigError(\n `${label}.sourceScopes must not contain the derived scope \"${question.derivedScope}\".`,\n { sourceScope },\n );\n }\n if (source.source === derived.source) {\n throw new DirectConfigError(\n `${label}.derivedScope \"${question.derivedScope}\" must not share its first dot-segment \"${derived.source}\" with source scope \"${sourceScope}\". Name the derived scope under the app's own namespace.`,\n { derivedScope: question.derivedScope, sourceScope },\n );\n }\n }\n if (typeof question.question !== \"string\") {\n throw new DirectConfigError(`${label}.question must be a string`, {\n field: `${label}.question`,\n });\n }\n const trimmedLength = question.question.trim().length;\n if (trimmedLength === 0 || trimmedLength > 4000) {\n throw new DirectConfigError(\n `${label}.question must be 1 to 4000 characters after trimming, got ${trimmedLength}.`,\n { length: trimmedLength },\n );\n }\n if (\n question.recompute !== undefined &&\n !RECOMPUTE_VALUES.includes(question.recompute)\n ) {\n throw new DirectConfigError(\n `${label}.recompute must be \"snapshot\" or \"on-change\" when present, got \"${String(question.recompute)}\".`,\n { recompute: question.recompute },\n );\n }\n });\n}\n\n/**\n * Create the default {@link AccessRequestClient} for the Vana Account\n * access-request API.\n *\n * @param options - Base URLs and an optional `fetch` implementation.\n * @returns An {@link AccessRequestClient} backed by HTTP calls.\n */\nexport function createDefaultAccessRequestClient(\n options: DefaultAccessRequestClientOptions,\n): AccessRequestClient {\n const fetchFn = options.fetchFn ?? (globalThis.fetch as FetchLike);\n if (!fetchFn) {\n throw new Error(\n \"No fetch implementation available. Pass `fetchFn` to createDefaultAccessRequestClient.\",\n );\n }\n const base = stripTrailingSlash(options.baseUrl);\n\n return {\n async createAccessRequest(input): Promise<AccessRequest> {\n if (input.questions !== undefined) {\n validateAccessRequestQuestions(input.questions, input.scopes);\n }\n const path = \"/api/data-connection-requests\";\n // Every call is an independent logical create, so it gets its own key.\n // The client cannot tell two look-alike creates apart — one shared backend\n // controller serves many users with the same app, scopes, and returnUrl —\n // so deriving a key from the input would let the service deduplicate two\n // users onto a single DCR. Retrying an uncertain create is the caller's\n // decision: pass the same `idempotencyKey` back in.\n const idempotencyKey =\n input.idempotencyKey ??\n (options.createIdempotencyKey ?? defaultCreateIdempotencyKey)();\n const body = JSON.stringify({\n appAddress: input.appAddress,\n app: input.app,\n source: input.source,\n scopes: input.scopes,\n returnUrl: input.returnUrl,\n network: input.network,\n ...(input.foregroundDelivery !== undefined\n ? { foregroundDelivery: input.foregroundDelivery }\n : {}),\n ...(input.questions !== undefined\n ? { questions: input.questions }\n : {}),\n idempotencyKey,\n });\n const res = await fetchFn(`${base}${path}`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...(await buildDirectAccessRequestHeaders(options, {\n body,\n method: \"POST\",\n path,\n })),\n },\n body,\n });\n if (!res.ok) {\n throw new Error(\n `Access request service error: ${res.status} ${res.statusText}`,\n );\n }\n const responseBody = (await res.json()) as {\n requestId?: string;\n id?: string;\n approvalUrl?: string;\n appAddress?: string;\n network?: unknown;\n expiresAt?: unknown;\n mobileContinuationUrl?: unknown;\n };\n const requestId = responseBody.requestId ?? responseBody.id;\n if (!requestId) {\n throw new Error(\"Access request service returned no requestId\");\n }\n return {\n requestId,\n approvalUrl:\n responseBody.approvalUrl ??\n buildApprovalUrl(options.approvalBaseUrl, requestId),\n appAddress: responseBody.appAddress ?? input.appAddress,\n network: normalizeNetwork(responseBody.network),\n expiresAt: normalizeExpiresAt(responseBody.expiresAt),\n mobileContinuationUrl: normalizeMobileContinuationUrl(\n responseBody.mobileContinuationUrl,\n options.env,\n ),\n };\n },\n\n async getAccessRequestStatus(\n requestId: string,\n ): Promise<AccessRequestStatus> {\n const path = `/api/data-connection-requests/${encodeURIComponent(requestId)}`;\n const res = await fetchFn(`${base}${path}`, {\n method: \"GET\",\n headers: await buildDirectAccessRequestHeaders(options, {\n body: \"\",\n method: \"GET\",\n path,\n }),\n });\n if (!res.ok) {\n throw new Error(\n `Access request service error: ${res.status} ${res.statusText}`,\n );\n }\n const body = (await res.json()) as {\n status?: string;\n personalServerUrl?: string;\n grantId?: string;\n scope?: string;\n mobileContinuationUrl?: unknown;\n scopes?: string[];\n };\n // `scopes` is the full approved set; `scope` is the first of them, kept\n // for callers (and deployments) that predate the array.\n const scopes =\n body.scopes && body.scopes.length > 0\n ? body.scopes\n : body.scope\n ? [body.scope]\n : undefined;\n return {\n status: normalizeStatus(body.status),\n personalServerUrl: body.personalServerUrl,\n grantId: body.grantId,\n scope: body.scope ?? scopes?.[0],\n scopes,\n mobileContinuationUrl: normalizeMobileContinuationUrl(\n body.mobileContinuationUrl,\n options.env,\n ),\n };\n },\n\n async acknowledgeRead(requestId: string): Promise<void> {\n const path = `/api/data-connection-requests/${encodeURIComponent(requestId)}/consumer-ack`;\n const res = await fetchFn(`${base}${path}`, {\n method: \"POST\",\n headers: await buildDirectAccessRequestHeaders(options, {\n body: \"\",\n method: \"POST\",\n path,\n }),\n });\n if (!res.ok) {\n throw new Error(\n `Access request ack service error: ${res.status} ${res.statusText}`,\n );\n }\n },\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* @category Direct
|
|
11
11
|
* @module direct/access-request-client
|
|
12
12
|
*/
|
|
13
|
-
import type { AccessRequestClient, DirectEnv } from "./types.js";
|
|
13
|
+
import type { AccessRequestClient, AccessRequestQuestion, DirectEnv } from "./types.js";
|
|
14
14
|
import type { Web3SignedSignFn } from "../auth/web3-signed-builder.js";
|
|
15
15
|
/** Minimal `fetch` signature so the client is testable without a global fetch. */
|
|
16
16
|
export type FetchLike = (input: string, init?: {
|
|
@@ -66,6 +66,28 @@ export declare function buildDirectAccessRequestAuthMessage(input: DirectAccessR
|
|
|
66
66
|
* @returns The full approval URL.
|
|
67
67
|
*/
|
|
68
68
|
export declare function buildApprovalUrl(approvalBaseUrl: string, requestId: string): string;
|
|
69
|
+
/**
|
|
70
|
+
* Validate the derivative questions on a create input against the request
|
|
71
|
+
* scope entries.
|
|
72
|
+
*
|
|
73
|
+
* @remarks
|
|
74
|
+
* Client-side mirror of the access-request service rules so builders fail
|
|
75
|
+
* fast, before the create request is signed and sent; the service remains
|
|
76
|
+
* authoritative. Rules: 1 to 4 questions; every `derivedScope` and every
|
|
77
|
+
* `sourceScope` is a concrete scope (wildcards rejected); 1 to 16 source
|
|
78
|
+
* scopes per question with no duplicates and none equal to the derived scope;
|
|
79
|
+
* the first dot-segment of the derived scope differs from the first
|
|
80
|
+
* dot-segment of every source scope; the derived scope appears verbatim in
|
|
81
|
+
* `scopes` as a bare read entry; no two questions share a derived scope; the
|
|
82
|
+
* question text is 1 to 4000 characters after trimming; `recompute`, when
|
|
83
|
+
* present, is `"snapshot"` or `"on-change"`.
|
|
84
|
+
*
|
|
85
|
+
* @param questions - The `questions` array from the create input.
|
|
86
|
+
* @param scopes - The request's grant scope entries, verbatim.
|
|
87
|
+
* @throws {DirectConfigError} - When any rule is violated. The message names
|
|
88
|
+
* the offending question index and field.
|
|
89
|
+
*/
|
|
90
|
+
export declare function validateAccessRequestQuestions(questions: readonly AccessRequestQuestion[], scopes: readonly string[]): void;
|
|
69
91
|
/**
|
|
70
92
|
* Create the default {@link AccessRequestClient} for the Vana Account
|
|
71
93
|
* access-request API.
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { normalizeMobileContinuationUrl } from "./types.js";
|
|
2
|
+
import { parseScope } from "../protocol/scopes.js";
|
|
3
|
+
import { DirectConfigError } from "./errors.js";
|
|
2
4
|
const VALID_STATUSES = [
|
|
3
5
|
"pending",
|
|
4
6
|
"approved",
|
|
@@ -61,6 +63,96 @@ function buildApprovalUrl(approvalBaseUrl, requestId) {
|
|
|
61
63
|
requestId
|
|
62
64
|
)}?mode=page`;
|
|
63
65
|
}
|
|
66
|
+
const RECOMPUTE_VALUES = ["snapshot", "on-change"];
|
|
67
|
+
function parseConcreteScope(field, value) {
|
|
68
|
+
if (typeof value !== "string") {
|
|
69
|
+
throw new DirectConfigError(`${field} must be a string`, { field });
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
return parseScope(value);
|
|
73
|
+
} catch {
|
|
74
|
+
throw new DirectConfigError(
|
|
75
|
+
`${field} "${value}" is not a concrete scope. Use {source}.{category}[.{subcategory}] with no wildcard and no operation prefix.`,
|
|
76
|
+
{ field, value }
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function validateAccessRequestQuestions(questions, scopes) {
|
|
81
|
+
if (questions.length === 0 || questions.length > 4) {
|
|
82
|
+
throw new DirectConfigError(
|
|
83
|
+
`questions must contain 1 to 4 entries when present, got ${questions.length}. Omit the field to send no questions.`,
|
|
84
|
+
{ count: questions.length }
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
const seenDerived = /* @__PURE__ */ new Set();
|
|
88
|
+
questions.forEach((question, index) => {
|
|
89
|
+
const label = `questions[${index}]`;
|
|
90
|
+
const derived = parseConcreteScope(
|
|
91
|
+
`${label}.derivedScope`,
|
|
92
|
+
question.derivedScope
|
|
93
|
+
);
|
|
94
|
+
if (seenDerived.has(question.derivedScope)) {
|
|
95
|
+
throw new DirectConfigError(
|
|
96
|
+
`${label}.derivedScope "${question.derivedScope}" is already used by an earlier question. Each question must target its own derived scope.`,
|
|
97
|
+
{ derivedScope: question.derivedScope }
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
seenDerived.add(question.derivedScope);
|
|
101
|
+
if (!scopes.includes(question.derivedScope)) {
|
|
102
|
+
throw new DirectConfigError(
|
|
103
|
+
`${label}.derivedScope "${question.derivedScope}" must also appear in scopes as a bare read entry, so the app can read the answer it asked for.`,
|
|
104
|
+
{ derivedScope: question.derivedScope, scopes: [...scopes] }
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
if (question.sourceScopes.length === 0 || question.sourceScopes.length > 16) {
|
|
108
|
+
throw new DirectConfigError(
|
|
109
|
+
`${label}.sourceScopes must contain 1 to 16 entries, got ${question.sourceScopes.length}.`,
|
|
110
|
+
{ count: question.sourceScopes.length }
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
const seenSources = /* @__PURE__ */ new Set();
|
|
114
|
+
for (const sourceScope of question.sourceScopes) {
|
|
115
|
+
const source = parseConcreteScope(`${label}.sourceScopes`, sourceScope);
|
|
116
|
+
if (seenSources.has(sourceScope)) {
|
|
117
|
+
throw new DirectConfigError(
|
|
118
|
+
`${label}.sourceScopes contains "${sourceScope}" more than once. Deduplicate the source scopes.`,
|
|
119
|
+
{ sourceScope }
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
seenSources.add(sourceScope);
|
|
123
|
+
if (sourceScope === question.derivedScope) {
|
|
124
|
+
throw new DirectConfigError(
|
|
125
|
+
`${label}.sourceScopes must not contain the derived scope "${question.derivedScope}".`,
|
|
126
|
+
{ sourceScope }
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
if (source.source === derived.source) {
|
|
130
|
+
throw new DirectConfigError(
|
|
131
|
+
`${label}.derivedScope "${question.derivedScope}" must not share its first dot-segment "${derived.source}" with source scope "${sourceScope}". Name the derived scope under the app's own namespace.`,
|
|
132
|
+
{ derivedScope: question.derivedScope, sourceScope }
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (typeof question.question !== "string") {
|
|
137
|
+
throw new DirectConfigError(`${label}.question must be a string`, {
|
|
138
|
+
field: `${label}.question`
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
const trimmedLength = question.question.trim().length;
|
|
142
|
+
if (trimmedLength === 0 || trimmedLength > 4e3) {
|
|
143
|
+
throw new DirectConfigError(
|
|
144
|
+
`${label}.question must be 1 to 4000 characters after trimming, got ${trimmedLength}.`,
|
|
145
|
+
{ length: trimmedLength }
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
if (question.recompute !== void 0 && !RECOMPUTE_VALUES.includes(question.recompute)) {
|
|
149
|
+
throw new DirectConfigError(
|
|
150
|
+
`${label}.recompute must be "snapshot" or "on-change" when present, got "${String(question.recompute)}".`,
|
|
151
|
+
{ recompute: question.recompute }
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
64
156
|
function createDefaultAccessRequestClient(options) {
|
|
65
157
|
const fetchFn = options.fetchFn ?? globalThis.fetch;
|
|
66
158
|
if (!fetchFn) {
|
|
@@ -71,6 +163,9 @@ function createDefaultAccessRequestClient(options) {
|
|
|
71
163
|
const base = stripTrailingSlash(options.baseUrl);
|
|
72
164
|
return {
|
|
73
165
|
async createAccessRequest(input) {
|
|
166
|
+
if (input.questions !== void 0) {
|
|
167
|
+
validateAccessRequestQuestions(input.questions, input.scopes);
|
|
168
|
+
}
|
|
74
169
|
const path = "/api/data-connection-requests";
|
|
75
170
|
const idempotencyKey = input.idempotencyKey ?? (options.createIdempotencyKey ?? defaultCreateIdempotencyKey)();
|
|
76
171
|
const body = JSON.stringify({
|
|
@@ -81,6 +176,7 @@ function createDefaultAccessRequestClient(options) {
|
|
|
81
176
|
returnUrl: input.returnUrl,
|
|
82
177
|
network: input.network,
|
|
83
178
|
...input.foregroundDelivery !== void 0 ? { foregroundDelivery: input.foregroundDelivery } : {},
|
|
179
|
+
...input.questions !== void 0 ? { questions: input.questions } : {},
|
|
84
180
|
idempotencyKey
|
|
85
181
|
});
|
|
86
182
|
const res = await fetchFn(`${base}${path}`, {
|
|
@@ -167,6 +263,7 @@ function createDefaultAccessRequestClient(options) {
|
|
|
167
263
|
export {
|
|
168
264
|
buildApprovalUrl,
|
|
169
265
|
buildDirectAccessRequestAuthMessage,
|
|
170
|
-
createDefaultAccessRequestClient
|
|
266
|
+
createDefaultAccessRequestClient,
|
|
267
|
+
validateAccessRequestQuestions
|
|
171
268
|
};
|
|
172
269
|
//# sourceMappingURL=access-request-client.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/direct/access-request-client.ts"],"sourcesContent":["/**\n * Default client for the Vana Account access-request API.\n *\n * @remarks\n * Calls the Vana Account endpoints that issue `dcr_*` ids and approval URLs and\n * report request status. Inject a custom {@link AccessRequestClient} on the\n * controller to point at a different deployment; pass `fetchFn` to supply a test\n * double for the HTTP layer.\n *\n * @category Direct\n * @module direct/access-request-client\n */\n\nimport type {\n AccessRequest,\n AccessRequestClient,\n AccessRequestStatus,\n AccessRequestStatusValue,\n DirectEnv,\n} from \"./types\";\nimport { normalizeMobileContinuationUrl } from \"./types\";\nimport type { Web3SignedSignFn } from \"../auth/web3-signed-builder\";\n\n/** Minimal `fetch` signature so the client is testable without a global fetch. */\nexport type FetchLike = (\n input: string,\n init?: {\n method?: string;\n headers?: Record<string, string>;\n body?: string;\n },\n) => Promise<{\n ok: boolean;\n status: number;\n statusText: string;\n json(): Promise<unknown>;\n text(): Promise<string>;\n}>;\n\n/** Options for {@link createDefaultAccessRequestClient}. */\nexport interface DefaultAccessRequestClientOptions {\n /** Base URL of the Vana Account access-request API. */\n baseUrl: string;\n /** Base URL the user is sent to for approval. */\n approvalBaseUrl: string;\n /**\n * Target environment. Pins the allowed mobile continuation link host\n * (`open.vana.org` for production, `open-dev.vana.org` for dev). When omitted,\n * both canonical hosts pass the structural continuation-URL check.\n */\n env?: DirectEnv;\n /** `fetch` implementation. Defaults to the global `fetch`. */\n fetchFn?: FetchLike;\n /** App identity address used for direct access-request authentication. */\n appAddress?: string;\n /** EIP-191 signer for direct access-request authentication. */\n signMessage?: Web3SignedSignFn;\n /** Clock source used for signed request timestamps. */\n now?: () => number;\n /**\n * Create the signed DCR idempotency key used when a create call omits one.\n * Called once per create. Injectable for deterministic tests.\n */\n createIdempotencyKey?: () => string;\n}\n\nconst VALID_STATUSES: readonly AccessRequestStatusValue[] = [\n \"pending\",\n \"approved\",\n \"ready_for_read\",\n \"completed\",\n \"denied\",\n \"expired\",\n];\n\nfunction normalizeStatus(value: unknown): AccessRequestStatusValue {\n return VALID_STATUSES.includes(value as AccessRequestStatusValue)\n ? (value as AccessRequestStatusValue)\n : \"pending\";\n}\n\nfunction normalizeNetwork(value: unknown): AccessRequest[\"network\"] {\n return value === \"mainnet\" || value === \"moksha\" ? value : undefined;\n}\n\nfunction normalizeExpiresAt(value: unknown): string | undefined {\n return typeof value === \"string\" && Number.isFinite(Date.parse(value))\n ? value\n : undefined;\n}\n\nfunction defaultCreateIdempotencyKey(): string {\n if (typeof globalThis.crypto?.randomUUID !== \"function\") {\n throw new Error(\n \"Secure randomUUID is unavailable. Pass createIdempotencyKey to createDefaultAccessRequestClient.\",\n );\n }\n return globalThis.crypto.randomUUID();\n}\n\nfunction stripTrailingSlash(url: string): string {\n return url.replace(/\\/+$/, \"\");\n}\n\nconst DIRECT_ACCESS_REQUEST_MESSAGE_PREFIX = \"Vana Direct Access Request v1\";\n\ninterface DirectAccessRequestAuthInput {\n body: string;\n method: string;\n path: string;\n timestamp: string;\n}\n\nexport function buildDirectAccessRequestAuthMessage(\n input: DirectAccessRequestAuthInput,\n): string {\n return [\n DIRECT_ACCESS_REQUEST_MESSAGE_PREFIX,\n `method:${input.method.toUpperCase()}`,\n `path:${input.path}`,\n `timestamp:${input.timestamp}`,\n `body:${input.body}`,\n ].join(\"\\n\");\n}\n\nasync function buildDirectAccessRequestHeaders(\n options: DefaultAccessRequestClientOptions,\n input: Omit<DirectAccessRequestAuthInput, \"timestamp\">,\n): Promise<Record<string, string>> {\n if (!options.appAddress && !options.signMessage) {\n return {};\n }\n if (!options.appAddress || !options.signMessage) {\n throw new Error(\n \"Direct access-request authentication requires both `appAddress` and `signMessage`.\",\n );\n }\n\n const timestamp = String(options.now?.() ?? Date.now());\n const signature = await options.signMessage(\n buildDirectAccessRequestAuthMessage({ ...input, timestamp }),\n );\n\n return {\n \"X-Vana-App-Address\": options.appAddress,\n \"X-Vana-App-Signature\": signature,\n \"X-Vana-App-Timestamp\": timestamp,\n };\n}\n\n/**\n * Build an approval URL for a request id, matching the documented format\n * (`{app}/data-connection-requests/{requestId}?mode=page`).\n *\n * @param approvalBaseUrl - Base URL of the Vana approval app.\n * @param requestId - The `dcr_*` request id.\n * @returns The full approval URL.\n */\nexport function buildApprovalUrl(\n approvalBaseUrl: string,\n requestId: string,\n): string {\n return `${stripTrailingSlash(approvalBaseUrl)}/data-connection-requests/${encodeURIComponent(\n requestId,\n )}?mode=page`;\n}\n\n/**\n * Create the default {@link AccessRequestClient} for the Vana Account\n * access-request API.\n *\n * @param options - Base URLs and an optional `fetch` implementation.\n * @returns An {@link AccessRequestClient} backed by HTTP calls.\n */\nexport function createDefaultAccessRequestClient(\n options: DefaultAccessRequestClientOptions,\n): AccessRequestClient {\n const fetchFn = options.fetchFn ?? (globalThis.fetch as FetchLike);\n if (!fetchFn) {\n throw new Error(\n \"No fetch implementation available. Pass `fetchFn` to createDefaultAccessRequestClient.\",\n );\n }\n const base = stripTrailingSlash(options.baseUrl);\n\n return {\n async createAccessRequest(input): Promise<AccessRequest> {\n const path = \"/api/data-connection-requests\";\n // Every call is an independent logical create, so it gets its own key.\n // The client cannot tell two look-alike creates apart — one shared backend\n // controller serves many users with the same app, scopes, and returnUrl —\n // so deriving a key from the input would let the service deduplicate two\n // users onto a single DCR. Retrying an uncertain create is the caller's\n // decision: pass the same `idempotencyKey` back in.\n const idempotencyKey =\n input.idempotencyKey ??\n (options.createIdempotencyKey ?? defaultCreateIdempotencyKey)();\n const body = JSON.stringify({\n appAddress: input.appAddress,\n app: input.app,\n source: input.source,\n scopes: input.scopes,\n returnUrl: input.returnUrl,\n network: input.network,\n ...(input.foregroundDelivery !== undefined\n ? { foregroundDelivery: input.foregroundDelivery }\n : {}),\n idempotencyKey,\n });\n const res = await fetchFn(`${base}${path}`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...(await buildDirectAccessRequestHeaders(options, {\n body,\n method: \"POST\",\n path,\n })),\n },\n body,\n });\n if (!res.ok) {\n throw new Error(\n `Access request service error: ${res.status} ${res.statusText}`,\n );\n }\n const responseBody = (await res.json()) as {\n requestId?: string;\n id?: string;\n approvalUrl?: string;\n appAddress?: string;\n network?: unknown;\n expiresAt?: unknown;\n mobileContinuationUrl?: unknown;\n };\n const requestId = responseBody.requestId ?? responseBody.id;\n if (!requestId) {\n throw new Error(\"Access request service returned no requestId\");\n }\n return {\n requestId,\n approvalUrl:\n responseBody.approvalUrl ??\n buildApprovalUrl(options.approvalBaseUrl, requestId),\n appAddress: responseBody.appAddress ?? input.appAddress,\n network: normalizeNetwork(responseBody.network),\n expiresAt: normalizeExpiresAt(responseBody.expiresAt),\n mobileContinuationUrl: normalizeMobileContinuationUrl(\n responseBody.mobileContinuationUrl,\n options.env,\n ),\n };\n },\n\n async getAccessRequestStatus(\n requestId: string,\n ): Promise<AccessRequestStatus> {\n const path = `/api/data-connection-requests/${encodeURIComponent(requestId)}`;\n const res = await fetchFn(`${base}${path}`, {\n method: \"GET\",\n headers: await buildDirectAccessRequestHeaders(options, {\n body: \"\",\n method: \"GET\",\n path,\n }),\n });\n if (!res.ok) {\n throw new Error(\n `Access request service error: ${res.status} ${res.statusText}`,\n );\n }\n const body = (await res.json()) as {\n status?: string;\n personalServerUrl?: string;\n grantId?: string;\n scope?: string;\n mobileContinuationUrl?: unknown;\n scopes?: string[];\n };\n // `scopes` is the full approved set; `scope` is the first of them, kept\n // for callers (and deployments) that predate the array.\n const scopes =\n body.scopes && body.scopes.length > 0\n ? body.scopes\n : body.scope\n ? [body.scope]\n : undefined;\n return {\n status: normalizeStatus(body.status),\n personalServerUrl: body.personalServerUrl,\n grantId: body.grantId,\n scope: body.scope ?? scopes?.[0],\n scopes,\n mobileContinuationUrl: normalizeMobileContinuationUrl(\n body.mobileContinuationUrl,\n options.env,\n ),\n };\n },\n\n async acknowledgeRead(requestId: string): Promise<void> {\n const path = `/api/data-connection-requests/${encodeURIComponent(requestId)}/consumer-ack`;\n const res = await fetchFn(`${base}${path}`, {\n method: \"POST\",\n headers: await buildDirectAccessRequestHeaders(options, {\n body: \"\",\n method: \"POST\",\n path,\n }),\n });\n if (!res.ok) {\n throw new Error(\n `Access request ack service error: ${res.status} ${res.statusText}`,\n );\n }\n },\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/direct/access-request-client.ts"],"sourcesContent":["/**\n * Default client for the Vana Account access-request API.\n *\n * @remarks\n * Calls the Vana Account endpoints that issue `dcr_*` ids and approval URLs and\n * report request status. Inject a custom {@link AccessRequestClient} on the\n * controller to point at a different deployment; pass `fetchFn` to supply a test\n * double for the HTTP layer.\n *\n * @category Direct\n * @module direct/access-request-client\n */\n\nimport type {\n AccessRequest,\n AccessRequestClient,\n AccessRequestQuestion,\n AccessRequestStatus,\n AccessRequestStatusValue,\n DirectEnv,\n} from \"./types\";\nimport { normalizeMobileContinuationUrl } from \"./types\";\nimport type { Web3SignedSignFn } from \"../auth/web3-signed-builder\";\nimport { parseScope, type ParsedScope } from \"../protocol/scopes\";\nimport { DirectConfigError } from \"./errors\";\n\n/** Minimal `fetch` signature so the client is testable without a global fetch. */\nexport type FetchLike = (\n input: string,\n init?: {\n method?: string;\n headers?: Record<string, string>;\n body?: string;\n },\n) => Promise<{\n ok: boolean;\n status: number;\n statusText: string;\n json(): Promise<unknown>;\n text(): Promise<string>;\n}>;\n\n/** Options for {@link createDefaultAccessRequestClient}. */\nexport interface DefaultAccessRequestClientOptions {\n /** Base URL of the Vana Account access-request API. */\n baseUrl: string;\n /** Base URL the user is sent to for approval. */\n approvalBaseUrl: string;\n /**\n * Target environment. Pins the allowed mobile continuation link host\n * (`open.vana.org` for production, `open-dev.vana.org` for dev). When omitted,\n * both canonical hosts pass the structural continuation-URL check.\n */\n env?: DirectEnv;\n /** `fetch` implementation. Defaults to the global `fetch`. */\n fetchFn?: FetchLike;\n /** App identity address used for direct access-request authentication. */\n appAddress?: string;\n /** EIP-191 signer for direct access-request authentication. */\n signMessage?: Web3SignedSignFn;\n /** Clock source used for signed request timestamps. */\n now?: () => number;\n /**\n * Create the signed DCR idempotency key used when a create call omits one.\n * Called once per create. Injectable for deterministic tests.\n */\n createIdempotencyKey?: () => string;\n}\n\nconst VALID_STATUSES: readonly AccessRequestStatusValue[] = [\n \"pending\",\n \"approved\",\n \"ready_for_read\",\n \"completed\",\n \"denied\",\n \"expired\",\n];\n\nfunction normalizeStatus(value: unknown): AccessRequestStatusValue {\n return VALID_STATUSES.includes(value as AccessRequestStatusValue)\n ? (value as AccessRequestStatusValue)\n : \"pending\";\n}\n\nfunction normalizeNetwork(value: unknown): AccessRequest[\"network\"] {\n return value === \"mainnet\" || value === \"moksha\" ? value : undefined;\n}\n\nfunction normalizeExpiresAt(value: unknown): string | undefined {\n return typeof value === \"string\" && Number.isFinite(Date.parse(value))\n ? value\n : undefined;\n}\n\nfunction defaultCreateIdempotencyKey(): string {\n if (typeof globalThis.crypto?.randomUUID !== \"function\") {\n throw new Error(\n \"Secure randomUUID is unavailable. Pass createIdempotencyKey to createDefaultAccessRequestClient.\",\n );\n }\n return globalThis.crypto.randomUUID();\n}\n\nfunction stripTrailingSlash(url: string): string {\n return url.replace(/\\/+$/, \"\");\n}\n\nconst DIRECT_ACCESS_REQUEST_MESSAGE_PREFIX = \"Vana Direct Access Request v1\";\n\ninterface DirectAccessRequestAuthInput {\n body: string;\n method: string;\n path: string;\n timestamp: string;\n}\n\nexport function buildDirectAccessRequestAuthMessage(\n input: DirectAccessRequestAuthInput,\n): string {\n return [\n DIRECT_ACCESS_REQUEST_MESSAGE_PREFIX,\n `method:${input.method.toUpperCase()}`,\n `path:${input.path}`,\n `timestamp:${input.timestamp}`,\n `body:${input.body}`,\n ].join(\"\\n\");\n}\n\nasync function buildDirectAccessRequestHeaders(\n options: DefaultAccessRequestClientOptions,\n input: Omit<DirectAccessRequestAuthInput, \"timestamp\">,\n): Promise<Record<string, string>> {\n if (!options.appAddress && !options.signMessage) {\n return {};\n }\n if (!options.appAddress || !options.signMessage) {\n throw new Error(\n \"Direct access-request authentication requires both `appAddress` and `signMessage`.\",\n );\n }\n\n const timestamp = String(options.now?.() ?? Date.now());\n const signature = await options.signMessage(\n buildDirectAccessRequestAuthMessage({ ...input, timestamp }),\n );\n\n return {\n \"X-Vana-App-Address\": options.appAddress,\n \"X-Vana-App-Signature\": signature,\n \"X-Vana-App-Timestamp\": timestamp,\n };\n}\n\n/**\n * Build an approval URL for a request id, matching the documented format\n * (`{app}/data-connection-requests/{requestId}?mode=page`).\n *\n * @param approvalBaseUrl - Base URL of the Vana approval app.\n * @param requestId - The `dcr_*` request id.\n * @returns The full approval URL.\n */\nexport function buildApprovalUrl(\n approvalBaseUrl: string,\n requestId: string,\n): string {\n return `${stripTrailingSlash(approvalBaseUrl)}/data-connection-requests/${encodeURIComponent(\n requestId,\n )}?mode=page`;\n}\n\n/** The `recompute` values the question contract defines today. */\nconst RECOMPUTE_VALUES: readonly string[] = [\"snapshot\", \"on-change\"];\n\nfunction parseConcreteScope(field: string, value: unknown): ParsedScope {\n if (typeof value !== \"string\") {\n throw new DirectConfigError(`${field} must be a string`, { field });\n }\n try {\n return parseScope(value);\n } catch {\n throw new DirectConfigError(\n `${field} \"${value}\" is not a concrete scope. Use {source}.{category}[.{subcategory}] with no wildcard and no operation prefix.`,\n { field, value },\n );\n }\n}\n\n/**\n * Validate the derivative questions on a create input against the request\n * scope entries.\n *\n * @remarks\n * Client-side mirror of the access-request service rules so builders fail\n * fast, before the create request is signed and sent; the service remains\n * authoritative. Rules: 1 to 4 questions; every `derivedScope` and every\n * `sourceScope` is a concrete scope (wildcards rejected); 1 to 16 source\n * scopes per question with no duplicates and none equal to the derived scope;\n * the first dot-segment of the derived scope differs from the first\n * dot-segment of every source scope; the derived scope appears verbatim in\n * `scopes` as a bare read entry; no two questions share a derived scope; the\n * question text is 1 to 4000 characters after trimming; `recompute`, when\n * present, is `\"snapshot\"` or `\"on-change\"`.\n *\n * @param questions - The `questions` array from the create input.\n * @param scopes - The request's grant scope entries, verbatim.\n * @throws {DirectConfigError} - When any rule is violated. The message names\n * the offending question index and field.\n */\nexport function validateAccessRequestQuestions(\n questions: readonly AccessRequestQuestion[],\n scopes: readonly string[],\n): void {\n if (questions.length === 0 || questions.length > 4) {\n throw new DirectConfigError(\n `questions must contain 1 to 4 entries when present, got ${questions.length}. Omit the field to send no questions.`,\n { count: questions.length },\n );\n }\n const seenDerived = new Set<string>();\n questions.forEach((question, index) => {\n const label = `questions[${index}]`;\n const derived = parseConcreteScope(\n `${label}.derivedScope`,\n question.derivedScope,\n );\n if (seenDerived.has(question.derivedScope)) {\n throw new DirectConfigError(\n `${label}.derivedScope \"${question.derivedScope}\" is already used by an earlier question. Each question must target its own derived scope.`,\n { derivedScope: question.derivedScope },\n );\n }\n seenDerived.add(question.derivedScope);\n // The bare entry (no operation prefix) is what makes the answer readable\n // by the app: `write:coach.weekly` alone would not grant the read back.\n if (!scopes.includes(question.derivedScope)) {\n throw new DirectConfigError(\n `${label}.derivedScope \"${question.derivedScope}\" must also appear in scopes as a bare read entry, so the app can read the answer it asked for.`,\n { derivedScope: question.derivedScope, scopes: [...scopes] },\n );\n }\n if (\n question.sourceScopes.length === 0 ||\n question.sourceScopes.length > 16\n ) {\n throw new DirectConfigError(\n `${label}.sourceScopes must contain 1 to 16 entries, got ${question.sourceScopes.length}.`,\n { count: question.sourceScopes.length },\n );\n }\n const seenSources = new Set<string>();\n for (const sourceScope of question.sourceScopes) {\n const source = parseConcreteScope(`${label}.sourceScopes`, sourceScope);\n if (seenSources.has(sourceScope)) {\n throw new DirectConfigError(\n `${label}.sourceScopes contains \"${sourceScope}\" more than once. Deduplicate the source scopes.`,\n { sourceScope },\n );\n }\n seenSources.add(sourceScope);\n if (sourceScope === question.derivedScope) {\n throw new DirectConfigError(\n `${label}.sourceScopes must not contain the derived scope \"${question.derivedScope}\".`,\n { sourceScope },\n );\n }\n if (source.source === derived.source) {\n throw new DirectConfigError(\n `${label}.derivedScope \"${question.derivedScope}\" must not share its first dot-segment \"${derived.source}\" with source scope \"${sourceScope}\". Name the derived scope under the app's own namespace.`,\n { derivedScope: question.derivedScope, sourceScope },\n );\n }\n }\n if (typeof question.question !== \"string\") {\n throw new DirectConfigError(`${label}.question must be a string`, {\n field: `${label}.question`,\n });\n }\n const trimmedLength = question.question.trim().length;\n if (trimmedLength === 0 || trimmedLength > 4000) {\n throw new DirectConfigError(\n `${label}.question must be 1 to 4000 characters after trimming, got ${trimmedLength}.`,\n { length: trimmedLength },\n );\n }\n if (\n question.recompute !== undefined &&\n !RECOMPUTE_VALUES.includes(question.recompute)\n ) {\n throw new DirectConfigError(\n `${label}.recompute must be \"snapshot\" or \"on-change\" when present, got \"${String(question.recompute)}\".`,\n { recompute: question.recompute },\n );\n }\n });\n}\n\n/**\n * Create the default {@link AccessRequestClient} for the Vana Account\n * access-request API.\n *\n * @param options - Base URLs and an optional `fetch` implementation.\n * @returns An {@link AccessRequestClient} backed by HTTP calls.\n */\nexport function createDefaultAccessRequestClient(\n options: DefaultAccessRequestClientOptions,\n): AccessRequestClient {\n const fetchFn = options.fetchFn ?? (globalThis.fetch as FetchLike);\n if (!fetchFn) {\n throw new Error(\n \"No fetch implementation available. Pass `fetchFn` to createDefaultAccessRequestClient.\",\n );\n }\n const base = stripTrailingSlash(options.baseUrl);\n\n return {\n async createAccessRequest(input): Promise<AccessRequest> {\n if (input.questions !== undefined) {\n validateAccessRequestQuestions(input.questions, input.scopes);\n }\n const path = \"/api/data-connection-requests\";\n // Every call is an independent logical create, so it gets its own key.\n // The client cannot tell two look-alike creates apart — one shared backend\n // controller serves many users with the same app, scopes, and returnUrl —\n // so deriving a key from the input would let the service deduplicate two\n // users onto a single DCR. Retrying an uncertain create is the caller's\n // decision: pass the same `idempotencyKey` back in.\n const idempotencyKey =\n input.idempotencyKey ??\n (options.createIdempotencyKey ?? defaultCreateIdempotencyKey)();\n const body = JSON.stringify({\n appAddress: input.appAddress,\n app: input.app,\n source: input.source,\n scopes: input.scopes,\n returnUrl: input.returnUrl,\n network: input.network,\n ...(input.foregroundDelivery !== undefined\n ? { foregroundDelivery: input.foregroundDelivery }\n : {}),\n ...(input.questions !== undefined\n ? { questions: input.questions }\n : {}),\n idempotencyKey,\n });\n const res = await fetchFn(`${base}${path}`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...(await buildDirectAccessRequestHeaders(options, {\n body,\n method: \"POST\",\n path,\n })),\n },\n body,\n });\n if (!res.ok) {\n throw new Error(\n `Access request service error: ${res.status} ${res.statusText}`,\n );\n }\n const responseBody = (await res.json()) as {\n requestId?: string;\n id?: string;\n approvalUrl?: string;\n appAddress?: string;\n network?: unknown;\n expiresAt?: unknown;\n mobileContinuationUrl?: unknown;\n };\n const requestId = responseBody.requestId ?? responseBody.id;\n if (!requestId) {\n throw new Error(\"Access request service returned no requestId\");\n }\n return {\n requestId,\n approvalUrl:\n responseBody.approvalUrl ??\n buildApprovalUrl(options.approvalBaseUrl, requestId),\n appAddress: responseBody.appAddress ?? input.appAddress,\n network: normalizeNetwork(responseBody.network),\n expiresAt: normalizeExpiresAt(responseBody.expiresAt),\n mobileContinuationUrl: normalizeMobileContinuationUrl(\n responseBody.mobileContinuationUrl,\n options.env,\n ),\n };\n },\n\n async getAccessRequestStatus(\n requestId: string,\n ): Promise<AccessRequestStatus> {\n const path = `/api/data-connection-requests/${encodeURIComponent(requestId)}`;\n const res = await fetchFn(`${base}${path}`, {\n method: \"GET\",\n headers: await buildDirectAccessRequestHeaders(options, {\n body: \"\",\n method: \"GET\",\n path,\n }),\n });\n if (!res.ok) {\n throw new Error(\n `Access request service error: ${res.status} ${res.statusText}`,\n );\n }\n const body = (await res.json()) as {\n status?: string;\n personalServerUrl?: string;\n grantId?: string;\n scope?: string;\n mobileContinuationUrl?: unknown;\n scopes?: string[];\n };\n // `scopes` is the full approved set; `scope` is the first of them, kept\n // for callers (and deployments) that predate the array.\n const scopes =\n body.scopes && body.scopes.length > 0\n ? body.scopes\n : body.scope\n ? [body.scope]\n : undefined;\n return {\n status: normalizeStatus(body.status),\n personalServerUrl: body.personalServerUrl,\n grantId: body.grantId,\n scope: body.scope ?? scopes?.[0],\n scopes,\n mobileContinuationUrl: normalizeMobileContinuationUrl(\n body.mobileContinuationUrl,\n options.env,\n ),\n };\n },\n\n async acknowledgeRead(requestId: string): Promise<void> {\n const path = `/api/data-connection-requests/${encodeURIComponent(requestId)}/consumer-ack`;\n const res = await fetchFn(`${base}${path}`, {\n method: \"POST\",\n headers: await buildDirectAccessRequestHeaders(options, {\n body: \"\",\n method: \"POST\",\n path,\n }),\n });\n if (!res.ok) {\n throw new Error(\n `Access request ack service error: ${res.status} ${res.statusText}`,\n );\n }\n },\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -97,6 +97,9 @@ function createDirectDataController(config) {
|
|
|
97
97
|
};
|
|
98
98
|
},
|
|
99
99
|
async createAccessRequest(input) {
|
|
100
|
+
if (input.questions !== void 0) {
|
|
101
|
+
(0, import_access_request_client.validateAccessRequestQuestions)(input.questions, config.scopes);
|
|
102
|
+
}
|
|
100
103
|
return accessRequestClient.createAccessRequest({
|
|
101
104
|
appAddress: account.address,
|
|
102
105
|
app: config.app,
|
|
@@ -105,6 +108,7 @@ function createDirectDataController(config) {
|
|
|
105
108
|
returnUrl: input.returnUrl,
|
|
106
109
|
network,
|
|
107
110
|
...input.foregroundDelivery !== void 0 ? { foregroundDelivery: input.foregroundDelivery } : {},
|
|
111
|
+
...input.questions !== void 0 ? { questions: input.questions } : {},
|
|
108
112
|
...input.idempotencyKey !== void 0 ? { idempotencyKey: input.idempotencyKey } : {}
|
|
109
113
|
});
|
|
110
114
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/direct/controller.ts"],"sourcesContent":["/**\n * Direct Data Controller — the server-side facade for the two-tab Data\n * Portability flow.\n *\n * @remarks\n * One controller owns an app's private key, source, scopes, app identity, and\n * payment flow. It exposes the three methods the builder guide documents:\n *\n * - {@link DirectDataController.createAccessRequest} — start an approval request.\n * - {@link DirectDataController.getAccessRequestStatus} — poll while the Vana tab is open.\n * - {@link DirectDataController.readApprovedData} — read from the Personal Server,\n * handling 402 Payment Required.\n *\n * Access requests are created through the Vana Account access-request API; the\n * Personal Server read uses Web3Signed auth; and payment uses the DPv2 escrow\n * surface (`protocol/escrow`) — when a read returns `402`, the controller signs\n * a `GenericPayment` with the app key, settles it through the escrow gateway,\n * and retries.\n *\n * @category Direct\n * @module direct/controller\n */\n\nimport { privateKeyToAccount } from \"viem/accounts\";\nimport type { Hex } from \"viem\";\nimport type { Web3SignedSignFn } from \"../auth/web3-signed-builder\";\nimport { parseScope } from \"../protocol/scopes\";\nimport { parseScopeEntry } from \"../protocol/scope-actions\";\nimport { createEscrowGatewayClient } from \"../protocol/escrow\";\nimport { CONTRACTS } from \"../generated/addresses\";\nimport {\n createDefaultAccessRequestClient,\n type FetchLike,\n} from \"./access-request-client\";\nimport {\n getDirectDefaultNetwork,\n getDirectEndpoints,\n getDirectNetworkChainId,\n} from \"./endpoints\";\nimport {\n AccessNotApprovedError,\n DirectConfigError,\n ScopeNotApprovedError,\n} from \"./errors\";\nimport {\n type EscrowPaymentConfig,\n type SignTypedDataFn,\n} from \"./escrow-payment\";\nimport {\n readPersonalServerData,\n type PersonalServerFetch,\n type PersonalServerTransportRetryOptions,\n} from \"./personal-server-read\";\nimport type {\n AccessRequest,\n AccessRequestClient,\n AccessRequestStatus,\n AccessRequestStatusValue,\n ApprovedDataResult,\n AppIdentity,\n DirectAppConfig,\n DirectEnv,\n DirectNetwork,\n DirectPaymentResponseMetadata,\n DirectServiceEndpoints,\n ForegroundDelivery,\n MultiScopeDataResult,\n} from \"./types\";\n\n/** Configuration for {@link createDirectDataController}. */\nexport interface DirectDataControllerConfig {\n /** Target environment. Defaults to `\"production\"`. */\n env?: DirectEnv;\n /**\n * Target Vana network for chain-aware defaults. Defaults to the selected\n * environment's historical network (`mainnet` for production, `moksha` for\n * dev). Use `network: \"moksha\"` with the default production env for\n * production app/API URLs on testnet.\n */\n network?: DirectNetwork;\n /**\n * The app private key (`0x`-prefixed, 32 bytes). Server-side only — this key\n * is the app's on-chain identity and is never exposed to the browser.\n */\n appPrivateKey?: string;\n /**\n * @deprecated Use {@link DirectDataControllerConfig.appPrivateKey}. Accepted as\n * a backwards-compatible alias; if both are set, `appPrivateKey` wins.\n */\n builderPrivateKey?: string;\n /** App identity advertised during approval. */\n app: DirectAppConfig;\n /** Data source key (e.g. `\"icloud_notes\"`). */\n source: string;\n /**\n * Grant scope entries to request. At least one required.\n *\n * Each entry is `[operation:]scope` (see `parseScopeEntry`): a bare entry\n * such as `\"icloud_notes.notes\"` requests read, and `\"write:coach.weekly\"`\n * requests write. The entries are carried through to the access request\n * verbatim and become the grant's `scopes`, so a request can mix both\n * (`[\"oura.sleep\", \"coach.weekly\", \"write:coach.weekly\"]`).\n *\n * The scope part must be a concrete `{source}.{category}[.{subcategory}]`\n * scope: this flow reads approved scopes back one by one, so wildcard\n * patterns (`chatgpt.*`, `write:chatgpt.*`) are not accepted here for\n * either operation.\n */\n scopes: string[];\n /**\n * Override the resolved service endpoints (partial). Useful for pointing at a\n * non-standard deployment.\n */\n endpoints?: Partial<DirectServiceEndpoints>;\n /**\n * Client for the Vana Account access-request API. Defaults to a client against\n * the resolved Vana Account endpoints; inject your own to point at a custom\n * deployment or to supply a test double.\n */\n accessRequestClient?: AccessRequestClient;\n /**\n * Escrow settlement config used when a Personal Server read returns `402`.\n *\n * @remarks\n * Wires the DPv2 escrow gateway (`protocol/escrow`). The controller supplies\n * the EIP-712 `signTypedData` from the app key automatically.\n *\n * When omitted (or partially omitted), the SDK derives defaults from the\n * per-network endpoints table and the contract registry:\n * - `client` defaults to a gateway client at `endpoints.escrowGatewayUrl`\n * - `escrowContract` defaults to `CONTRACTS.DataPortabilityEscrow.addresses[chainId]`\n * - `chainId` defaults to the controller's resolved chain id\n *\n * Provide this field only to override a specific default.\n */\n escrow?: Partial<DirectEscrowConfig>;\n /** `fetch` used by the default access-request client. Defaults to `globalThis.fetch`. */\n fetchFn?: FetchLike;\n /** `fetch` used for the Personal Server read. Defaults to `globalThis.fetch`. */\n personalServerFetch?: PersonalServerFetch;\n /**\n * Transport-retry knobs for the Personal Server read\n * ({@link PersonalServerTransportRetryOptions}). Defaults to 3 attempts with\n * exponential backoff. Retries fire only when fetch throws (the browser-PS\n * relay reconnect window), never on a received HTTP status, and never\n * re-sign a payment.\n */\n personalServerTransportRetry?: PersonalServerTransportRetryOptions;\n}\n\n/**\n * Controller-level escrow config — the {@link EscrowPaymentConfig} minus the\n * `signTypedData` and `chainId` the controller injects itself.\n */\nexport interface DirectEscrowConfig extends Omit<\n EscrowPaymentConfig,\n \"signTypedData\" | \"chainId\"\n> {\n /**\n * Chain id for the EIP-712 domain. Defaults to the controller's environment\n * (1480 for mainnet, 14800 for moksha).\n */\n chainId?: number;\n}\n\n/**\n * Server-side controller for the direct Data Portability flow.\n *\n * @typeParam T - Shape of the data returned by {@link DirectDataController.readApprovedData}.\n */\nexport interface DirectDataController {\n /** The on-chain address of the app, derived from `appPrivateKey`. */\n readonly appAddress: string;\n\n /**\n * The app's on-chain address — the address to fund and inspect in the Builder\n * activity report. Equivalent to {@link DirectDataController.appAddress}.\n *\n * @returns The app's `0x`-prefixed address.\n */\n getAppAddress(): string;\n\n /**\n * The app's full identity: its configured id/name/homepage plus the derived\n * on-chain address. Useful for telling builders which app address to fund or\n * look up.\n *\n * @returns `{ id, name, homepageUrl, address }`.\n */\n getAppIdentity(): AppIdentity;\n\n /**\n * Create an access request the user can approve.\n *\n * @param input - The post-approval return URL and optional create retry key.\n * @returns The request id, HTTPS approval URL, and — for a pending deep Direct\n * request on mobile — an optional HTTPS `mobileContinuationUrl`.\n */\n createAccessRequest(input: {\n returnUrl: string;\n /** Optional foreground mobile delivery callback. */\n foregroundDelivery?: ForegroundDelivery;\n /**\n * Stable retry key when the caller retries after an uncertain response.\n * Each create without one gets its own generated key.\n */\n idempotencyKey?: string;\n }): Promise<AccessRequest>;\n\n /**\n * Fetch the current status of an access request.\n *\n * @param requestId - The `dcr_*` id from {@link DirectDataController.createAccessRequest}.\n * @returns `{ status, personalServerUrl?, grantId?, scope?, scopes? }`.\n */\n getAccessRequestStatus(requestId: string): Promise<AccessRequestStatus>;\n\n /**\n * Read the approved data from the user's Personal Server.\n *\n * @remarks\n * Resolves the request to its grant + Personal Server and performs a Web3Signed\n * read. Hides the `402 Payment Required` flow by default: if a read needs\n * payment, it signs the Personal Server's payment challenge, retries with\n * `X-PAYMENT`, and attaches shape-validated but unauthenticated\n * {@link DirectPaymentResponseMetadata} under `payment` when the Personal\n * Server returns it. After a successful read, the controller acknowledges\n * the DCR so Vana Web can close/redirect the approval tab.\n *\n * A request can approve several scopes. This reads **one** of them — `scope`\n * when given, otherwise the first approved scope. Use\n * {@link DirectDataController.readAllApprovedData} to read them all.\n *\n * Acknowledging moves the DCR to `completed`, which is terminal and no longer\n * read-ready. To read several scopes with your own loop, pass\n * `acknowledge: false` on every call but the last.\n *\n * @param input - The `dcr_*` request id, the optional `scope` to read, and an\n * optional `acknowledge` flag (default `true`).\n * @returns `{ scope, data, payment? }`.\n * @throws {@link AccessNotApprovedError} if the request is not approved.\n * @throws {@link ScopeNotApprovedError} if `scope` is not an approved scope.\n * @throws {@link PaymentRequiredError} if payment is required but unsettled.\n */\n readApprovedData<T = unknown>(input: {\n requestId: string;\n scope?: string;\n acknowledge?: boolean;\n }): Promise<ApprovedDataResult<T>>;\n\n /**\n * Read every scope the user approved on a request.\n *\n * @remarks\n * Reads the scopes in approval order, then acknowledges the DCR **once**,\n * after the last read — acknowledging earlier would move the request to\n * `completed` and make the remaining scopes unreadable.\n *\n * Each scope is a separate Personal Server read that settles its own\n * `data_access` fee from escrow, so reading N scopes costs N times a\n * single-scope read. The one-off registration fee is charged per grant, not\n * per scope.\n *\n * A scope that fails does not abort the rest: successes land in `results` and\n * failures in `errors`, because the fees for earlier scopes are already spent.\n * If any scope fails the request is left unacknowledged, so the scopes that\n * failed stay retryable — read them with `readApprovedData({ scope })` and\n * acknowledge on the last one.\n *\n * @param input - The `dcr_*` request id to read.\n * @returns `{ results, errors }`, both keyed by scope.\n * @throws {@link AccessNotApprovedError} if the request is not approved.\n */\n readAllApprovedData<T = unknown>(input: {\n requestId: string;\n }): Promise<MultiScopeDataResult<T>>;\n}\n\nfunction isHexPrivateKey(value: string): value is Hex {\n return /^0x[0-9a-fA-F]{64}$/.test(value);\n}\n\n// A DCR is read-ready only while the grant exists and the Personal Server is\n// still serving it: `approved` (durable PS) or `ready_for_read` (browser PS).\n// `completed` is terminal — the app already read and acknowledged, and the\n// browser PS may be gone — so it is deliberately excluded here.\nfunction isReadReadyStatus(status: AccessRequestStatusValue): boolean {\n return status === \"approved\" || status === \"ready_for_read\";\n}\n\n/**\n * Create a {@link DirectDataController}.\n *\n * @param config - Controller configuration (env, key, app identity, source, scopes).\n * @returns A ready-to-use controller.\n * @throws {@link DirectConfigError} when the key is missing or malformed, when\n * `scopes` is empty, or when no escrow contract can be resolved.\n * @throws InvalidScopeEntryError when a `scopes` entry does not fit the\n * `[operation:]scope` grammar (an unknown operation prefix such as `delete:`).\n * @throws ZodError when the scope part of an entry is not a valid scope.\n */\nexport function createDirectDataController(\n config: DirectDataControllerConfig,\n): DirectDataController {\n // `appPrivateKey` is the documented field; `builderPrivateKey` is a\n // deprecated alias kept for backwards compatibility.\n const privateKey = config.appPrivateKey ?? config.builderPrivateKey;\n if (!privateKey || !isHexPrivateKey(privateKey)) {\n throw new DirectConfigError(\n \"appPrivateKey must be a 0x-prefixed 32-byte hex string\",\n );\n }\n if (!config.scopes || config.scopes.length === 0) {\n throw new DirectConfigError(\"At least one scope is required\");\n }\n // Validate scopes eagerly so misconfiguration fails at construction. Each\n // element is a grant scope entry (`[operation:]scope`), so the operation\n // prefix is stripped first and only the scope part is checked against the\n // scope grammar — `write:coach.weekly` is a valid write-grant request, and\n // an unknown operation (`delete:x`) throws rather than being taken as read.\n // The entries themselves are passed through to the access request verbatim,\n // prefix included.\n for (const entry of config.scopes) {\n parseScope(parseScopeEntry(entry).scope);\n }\n\n const env: DirectEnv = config.env ?? \"production\";\n const network: DirectNetwork = config.network ?? getDirectDefaultNetwork(env);\n const defaultEndpoints = getDirectEndpoints(env);\n const chainId = config.endpoints?.chainId ?? getDirectNetworkChainId(network);\n const endpoints: DirectServiceEndpoints = {\n ...defaultEndpoints,\n ...config.endpoints,\n chainId,\n };\n\n const account = privateKeyToAccount(privateKey as Hex);\n const signMessage: Web3SignedSignFn = (message: string) =>\n account.signMessage({ message });\n // viem's account.signTypedData satisfies the structural SignTypedDataFn used\n // by the escrow GenericPayment signer.\n const signTypedData = account.signTypedData as unknown as SignTypedDataFn;\n const accessRequestClient: AccessRequestClient =\n config.accessRequestClient ??\n createDefaultAccessRequestClient({\n baseUrl: endpoints.accessRequestBaseUrl,\n approvalBaseUrl: endpoints.approvalAppBaseUrl,\n env,\n fetchFn: config.fetchFn,\n appAddress: account.address,\n signMessage,\n });\n\n // Build the escrow payment config, defaulting from the per-network endpoints\n // table and the contract registry when `config.escrow` is omitted or partial.\n const escrowChainId = config.escrow?.chainId ?? chainId;\n const defaultEscrowContract =\n CONTRACTS.DataPortabilityEscrow.addresses[\n escrowChainId as keyof typeof CONTRACTS.DataPortabilityEscrow.addresses\n ] ?? undefined;\n if (!config.escrow?.escrowContract && !defaultEscrowContract) {\n throw new DirectConfigError(\n `No DataPortabilityEscrow address found in the registry for chainId ${escrowChainId}. ` +\n `Provide an explicit escrow.escrowContract in the controller config.`,\n );\n }\n const escrow: EscrowPaymentConfig = {\n client:\n config.escrow?.client ??\n createEscrowGatewayClient(endpoints.escrowGatewayUrl),\n escrowContract:\n config.escrow?.escrowContract ?? (defaultEscrowContract as `0x${string}`),\n chainId: escrowChainId,\n nonceSource: config.escrow?.nonceSource,\n signTypedData,\n };\n\n return {\n appAddress: account.address,\n\n getAppAddress(): string {\n return account.address;\n },\n\n getAppIdentity(): AppIdentity {\n return {\n id: config.app.id,\n name: config.app.name,\n homepageUrl: config.app.homepageUrl,\n address: account.address,\n };\n },\n\n async createAccessRequest(input): Promise<AccessRequest> {\n return accessRequestClient.createAccessRequest({\n appAddress: account.address,\n app: config.app,\n source: config.source,\n scopes: config.scopes,\n returnUrl: input.returnUrl,\n network,\n ...(input.foregroundDelivery !== undefined\n ? { foregroundDelivery: input.foregroundDelivery }\n : {}),\n ...(input.idempotencyKey !== undefined\n ? { idempotencyKey: input.idempotencyKey }\n : {}),\n });\n },\n\n async getAccessRequestStatus(\n requestId: string,\n ): Promise<AccessRequestStatus> {\n return accessRequestClient.getAccessRequestStatus(requestId);\n },\n\n async readApprovedData<T = unknown>(input: {\n requestId: string;\n scope?: string;\n acknowledge?: boolean;\n }): Promise<ApprovedDataResult<T>> {\n const status = await requireReadReady(input.requestId);\n const scope = resolveRequestedScope(status, input.scope);\n\n const result = await readScope<T>(status, scope);\n if (input.acknowledge !== false) {\n await acknowledgeQuietly(input.requestId);\n }\n return result;\n },\n\n async readAllApprovedData<T = unknown>(input: {\n requestId: string;\n }): Promise<MultiScopeDataResult<T>> {\n const status = await requireReadReady(input.requestId);\n const scopes = approvedScopes(status);\n\n const results: Record<string, ApprovedDataResult<T>> = {};\n const errors: Record<string, Error> = {};\n // Sequential, not parallel: each read settles its own escrow payment and\n // the default nonce source is process-local, so concurrent reads would\n // race on the payment nonce.\n for (const scope of scopes) {\n try {\n results[scope] = await readScope<T>(status, scope);\n } catch (error) {\n errors[scope] =\n error instanceof Error ? error : new Error(String(error));\n }\n }\n\n // Acknowledge only after the last read, and only if every scope read —\n // acking moves the DCR to `completed`, which is terminal and no longer\n // read-ready, so acking on a partial failure would make the scope that\n // failed impossible to retry.\n if (Object.keys(errors).length === 0) {\n await acknowledgeQuietly(input.requestId);\n }\n\n return { results, errors };\n },\n };\n\n async function requireReadReady(\n requestId: string,\n ): Promise<AccessRequestStatus> {\n const status = await accessRequestClient.getAccessRequestStatus(requestId);\n // `scope` and `scopes` are both optional on the public status type, and a\n // client may return either one — require at least one approved scope rather\n // than the singular field specifically.\n if (\n !isReadReadyStatus(status.status) ||\n !status.personalServerUrl ||\n !status.grantId ||\n approvedScopes(status).length === 0\n ) {\n throw new AccessNotApprovedError(\n \"Request is not approved or is missing grantId/scope/personalServerUrl\",\n {\n requestId,\n status: status.status,\n hasPersonalServerUrl: Boolean(status.personalServerUrl),\n hasGrantId: Boolean(status.grantId),\n hasScope: approvedScopes(status).length > 0,\n },\n );\n }\n return status;\n }\n\n /** Approved scopes in approval order, falling back to the single `scope`. */\n function approvedScopes(status: AccessRequestStatus): string[] {\n if (status.scopes && status.scopes.length > 0) return status.scopes;\n return status.scope ? [status.scope] : [];\n }\n\n /**\n * Resolve which scope to read. Rejects an unapproved scope up front so it\n * never reaches the Personal Server and never settles a fee.\n */\n function resolveRequestedScope(\n status: AccessRequestStatus,\n requested?: string,\n ): string {\n const scopes = approvedScopes(status);\n if (requested === undefined) return scopes[0];\n if (!scopes.includes(requested)) {\n throw new ScopeNotApprovedError(\n `Scope \"${requested}\" is not approved on this request`,\n { requestedScope: requested, approvedScopes: scopes },\n );\n }\n return requested;\n }\n\n async function readScope<T>(\n status: AccessRequestStatus,\n scope: string,\n ): Promise<ApprovedDataResult<T>> {\n const result = await readPersonalServerData({\n personalServerUrl: status.personalServerUrl as string,\n scope,\n grantId: status.grantId as string,\n payerAddress: account.address,\n signMessage,\n escrow,\n fetchFn: config.personalServerFetch,\n transportRetry: config.personalServerTransportRetry,\n });\n return { scope, data: result.data as T, payment: result.payment };\n }\n\n async function acknowledgeQuietly(requestId: string): Promise<void> {\n try {\n await accessRequestClient.acknowledgeRead?.(requestId);\n } catch {\n // The read already succeeded; ack only drives Vana Web completion UX.\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/direct/controller.ts"],"sourcesContent":["/**\n * Direct Data Controller — the server-side facade for the two-tab Data\n * Portability flow.\n *\n * @remarks\n * One controller owns an app's private key, source, scopes, app identity, and\n * payment flow. It exposes the three methods the builder guide documents:\n *\n * - {@link DirectDataController.createAccessRequest} — start an approval request.\n * - {@link DirectDataController.getAccessRequestStatus} — poll while the Vana tab is open.\n * - {@link DirectDataController.readApprovedData} — read from the Personal Server,\n * handling 402 Payment Required.\n *\n * Access requests are created through the Vana Account access-request API; the\n * Personal Server read uses Web3Signed auth; and payment uses the DPv2 escrow\n * surface (`protocol/escrow`) — when a read returns `402`, the controller signs\n * a `GenericPayment` with the app key, settles it through the escrow gateway,\n * and retries.\n *\n * @category Direct\n * @module direct/controller\n */\n\nimport { privateKeyToAccount } from \"viem/accounts\";\nimport type { Hex } from \"viem\";\nimport type { Web3SignedSignFn } from \"../auth/web3-signed-builder\";\nimport { parseScope } from \"../protocol/scopes\";\nimport { parseScopeEntry } from \"../protocol/scope-actions\";\nimport { createEscrowGatewayClient } from \"../protocol/escrow\";\nimport { CONTRACTS } from \"../generated/addresses\";\nimport {\n createDefaultAccessRequestClient,\n validateAccessRequestQuestions,\n type FetchLike,\n} from \"./access-request-client\";\nimport {\n getDirectDefaultNetwork,\n getDirectEndpoints,\n getDirectNetworkChainId,\n} from \"./endpoints\";\nimport {\n AccessNotApprovedError,\n DirectConfigError,\n ScopeNotApprovedError,\n} from \"./errors\";\nimport {\n type EscrowPaymentConfig,\n type SignTypedDataFn,\n} from \"./escrow-payment\";\nimport {\n readPersonalServerData,\n type PersonalServerFetch,\n type PersonalServerTransportRetryOptions,\n} from \"./personal-server-read\";\nimport type {\n AccessRequest,\n AccessRequestClient,\n AccessRequestQuestion,\n AccessRequestStatus,\n AccessRequestStatusValue,\n ApprovedDataResult,\n AppIdentity,\n DirectAppConfig,\n DirectEnv,\n DirectNetwork,\n DirectPaymentResponseMetadata,\n DirectServiceEndpoints,\n ForegroundDelivery,\n MultiScopeDataResult,\n} from \"./types\";\n\n/** Configuration for {@link createDirectDataController}. */\nexport interface DirectDataControllerConfig {\n /** Target environment. Defaults to `\"production\"`. */\n env?: DirectEnv;\n /**\n * Target Vana network for chain-aware defaults. Defaults to the selected\n * environment's historical network (`mainnet` for production, `moksha` for\n * dev). Use `network: \"moksha\"` with the default production env for\n * production app/API URLs on testnet.\n */\n network?: DirectNetwork;\n /**\n * The app private key (`0x`-prefixed, 32 bytes). Server-side only — this key\n * is the app's on-chain identity and is never exposed to the browser.\n */\n appPrivateKey?: string;\n /**\n * @deprecated Use {@link DirectDataControllerConfig.appPrivateKey}. Accepted as\n * a backwards-compatible alias; if both are set, `appPrivateKey` wins.\n */\n builderPrivateKey?: string;\n /** App identity advertised during approval. */\n app: DirectAppConfig;\n /** Data source key (e.g. `\"icloud_notes\"`). */\n source: string;\n /**\n * Grant scope entries to request. At least one required.\n *\n * Each entry is `[operation:]scope` (see `parseScopeEntry`): a bare entry\n * such as `\"icloud_notes.notes\"` requests read, and `\"write:coach.weekly\"`\n * requests write. The entries are carried through to the access request\n * verbatim and become the grant's `scopes`, so a request can mix both\n * (`[\"oura.sleep\", \"coach.weekly\", \"write:coach.weekly\"]`).\n *\n * The scope part must be a concrete `{source}.{category}[.{subcategory}]`\n * scope: this flow reads approved scopes back one by one, so wildcard\n * patterns (`chatgpt.*`, `write:chatgpt.*`) are not accepted here for\n * either operation.\n */\n scopes: string[];\n /**\n * Override the resolved service endpoints (partial). Useful for pointing at a\n * non-standard deployment.\n */\n endpoints?: Partial<DirectServiceEndpoints>;\n /**\n * Client for the Vana Account access-request API. Defaults to a client against\n * the resolved Vana Account endpoints; inject your own to point at a custom\n * deployment or to supply a test double.\n */\n accessRequestClient?: AccessRequestClient;\n /**\n * Escrow settlement config used when a Personal Server read returns `402`.\n *\n * @remarks\n * Wires the DPv2 escrow gateway (`protocol/escrow`). The controller supplies\n * the EIP-712 `signTypedData` from the app key automatically.\n *\n * When omitted (or partially omitted), the SDK derives defaults from the\n * per-network endpoints table and the contract registry:\n * - `client` defaults to a gateway client at `endpoints.escrowGatewayUrl`\n * - `escrowContract` defaults to `CONTRACTS.DataPortabilityEscrow.addresses[chainId]`\n * - `chainId` defaults to the controller's resolved chain id\n *\n * Provide this field only to override a specific default.\n */\n escrow?: Partial<DirectEscrowConfig>;\n /** `fetch` used by the default access-request client. Defaults to `globalThis.fetch`. */\n fetchFn?: FetchLike;\n /** `fetch` used for the Personal Server read. Defaults to `globalThis.fetch`. */\n personalServerFetch?: PersonalServerFetch;\n /**\n * Transport-retry knobs for the Personal Server read\n * ({@link PersonalServerTransportRetryOptions}). Defaults to 3 attempts with\n * exponential backoff. Retries fire only when fetch throws (the browser-PS\n * relay reconnect window), never on a received HTTP status, and never\n * re-sign a payment.\n */\n personalServerTransportRetry?: PersonalServerTransportRetryOptions;\n}\n\n/**\n * Controller-level escrow config — the {@link EscrowPaymentConfig} minus the\n * `signTypedData` and `chainId` the controller injects itself.\n */\nexport interface DirectEscrowConfig extends Omit<\n EscrowPaymentConfig,\n \"signTypedData\" | \"chainId\"\n> {\n /**\n * Chain id for the EIP-712 domain. Defaults to the controller's environment\n * (1480 for mainnet, 14800 for moksha).\n */\n chainId?: number;\n}\n\n/**\n * Server-side controller for the direct Data Portability flow.\n *\n * @typeParam T - Shape of the data returned by {@link DirectDataController.readApprovedData}.\n */\nexport interface DirectDataController {\n /** The on-chain address of the app, derived from `appPrivateKey`. */\n readonly appAddress: string;\n\n /**\n * The app's on-chain address — the address to fund and inspect in the Builder\n * activity report. Equivalent to {@link DirectDataController.appAddress}.\n *\n * @returns The app's `0x`-prefixed address.\n */\n getAppAddress(): string;\n\n /**\n * The app's full identity: its configured id/name/homepage plus the derived\n * on-chain address. Useful for telling builders which app address to fund or\n * look up.\n *\n * @returns `{ id, name, homepageUrl, address }`.\n */\n getAppIdentity(): AppIdentity;\n\n /**\n * Create an access request the user can approve.\n *\n * @param input - The post-approval return URL and optional create retry key.\n * @returns The request id, HTTPS approval URL, and — for a pending deep Direct\n * request on mobile — an optional HTTPS `mobileContinuationUrl`.\n */\n createAccessRequest(input: {\n returnUrl: string;\n /** Optional foreground mobile delivery callback. */\n foregroundDelivery?: ForegroundDelivery;\n /**\n * Derivative questions to carry on the request (1 to 4). Each question\n * asks the user's Personal Server to compute its `derivedScope` from\n * `sourceScopes` the app never reads; the derived scope must also appear\n * in the controller's `scopes` as a bare read entry. Validated eagerly\n * against the configured scopes before the request is sent. See\n * {@link AccessRequestQuestion}.\n */\n questions?: AccessRequestQuestion[];\n /**\n * Stable retry key when the caller retries after an uncertain response.\n * Each create without one gets its own generated key.\n */\n idempotencyKey?: string;\n }): Promise<AccessRequest>;\n\n /**\n * Fetch the current status of an access request.\n *\n * @param requestId - The `dcr_*` id from {@link DirectDataController.createAccessRequest}.\n * @returns `{ status, personalServerUrl?, grantId?, scope?, scopes? }`.\n */\n getAccessRequestStatus(requestId: string): Promise<AccessRequestStatus>;\n\n /**\n * Read the approved data from the user's Personal Server.\n *\n * @remarks\n * Resolves the request to its grant + Personal Server and performs a Web3Signed\n * read. Hides the `402 Payment Required` flow by default: if a read needs\n * payment, it signs the Personal Server's payment challenge, retries with\n * `X-PAYMENT`, and attaches shape-validated but unauthenticated\n * {@link DirectPaymentResponseMetadata} under `payment` when the Personal\n * Server returns it. After a successful read, the controller acknowledges\n * the DCR so Vana Web can close/redirect the approval tab.\n *\n * A request can approve several scopes. This reads **one** of them — `scope`\n * when given, otherwise the first approved scope. Use\n * {@link DirectDataController.readAllApprovedData} to read them all.\n *\n * Acknowledging moves the DCR to `completed`, which is terminal and no longer\n * read-ready. To read several scopes with your own loop, pass\n * `acknowledge: false` on every call but the last.\n *\n * @param input - The `dcr_*` request id, the optional `scope` to read, and an\n * optional `acknowledge` flag (default `true`).\n * @returns `{ scope, data, payment? }`.\n * @throws {@link AccessNotApprovedError} if the request is not approved.\n * @throws {@link ScopeNotApprovedError} if `scope` is not an approved scope.\n * @throws {@link PaymentRequiredError} if payment is required but unsettled.\n */\n readApprovedData<T = unknown>(input: {\n requestId: string;\n scope?: string;\n acknowledge?: boolean;\n }): Promise<ApprovedDataResult<T>>;\n\n /**\n * Read every scope the user approved on a request.\n *\n * @remarks\n * Reads the scopes in approval order, then acknowledges the DCR **once**,\n * after the last read — acknowledging earlier would move the request to\n * `completed` and make the remaining scopes unreadable.\n *\n * Each scope is a separate Personal Server read that settles its own\n * `data_access` fee from escrow, so reading N scopes costs N times a\n * single-scope read. The one-off registration fee is charged per grant, not\n * per scope.\n *\n * A scope that fails does not abort the rest: successes land in `results` and\n * failures in `errors`, because the fees for earlier scopes are already spent.\n * If any scope fails the request is left unacknowledged, so the scopes that\n * failed stay retryable — read them with `readApprovedData({ scope })` and\n * acknowledge on the last one.\n *\n * @param input - The `dcr_*` request id to read.\n * @returns `{ results, errors }`, both keyed by scope.\n * @throws {@link AccessNotApprovedError} if the request is not approved.\n */\n readAllApprovedData<T = unknown>(input: {\n requestId: string;\n }): Promise<MultiScopeDataResult<T>>;\n}\n\nfunction isHexPrivateKey(value: string): value is Hex {\n return /^0x[0-9a-fA-F]{64}$/.test(value);\n}\n\n// A DCR is read-ready only while the grant exists and the Personal Server is\n// still serving it: `approved` (durable PS) or `ready_for_read` (browser PS).\n// `completed` is terminal — the app already read and acknowledged, and the\n// browser PS may be gone — so it is deliberately excluded here.\nfunction isReadReadyStatus(status: AccessRequestStatusValue): boolean {\n return status === \"approved\" || status === \"ready_for_read\";\n}\n\n/**\n * Create a {@link DirectDataController}.\n *\n * @param config - Controller configuration (env, key, app identity, source, scopes).\n * @returns A ready-to-use controller.\n * @throws {@link DirectConfigError} when the key is missing or malformed, when\n * `scopes` is empty, or when no escrow contract can be resolved.\n * @throws InvalidScopeEntryError when a `scopes` entry does not fit the\n * `[operation:]scope` grammar (an unknown operation prefix such as `delete:`).\n * @throws ZodError when the scope part of an entry is not a valid scope.\n */\nexport function createDirectDataController(\n config: DirectDataControllerConfig,\n): DirectDataController {\n // `appPrivateKey` is the documented field; `builderPrivateKey` is a\n // deprecated alias kept for backwards compatibility.\n const privateKey = config.appPrivateKey ?? config.builderPrivateKey;\n if (!privateKey || !isHexPrivateKey(privateKey)) {\n throw new DirectConfigError(\n \"appPrivateKey must be a 0x-prefixed 32-byte hex string\",\n );\n }\n if (!config.scopes || config.scopes.length === 0) {\n throw new DirectConfigError(\"At least one scope is required\");\n }\n // Validate scopes eagerly so misconfiguration fails at construction. Each\n // element is a grant scope entry (`[operation:]scope`), so the operation\n // prefix is stripped first and only the scope part is checked against the\n // scope grammar — `write:coach.weekly` is a valid write-grant request, and\n // an unknown operation (`delete:x`) throws rather than being taken as read.\n // The entries themselves are passed through to the access request verbatim,\n // prefix included.\n for (const entry of config.scopes) {\n parseScope(parseScopeEntry(entry).scope);\n }\n\n const env: DirectEnv = config.env ?? \"production\";\n const network: DirectNetwork = config.network ?? getDirectDefaultNetwork(env);\n const defaultEndpoints = getDirectEndpoints(env);\n const chainId = config.endpoints?.chainId ?? getDirectNetworkChainId(network);\n const endpoints: DirectServiceEndpoints = {\n ...defaultEndpoints,\n ...config.endpoints,\n chainId,\n };\n\n const account = privateKeyToAccount(privateKey as Hex);\n const signMessage: Web3SignedSignFn = (message: string) =>\n account.signMessage({ message });\n // viem's account.signTypedData satisfies the structural SignTypedDataFn used\n // by the escrow GenericPayment signer.\n const signTypedData = account.signTypedData as unknown as SignTypedDataFn;\n const accessRequestClient: AccessRequestClient =\n config.accessRequestClient ??\n createDefaultAccessRequestClient({\n baseUrl: endpoints.accessRequestBaseUrl,\n approvalBaseUrl: endpoints.approvalAppBaseUrl,\n env,\n fetchFn: config.fetchFn,\n appAddress: account.address,\n signMessage,\n });\n\n // Build the escrow payment config, defaulting from the per-network endpoints\n // table and the contract registry when `config.escrow` is omitted or partial.\n const escrowChainId = config.escrow?.chainId ?? chainId;\n const defaultEscrowContract =\n CONTRACTS.DataPortabilityEscrow.addresses[\n escrowChainId as keyof typeof CONTRACTS.DataPortabilityEscrow.addresses\n ] ?? undefined;\n if (!config.escrow?.escrowContract && !defaultEscrowContract) {\n throw new DirectConfigError(\n `No DataPortabilityEscrow address found in the registry for chainId ${escrowChainId}. ` +\n `Provide an explicit escrow.escrowContract in the controller config.`,\n );\n }\n const escrow: EscrowPaymentConfig = {\n client:\n config.escrow?.client ??\n createEscrowGatewayClient(endpoints.escrowGatewayUrl),\n escrowContract:\n config.escrow?.escrowContract ?? (defaultEscrowContract as `0x${string}`),\n chainId: escrowChainId,\n nonceSource: config.escrow?.nonceSource,\n signTypedData,\n };\n\n return {\n appAddress: account.address,\n\n getAppAddress(): string {\n return account.address;\n },\n\n getAppIdentity(): AppIdentity {\n return {\n id: config.app.id,\n name: config.app.name,\n homepageUrl: config.app.homepageUrl,\n address: account.address,\n };\n },\n\n async createAccessRequest(input): Promise<AccessRequest> {\n // Fail fast even with an injected client: a malformed question would be\n // rejected by the access-request service anyway, after a signed round\n // trip.\n if (input.questions !== undefined) {\n validateAccessRequestQuestions(input.questions, config.scopes);\n }\n return accessRequestClient.createAccessRequest({\n appAddress: account.address,\n app: config.app,\n source: config.source,\n scopes: config.scopes,\n returnUrl: input.returnUrl,\n network,\n ...(input.foregroundDelivery !== undefined\n ? { foregroundDelivery: input.foregroundDelivery }\n : {}),\n ...(input.questions !== undefined\n ? { questions: input.questions }\n : {}),\n ...(input.idempotencyKey !== undefined\n ? { idempotencyKey: input.idempotencyKey }\n : {}),\n });\n },\n\n async getAccessRequestStatus(\n requestId: string,\n ): Promise<AccessRequestStatus> {\n return accessRequestClient.getAccessRequestStatus(requestId);\n },\n\n async readApprovedData<T = unknown>(input: {\n requestId: string;\n scope?: string;\n acknowledge?: boolean;\n }): Promise<ApprovedDataResult<T>> {\n const status = await requireReadReady(input.requestId);\n const scope = resolveRequestedScope(status, input.scope);\n\n const result = await readScope<T>(status, scope);\n if (input.acknowledge !== false) {\n await acknowledgeQuietly(input.requestId);\n }\n return result;\n },\n\n async readAllApprovedData<T = unknown>(input: {\n requestId: string;\n }): Promise<MultiScopeDataResult<T>> {\n const status = await requireReadReady(input.requestId);\n const scopes = approvedScopes(status);\n\n const results: Record<string, ApprovedDataResult<T>> = {};\n const errors: Record<string, Error> = {};\n // Sequential, not parallel: each read settles its own escrow payment and\n // the default nonce source is process-local, so concurrent reads would\n // race on the payment nonce.\n for (const scope of scopes) {\n try {\n results[scope] = await readScope<T>(status, scope);\n } catch (error) {\n errors[scope] =\n error instanceof Error ? error : new Error(String(error));\n }\n }\n\n // Acknowledge only after the last read, and only if every scope read —\n // acking moves the DCR to `completed`, which is terminal and no longer\n // read-ready, so acking on a partial failure would make the scope that\n // failed impossible to retry.\n if (Object.keys(errors).length === 0) {\n await acknowledgeQuietly(input.requestId);\n }\n\n return { results, errors };\n },\n };\n\n async function requireReadReady(\n requestId: string,\n ): Promise<AccessRequestStatus> {\n const status = await accessRequestClient.getAccessRequestStatus(requestId);\n // `scope` and `scopes` are both optional on the public status type, and a\n // client may return either one — require at least one approved scope rather\n // than the singular field specifically.\n if (\n !isReadReadyStatus(status.status) ||\n !status.personalServerUrl ||\n !status.grantId ||\n approvedScopes(status).length === 0\n ) {\n throw new AccessNotApprovedError(\n \"Request is not approved or is missing grantId/scope/personalServerUrl\",\n {\n requestId,\n status: status.status,\n hasPersonalServerUrl: Boolean(status.personalServerUrl),\n hasGrantId: Boolean(status.grantId),\n hasScope: approvedScopes(status).length > 0,\n },\n );\n }\n return status;\n }\n\n /** Approved scopes in approval order, falling back to the single `scope`. */\n function approvedScopes(status: AccessRequestStatus): string[] {\n if (status.scopes && status.scopes.length > 0) return status.scopes;\n return status.scope ? [status.scope] : [];\n }\n\n /**\n * Resolve which scope to read. Rejects an unapproved scope up front so it\n * never reaches the Personal Server and never settles a fee.\n */\n function resolveRequestedScope(\n status: AccessRequestStatus,\n requested?: string,\n ): string {\n const scopes = approvedScopes(status);\n if (requested === undefined) return scopes[0];\n if (!scopes.includes(requested)) {\n throw new ScopeNotApprovedError(\n `Scope \"${requested}\" is not approved on this request`,\n { requestedScope: requested, approvedScopes: scopes },\n );\n }\n return requested;\n }\n\n async function readScope<T>(\n status: AccessRequestStatus,\n scope: string,\n ): Promise<ApprovedDataResult<T>> {\n const result = await readPersonalServerData({\n personalServerUrl: status.personalServerUrl as string,\n scope,\n grantId: status.grantId as string,\n payerAddress: account.address,\n signMessage,\n escrow,\n fetchFn: config.personalServerFetch,\n transportRetry: config.personalServerTransportRetry,\n });\n return { scope, data: result.data as T, payment: result.payment };\n }\n\n async function acknowledgeQuietly(requestId: string): Promise<void> {\n try {\n await accessRequestClient.acknowledgeRead?.(requestId);\n } catch {\n // The read already succeeded; ack only drives Vana Web completion UX.\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
import { type FetchLike } from "./access-request-client.js";
|
|
24
24
|
import { type EscrowPaymentConfig } from "./escrow-payment.js";
|
|
25
25
|
import { type PersonalServerFetch, type PersonalServerTransportRetryOptions } from "./personal-server-read.js";
|
|
26
|
-
import type { AccessRequest, AccessRequestClient, AccessRequestStatus, ApprovedDataResult, AppIdentity, DirectAppConfig, DirectEnv, DirectNetwork, DirectServiceEndpoints, ForegroundDelivery, MultiScopeDataResult } from "./types.js";
|
|
26
|
+
import type { AccessRequest, AccessRequestClient, AccessRequestQuestion, AccessRequestStatus, ApprovedDataResult, AppIdentity, DirectAppConfig, DirectEnv, DirectNetwork, DirectServiceEndpoints, ForegroundDelivery, MultiScopeDataResult } from "./types.js";
|
|
27
27
|
/** Configuration for {@link createDirectDataController}. */
|
|
28
28
|
export interface DirectDataControllerConfig {
|
|
29
29
|
/** Target environment. Defaults to `"production"`. */
|
|
@@ -149,6 +149,15 @@ export interface DirectDataController {
|
|
|
149
149
|
returnUrl: string;
|
|
150
150
|
/** Optional foreground mobile delivery callback. */
|
|
151
151
|
foregroundDelivery?: ForegroundDelivery;
|
|
152
|
+
/**
|
|
153
|
+
* Derivative questions to carry on the request (1 to 4). Each question
|
|
154
|
+
* asks the user's Personal Server to compute its `derivedScope` from
|
|
155
|
+
* `sourceScopes` the app never reads; the derived scope must also appear
|
|
156
|
+
* in the controller's `scopes` as a bare read entry. Validated eagerly
|
|
157
|
+
* against the configured scopes before the request is sent. See
|
|
158
|
+
* {@link AccessRequestQuestion}.
|
|
159
|
+
*/
|
|
160
|
+
questions?: AccessRequestQuestion[];
|
|
152
161
|
/**
|
|
153
162
|
* Stable retry key when the caller retries after an uncertain response.
|
|
154
163
|
* Each create without one gets its own generated key.
|
|
@@ -4,7 +4,8 @@ import { parseScopeEntry } from "../protocol/scope-actions.js";
|
|
|
4
4
|
import { createEscrowGatewayClient } from "../protocol/escrow.js";
|
|
5
5
|
import { CONTRACTS } from "../generated/addresses.js";
|
|
6
6
|
import {
|
|
7
|
-
createDefaultAccessRequestClient
|
|
7
|
+
createDefaultAccessRequestClient,
|
|
8
|
+
validateAccessRequestQuestions
|
|
8
9
|
} from "./access-request-client.js";
|
|
9
10
|
import {
|
|
10
11
|
getDirectDefaultNetwork,
|
|
@@ -86,6 +87,9 @@ function createDirectDataController(config) {
|
|
|
86
87
|
};
|
|
87
88
|
},
|
|
88
89
|
async createAccessRequest(input) {
|
|
90
|
+
if (input.questions !== void 0) {
|
|
91
|
+
validateAccessRequestQuestions(input.questions, config.scopes);
|
|
92
|
+
}
|
|
89
93
|
return accessRequestClient.createAccessRequest({
|
|
90
94
|
appAddress: account.address,
|
|
91
95
|
app: config.app,
|
|
@@ -94,6 +98,7 @@ function createDirectDataController(config) {
|
|
|
94
98
|
returnUrl: input.returnUrl,
|
|
95
99
|
network,
|
|
96
100
|
...input.foregroundDelivery !== void 0 ? { foregroundDelivery: input.foregroundDelivery } : {},
|
|
101
|
+
...input.questions !== void 0 ? { questions: input.questions } : {},
|
|
97
102
|
...input.idempotencyKey !== void 0 ? { idempotencyKey: input.idempotencyKey } : {}
|
|
98
103
|
});
|
|
99
104
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/direct/controller.ts"],"sourcesContent":["/**\n * Direct Data Controller — the server-side facade for the two-tab Data\n * Portability flow.\n *\n * @remarks\n * One controller owns an app's private key, source, scopes, app identity, and\n * payment flow. It exposes the three methods the builder guide documents:\n *\n * - {@link DirectDataController.createAccessRequest} — start an approval request.\n * - {@link DirectDataController.getAccessRequestStatus} — poll while the Vana tab is open.\n * - {@link DirectDataController.readApprovedData} — read from the Personal Server,\n * handling 402 Payment Required.\n *\n * Access requests are created through the Vana Account access-request API; the\n * Personal Server read uses Web3Signed auth; and payment uses the DPv2 escrow\n * surface (`protocol/escrow`) — when a read returns `402`, the controller signs\n * a `GenericPayment` with the app key, settles it through the escrow gateway,\n * and retries.\n *\n * @category Direct\n * @module direct/controller\n */\n\nimport { privateKeyToAccount } from \"viem/accounts\";\nimport type { Hex } from \"viem\";\nimport type { Web3SignedSignFn } from \"../auth/web3-signed-builder\";\nimport { parseScope } from \"../protocol/scopes\";\nimport { parseScopeEntry } from \"../protocol/scope-actions\";\nimport { createEscrowGatewayClient } from \"../protocol/escrow\";\nimport { CONTRACTS } from \"../generated/addresses\";\nimport {\n createDefaultAccessRequestClient,\n type FetchLike,\n} from \"./access-request-client\";\nimport {\n getDirectDefaultNetwork,\n getDirectEndpoints,\n getDirectNetworkChainId,\n} from \"./endpoints\";\nimport {\n AccessNotApprovedError,\n DirectConfigError,\n ScopeNotApprovedError,\n} from \"./errors\";\nimport {\n type EscrowPaymentConfig,\n type SignTypedDataFn,\n} from \"./escrow-payment\";\nimport {\n readPersonalServerData,\n type PersonalServerFetch,\n type PersonalServerTransportRetryOptions,\n} from \"./personal-server-read\";\nimport type {\n AccessRequest,\n AccessRequestClient,\n AccessRequestStatus,\n AccessRequestStatusValue,\n ApprovedDataResult,\n AppIdentity,\n DirectAppConfig,\n DirectEnv,\n DirectNetwork,\n DirectPaymentResponseMetadata,\n DirectServiceEndpoints,\n ForegroundDelivery,\n MultiScopeDataResult,\n} from \"./types\";\n\n/** Configuration for {@link createDirectDataController}. */\nexport interface DirectDataControllerConfig {\n /** Target environment. Defaults to `\"production\"`. */\n env?: DirectEnv;\n /**\n * Target Vana network for chain-aware defaults. Defaults to the selected\n * environment's historical network (`mainnet` for production, `moksha` for\n * dev). Use `network: \"moksha\"` with the default production env for\n * production app/API URLs on testnet.\n */\n network?: DirectNetwork;\n /**\n * The app private key (`0x`-prefixed, 32 bytes). Server-side only — this key\n * is the app's on-chain identity and is never exposed to the browser.\n */\n appPrivateKey?: string;\n /**\n * @deprecated Use {@link DirectDataControllerConfig.appPrivateKey}. Accepted as\n * a backwards-compatible alias; if both are set, `appPrivateKey` wins.\n */\n builderPrivateKey?: string;\n /** App identity advertised during approval. */\n app: DirectAppConfig;\n /** Data source key (e.g. `\"icloud_notes\"`). */\n source: string;\n /**\n * Grant scope entries to request. At least one required.\n *\n * Each entry is `[operation:]scope` (see `parseScopeEntry`): a bare entry\n * such as `\"icloud_notes.notes\"` requests read, and `\"write:coach.weekly\"`\n * requests write. The entries are carried through to the access request\n * verbatim and become the grant's `scopes`, so a request can mix both\n * (`[\"oura.sleep\", \"coach.weekly\", \"write:coach.weekly\"]`).\n *\n * The scope part must be a concrete `{source}.{category}[.{subcategory}]`\n * scope: this flow reads approved scopes back one by one, so wildcard\n * patterns (`chatgpt.*`, `write:chatgpt.*`) are not accepted here for\n * either operation.\n */\n scopes: string[];\n /**\n * Override the resolved service endpoints (partial). Useful for pointing at a\n * non-standard deployment.\n */\n endpoints?: Partial<DirectServiceEndpoints>;\n /**\n * Client for the Vana Account access-request API. Defaults to a client against\n * the resolved Vana Account endpoints; inject your own to point at a custom\n * deployment or to supply a test double.\n */\n accessRequestClient?: AccessRequestClient;\n /**\n * Escrow settlement config used when a Personal Server read returns `402`.\n *\n * @remarks\n * Wires the DPv2 escrow gateway (`protocol/escrow`). The controller supplies\n * the EIP-712 `signTypedData` from the app key automatically.\n *\n * When omitted (or partially omitted), the SDK derives defaults from the\n * per-network endpoints table and the contract registry:\n * - `client` defaults to a gateway client at `endpoints.escrowGatewayUrl`\n * - `escrowContract` defaults to `CONTRACTS.DataPortabilityEscrow.addresses[chainId]`\n * - `chainId` defaults to the controller's resolved chain id\n *\n * Provide this field only to override a specific default.\n */\n escrow?: Partial<DirectEscrowConfig>;\n /** `fetch` used by the default access-request client. Defaults to `globalThis.fetch`. */\n fetchFn?: FetchLike;\n /** `fetch` used for the Personal Server read. Defaults to `globalThis.fetch`. */\n personalServerFetch?: PersonalServerFetch;\n /**\n * Transport-retry knobs for the Personal Server read\n * ({@link PersonalServerTransportRetryOptions}). Defaults to 3 attempts with\n * exponential backoff. Retries fire only when fetch throws (the browser-PS\n * relay reconnect window), never on a received HTTP status, and never\n * re-sign a payment.\n */\n personalServerTransportRetry?: PersonalServerTransportRetryOptions;\n}\n\n/**\n * Controller-level escrow config — the {@link EscrowPaymentConfig} minus the\n * `signTypedData` and `chainId` the controller injects itself.\n */\nexport interface DirectEscrowConfig extends Omit<\n EscrowPaymentConfig,\n \"signTypedData\" | \"chainId\"\n> {\n /**\n * Chain id for the EIP-712 domain. Defaults to the controller's environment\n * (1480 for mainnet, 14800 for moksha).\n */\n chainId?: number;\n}\n\n/**\n * Server-side controller for the direct Data Portability flow.\n *\n * @typeParam T - Shape of the data returned by {@link DirectDataController.readApprovedData}.\n */\nexport interface DirectDataController {\n /** The on-chain address of the app, derived from `appPrivateKey`. */\n readonly appAddress: string;\n\n /**\n * The app's on-chain address — the address to fund and inspect in the Builder\n * activity report. Equivalent to {@link DirectDataController.appAddress}.\n *\n * @returns The app's `0x`-prefixed address.\n */\n getAppAddress(): string;\n\n /**\n * The app's full identity: its configured id/name/homepage plus the derived\n * on-chain address. Useful for telling builders which app address to fund or\n * look up.\n *\n * @returns `{ id, name, homepageUrl, address }`.\n */\n getAppIdentity(): AppIdentity;\n\n /**\n * Create an access request the user can approve.\n *\n * @param input - The post-approval return URL and optional create retry key.\n * @returns The request id, HTTPS approval URL, and — for a pending deep Direct\n * request on mobile — an optional HTTPS `mobileContinuationUrl`.\n */\n createAccessRequest(input: {\n returnUrl: string;\n /** Optional foreground mobile delivery callback. */\n foregroundDelivery?: ForegroundDelivery;\n /**\n * Stable retry key when the caller retries after an uncertain response.\n * Each create without one gets its own generated key.\n */\n idempotencyKey?: string;\n }): Promise<AccessRequest>;\n\n /**\n * Fetch the current status of an access request.\n *\n * @param requestId - The `dcr_*` id from {@link DirectDataController.createAccessRequest}.\n * @returns `{ status, personalServerUrl?, grantId?, scope?, scopes? }`.\n */\n getAccessRequestStatus(requestId: string): Promise<AccessRequestStatus>;\n\n /**\n * Read the approved data from the user's Personal Server.\n *\n * @remarks\n * Resolves the request to its grant + Personal Server and performs a Web3Signed\n * read. Hides the `402 Payment Required` flow by default: if a read needs\n * payment, it signs the Personal Server's payment challenge, retries with\n * `X-PAYMENT`, and attaches shape-validated but unauthenticated\n * {@link DirectPaymentResponseMetadata} under `payment` when the Personal\n * Server returns it. After a successful read, the controller acknowledges\n * the DCR so Vana Web can close/redirect the approval tab.\n *\n * A request can approve several scopes. This reads **one** of them — `scope`\n * when given, otherwise the first approved scope. Use\n * {@link DirectDataController.readAllApprovedData} to read them all.\n *\n * Acknowledging moves the DCR to `completed`, which is terminal and no longer\n * read-ready. To read several scopes with your own loop, pass\n * `acknowledge: false` on every call but the last.\n *\n * @param input - The `dcr_*` request id, the optional `scope` to read, and an\n * optional `acknowledge` flag (default `true`).\n * @returns `{ scope, data, payment? }`.\n * @throws {@link AccessNotApprovedError} if the request is not approved.\n * @throws {@link ScopeNotApprovedError} if `scope` is not an approved scope.\n * @throws {@link PaymentRequiredError} if payment is required but unsettled.\n */\n readApprovedData<T = unknown>(input: {\n requestId: string;\n scope?: string;\n acknowledge?: boolean;\n }): Promise<ApprovedDataResult<T>>;\n\n /**\n * Read every scope the user approved on a request.\n *\n * @remarks\n * Reads the scopes in approval order, then acknowledges the DCR **once**,\n * after the last read — acknowledging earlier would move the request to\n * `completed` and make the remaining scopes unreadable.\n *\n * Each scope is a separate Personal Server read that settles its own\n * `data_access` fee from escrow, so reading N scopes costs N times a\n * single-scope read. The one-off registration fee is charged per grant, not\n * per scope.\n *\n * A scope that fails does not abort the rest: successes land in `results` and\n * failures in `errors`, because the fees for earlier scopes are already spent.\n * If any scope fails the request is left unacknowledged, so the scopes that\n * failed stay retryable — read them with `readApprovedData({ scope })` and\n * acknowledge on the last one.\n *\n * @param input - The `dcr_*` request id to read.\n * @returns `{ results, errors }`, both keyed by scope.\n * @throws {@link AccessNotApprovedError} if the request is not approved.\n */\n readAllApprovedData<T = unknown>(input: {\n requestId: string;\n }): Promise<MultiScopeDataResult<T>>;\n}\n\nfunction isHexPrivateKey(value: string): value is Hex {\n return /^0x[0-9a-fA-F]{64}$/.test(value);\n}\n\n// A DCR is read-ready only while the grant exists and the Personal Server is\n// still serving it: `approved` (durable PS) or `ready_for_read` (browser PS).\n// `completed` is terminal — the app already read and acknowledged, and the\n// browser PS may be gone — so it is deliberately excluded here.\nfunction isReadReadyStatus(status: AccessRequestStatusValue): boolean {\n return status === \"approved\" || status === \"ready_for_read\";\n}\n\n/**\n * Create a {@link DirectDataController}.\n *\n * @param config - Controller configuration (env, key, app identity, source, scopes).\n * @returns A ready-to-use controller.\n * @throws {@link DirectConfigError} when the key is missing or malformed, when\n * `scopes` is empty, or when no escrow contract can be resolved.\n * @throws InvalidScopeEntryError when a `scopes` entry does not fit the\n * `[operation:]scope` grammar (an unknown operation prefix such as `delete:`).\n * @throws ZodError when the scope part of an entry is not a valid scope.\n */\nexport function createDirectDataController(\n config: DirectDataControllerConfig,\n): DirectDataController {\n // `appPrivateKey` is the documented field; `builderPrivateKey` is a\n // deprecated alias kept for backwards compatibility.\n const privateKey = config.appPrivateKey ?? config.builderPrivateKey;\n if (!privateKey || !isHexPrivateKey(privateKey)) {\n throw new DirectConfigError(\n \"appPrivateKey must be a 0x-prefixed 32-byte hex string\",\n );\n }\n if (!config.scopes || config.scopes.length === 0) {\n throw new DirectConfigError(\"At least one scope is required\");\n }\n // Validate scopes eagerly so misconfiguration fails at construction. Each\n // element is a grant scope entry (`[operation:]scope`), so the operation\n // prefix is stripped first and only the scope part is checked against the\n // scope grammar — `write:coach.weekly` is a valid write-grant request, and\n // an unknown operation (`delete:x`) throws rather than being taken as read.\n // The entries themselves are passed through to the access request verbatim,\n // prefix included.\n for (const entry of config.scopes) {\n parseScope(parseScopeEntry(entry).scope);\n }\n\n const env: DirectEnv = config.env ?? \"production\";\n const network: DirectNetwork = config.network ?? getDirectDefaultNetwork(env);\n const defaultEndpoints = getDirectEndpoints(env);\n const chainId = config.endpoints?.chainId ?? getDirectNetworkChainId(network);\n const endpoints: DirectServiceEndpoints = {\n ...defaultEndpoints,\n ...config.endpoints,\n chainId,\n };\n\n const account = privateKeyToAccount(privateKey as Hex);\n const signMessage: Web3SignedSignFn = (message: string) =>\n account.signMessage({ message });\n // viem's account.signTypedData satisfies the structural SignTypedDataFn used\n // by the escrow GenericPayment signer.\n const signTypedData = account.signTypedData as unknown as SignTypedDataFn;\n const accessRequestClient: AccessRequestClient =\n config.accessRequestClient ??\n createDefaultAccessRequestClient({\n baseUrl: endpoints.accessRequestBaseUrl,\n approvalBaseUrl: endpoints.approvalAppBaseUrl,\n env,\n fetchFn: config.fetchFn,\n appAddress: account.address,\n signMessage,\n });\n\n // Build the escrow payment config, defaulting from the per-network endpoints\n // table and the contract registry when `config.escrow` is omitted or partial.\n const escrowChainId = config.escrow?.chainId ?? chainId;\n const defaultEscrowContract =\n CONTRACTS.DataPortabilityEscrow.addresses[\n escrowChainId as keyof typeof CONTRACTS.DataPortabilityEscrow.addresses\n ] ?? undefined;\n if (!config.escrow?.escrowContract && !defaultEscrowContract) {\n throw new DirectConfigError(\n `No DataPortabilityEscrow address found in the registry for chainId ${escrowChainId}. ` +\n `Provide an explicit escrow.escrowContract in the controller config.`,\n );\n }\n const escrow: EscrowPaymentConfig = {\n client:\n config.escrow?.client ??\n createEscrowGatewayClient(endpoints.escrowGatewayUrl),\n escrowContract:\n config.escrow?.escrowContract ?? (defaultEscrowContract as `0x${string}`),\n chainId: escrowChainId,\n nonceSource: config.escrow?.nonceSource,\n signTypedData,\n };\n\n return {\n appAddress: account.address,\n\n getAppAddress(): string {\n return account.address;\n },\n\n getAppIdentity(): AppIdentity {\n return {\n id: config.app.id,\n name: config.app.name,\n homepageUrl: config.app.homepageUrl,\n address: account.address,\n };\n },\n\n async createAccessRequest(input): Promise<AccessRequest> {\n return accessRequestClient.createAccessRequest({\n appAddress: account.address,\n app: config.app,\n source: config.source,\n scopes: config.scopes,\n returnUrl: input.returnUrl,\n network,\n ...(input.foregroundDelivery !== undefined\n ? { foregroundDelivery: input.foregroundDelivery }\n : {}),\n ...(input.idempotencyKey !== undefined\n ? { idempotencyKey: input.idempotencyKey }\n : {}),\n });\n },\n\n async getAccessRequestStatus(\n requestId: string,\n ): Promise<AccessRequestStatus> {\n return accessRequestClient.getAccessRequestStatus(requestId);\n },\n\n async readApprovedData<T = unknown>(input: {\n requestId: string;\n scope?: string;\n acknowledge?: boolean;\n }): Promise<ApprovedDataResult<T>> {\n const status = await requireReadReady(input.requestId);\n const scope = resolveRequestedScope(status, input.scope);\n\n const result = await readScope<T>(status, scope);\n if (input.acknowledge !== false) {\n await acknowledgeQuietly(input.requestId);\n }\n return result;\n },\n\n async readAllApprovedData<T = unknown>(input: {\n requestId: string;\n }): Promise<MultiScopeDataResult<T>> {\n const status = await requireReadReady(input.requestId);\n const scopes = approvedScopes(status);\n\n const results: Record<string, ApprovedDataResult<T>> = {};\n const errors: Record<string, Error> = {};\n // Sequential, not parallel: each read settles its own escrow payment and\n // the default nonce source is process-local, so concurrent reads would\n // race on the payment nonce.\n for (const scope of scopes) {\n try {\n results[scope] = await readScope<T>(status, scope);\n } catch (error) {\n errors[scope] =\n error instanceof Error ? error : new Error(String(error));\n }\n }\n\n // Acknowledge only after the last read, and only if every scope read —\n // acking moves the DCR to `completed`, which is terminal and no longer\n // read-ready, so acking on a partial failure would make the scope that\n // failed impossible to retry.\n if (Object.keys(errors).length === 0) {\n await acknowledgeQuietly(input.requestId);\n }\n\n return { results, errors };\n },\n };\n\n async function requireReadReady(\n requestId: string,\n ): Promise<AccessRequestStatus> {\n const status = await accessRequestClient.getAccessRequestStatus(requestId);\n // `scope` and `scopes` are both optional on the public status type, and a\n // client may return either one — require at least one approved scope rather\n // than the singular field specifically.\n if (\n !isReadReadyStatus(status.status) ||\n !status.personalServerUrl ||\n !status.grantId ||\n approvedScopes(status).length === 0\n ) {\n throw new AccessNotApprovedError(\n \"Request is not approved or is missing grantId/scope/personalServerUrl\",\n {\n requestId,\n status: status.status,\n hasPersonalServerUrl: Boolean(status.personalServerUrl),\n hasGrantId: Boolean(status.grantId),\n hasScope: approvedScopes(status).length > 0,\n },\n );\n }\n return status;\n }\n\n /** Approved scopes in approval order, falling back to the single `scope`. */\n function approvedScopes(status: AccessRequestStatus): string[] {\n if (status.scopes && status.scopes.length > 0) return status.scopes;\n return status.scope ? [status.scope] : [];\n }\n\n /**\n * Resolve which scope to read. Rejects an unapproved scope up front so it\n * never reaches the Personal Server and never settles a fee.\n */\n function resolveRequestedScope(\n status: AccessRequestStatus,\n requested?: string,\n ): string {\n const scopes = approvedScopes(status);\n if (requested === undefined) return scopes[0];\n if (!scopes.includes(requested)) {\n throw new ScopeNotApprovedError(\n `Scope \"${requested}\" is not approved on this request`,\n { requestedScope: requested, approvedScopes: scopes },\n );\n }\n return requested;\n }\n\n async function readScope<T>(\n status: AccessRequestStatus,\n scope: string,\n ): Promise<ApprovedDataResult<T>> {\n const result = await readPersonalServerData({\n personalServerUrl: status.personalServerUrl as string,\n scope,\n grantId: status.grantId as string,\n payerAddress: account.address,\n signMessage,\n escrow,\n fetchFn: config.personalServerFetch,\n transportRetry: config.personalServerTransportRetry,\n });\n return { scope, data: result.data as T, payment: result.payment };\n }\n\n async function acknowledgeQuietly(requestId: string): Promise<void> {\n try {\n await accessRequestClient.acknowledgeRead?.(requestId);\n } catch {\n // The read already succeeded; ack only drives Vana Web completion UX.\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/direct/controller.ts"],"sourcesContent":["/**\n * Direct Data Controller — the server-side facade for the two-tab Data\n * Portability flow.\n *\n * @remarks\n * One controller owns an app's private key, source, scopes, app identity, and\n * payment flow. It exposes the three methods the builder guide documents:\n *\n * - {@link DirectDataController.createAccessRequest} — start an approval request.\n * - {@link DirectDataController.getAccessRequestStatus} — poll while the Vana tab is open.\n * - {@link DirectDataController.readApprovedData} — read from the Personal Server,\n * handling 402 Payment Required.\n *\n * Access requests are created through the Vana Account access-request API; the\n * Personal Server read uses Web3Signed auth; and payment uses the DPv2 escrow\n * surface (`protocol/escrow`) — when a read returns `402`, the controller signs\n * a `GenericPayment` with the app key, settles it through the escrow gateway,\n * and retries.\n *\n * @category Direct\n * @module direct/controller\n */\n\nimport { privateKeyToAccount } from \"viem/accounts\";\nimport type { Hex } from \"viem\";\nimport type { Web3SignedSignFn } from \"../auth/web3-signed-builder\";\nimport { parseScope } from \"../protocol/scopes\";\nimport { parseScopeEntry } from \"../protocol/scope-actions\";\nimport { createEscrowGatewayClient } from \"../protocol/escrow\";\nimport { CONTRACTS } from \"../generated/addresses\";\nimport {\n createDefaultAccessRequestClient,\n validateAccessRequestQuestions,\n type FetchLike,\n} from \"./access-request-client\";\nimport {\n getDirectDefaultNetwork,\n getDirectEndpoints,\n getDirectNetworkChainId,\n} from \"./endpoints\";\nimport {\n AccessNotApprovedError,\n DirectConfigError,\n ScopeNotApprovedError,\n} from \"./errors\";\nimport {\n type EscrowPaymentConfig,\n type SignTypedDataFn,\n} from \"./escrow-payment\";\nimport {\n readPersonalServerData,\n type PersonalServerFetch,\n type PersonalServerTransportRetryOptions,\n} from \"./personal-server-read\";\nimport type {\n AccessRequest,\n AccessRequestClient,\n AccessRequestQuestion,\n AccessRequestStatus,\n AccessRequestStatusValue,\n ApprovedDataResult,\n AppIdentity,\n DirectAppConfig,\n DirectEnv,\n DirectNetwork,\n DirectPaymentResponseMetadata,\n DirectServiceEndpoints,\n ForegroundDelivery,\n MultiScopeDataResult,\n} from \"./types\";\n\n/** Configuration for {@link createDirectDataController}. */\nexport interface DirectDataControllerConfig {\n /** Target environment. Defaults to `\"production\"`. */\n env?: DirectEnv;\n /**\n * Target Vana network for chain-aware defaults. Defaults to the selected\n * environment's historical network (`mainnet` for production, `moksha` for\n * dev). Use `network: \"moksha\"` with the default production env for\n * production app/API URLs on testnet.\n */\n network?: DirectNetwork;\n /**\n * The app private key (`0x`-prefixed, 32 bytes). Server-side only — this key\n * is the app's on-chain identity and is never exposed to the browser.\n */\n appPrivateKey?: string;\n /**\n * @deprecated Use {@link DirectDataControllerConfig.appPrivateKey}. Accepted as\n * a backwards-compatible alias; if both are set, `appPrivateKey` wins.\n */\n builderPrivateKey?: string;\n /** App identity advertised during approval. */\n app: DirectAppConfig;\n /** Data source key (e.g. `\"icloud_notes\"`). */\n source: string;\n /**\n * Grant scope entries to request. At least one required.\n *\n * Each entry is `[operation:]scope` (see `parseScopeEntry`): a bare entry\n * such as `\"icloud_notes.notes\"` requests read, and `\"write:coach.weekly\"`\n * requests write. The entries are carried through to the access request\n * verbatim and become the grant's `scopes`, so a request can mix both\n * (`[\"oura.sleep\", \"coach.weekly\", \"write:coach.weekly\"]`).\n *\n * The scope part must be a concrete `{source}.{category}[.{subcategory}]`\n * scope: this flow reads approved scopes back one by one, so wildcard\n * patterns (`chatgpt.*`, `write:chatgpt.*`) are not accepted here for\n * either operation.\n */\n scopes: string[];\n /**\n * Override the resolved service endpoints (partial). Useful for pointing at a\n * non-standard deployment.\n */\n endpoints?: Partial<DirectServiceEndpoints>;\n /**\n * Client for the Vana Account access-request API. Defaults to a client against\n * the resolved Vana Account endpoints; inject your own to point at a custom\n * deployment or to supply a test double.\n */\n accessRequestClient?: AccessRequestClient;\n /**\n * Escrow settlement config used when a Personal Server read returns `402`.\n *\n * @remarks\n * Wires the DPv2 escrow gateway (`protocol/escrow`). The controller supplies\n * the EIP-712 `signTypedData` from the app key automatically.\n *\n * When omitted (or partially omitted), the SDK derives defaults from the\n * per-network endpoints table and the contract registry:\n * - `client` defaults to a gateway client at `endpoints.escrowGatewayUrl`\n * - `escrowContract` defaults to `CONTRACTS.DataPortabilityEscrow.addresses[chainId]`\n * - `chainId` defaults to the controller's resolved chain id\n *\n * Provide this field only to override a specific default.\n */\n escrow?: Partial<DirectEscrowConfig>;\n /** `fetch` used by the default access-request client. Defaults to `globalThis.fetch`. */\n fetchFn?: FetchLike;\n /** `fetch` used for the Personal Server read. Defaults to `globalThis.fetch`. */\n personalServerFetch?: PersonalServerFetch;\n /**\n * Transport-retry knobs for the Personal Server read\n * ({@link PersonalServerTransportRetryOptions}). Defaults to 3 attempts with\n * exponential backoff. Retries fire only when fetch throws (the browser-PS\n * relay reconnect window), never on a received HTTP status, and never\n * re-sign a payment.\n */\n personalServerTransportRetry?: PersonalServerTransportRetryOptions;\n}\n\n/**\n * Controller-level escrow config — the {@link EscrowPaymentConfig} minus the\n * `signTypedData` and `chainId` the controller injects itself.\n */\nexport interface DirectEscrowConfig extends Omit<\n EscrowPaymentConfig,\n \"signTypedData\" | \"chainId\"\n> {\n /**\n * Chain id for the EIP-712 domain. Defaults to the controller's environment\n * (1480 for mainnet, 14800 for moksha).\n */\n chainId?: number;\n}\n\n/**\n * Server-side controller for the direct Data Portability flow.\n *\n * @typeParam T - Shape of the data returned by {@link DirectDataController.readApprovedData}.\n */\nexport interface DirectDataController {\n /** The on-chain address of the app, derived from `appPrivateKey`. */\n readonly appAddress: string;\n\n /**\n * The app's on-chain address — the address to fund and inspect in the Builder\n * activity report. Equivalent to {@link DirectDataController.appAddress}.\n *\n * @returns The app's `0x`-prefixed address.\n */\n getAppAddress(): string;\n\n /**\n * The app's full identity: its configured id/name/homepage plus the derived\n * on-chain address. Useful for telling builders which app address to fund or\n * look up.\n *\n * @returns `{ id, name, homepageUrl, address }`.\n */\n getAppIdentity(): AppIdentity;\n\n /**\n * Create an access request the user can approve.\n *\n * @param input - The post-approval return URL and optional create retry key.\n * @returns The request id, HTTPS approval URL, and — for a pending deep Direct\n * request on mobile — an optional HTTPS `mobileContinuationUrl`.\n */\n createAccessRequest(input: {\n returnUrl: string;\n /** Optional foreground mobile delivery callback. */\n foregroundDelivery?: ForegroundDelivery;\n /**\n * Derivative questions to carry on the request (1 to 4). Each question\n * asks the user's Personal Server to compute its `derivedScope` from\n * `sourceScopes` the app never reads; the derived scope must also appear\n * in the controller's `scopes` as a bare read entry. Validated eagerly\n * against the configured scopes before the request is sent. See\n * {@link AccessRequestQuestion}.\n */\n questions?: AccessRequestQuestion[];\n /**\n * Stable retry key when the caller retries after an uncertain response.\n * Each create without one gets its own generated key.\n */\n idempotencyKey?: string;\n }): Promise<AccessRequest>;\n\n /**\n * Fetch the current status of an access request.\n *\n * @param requestId - The `dcr_*` id from {@link DirectDataController.createAccessRequest}.\n * @returns `{ status, personalServerUrl?, grantId?, scope?, scopes? }`.\n */\n getAccessRequestStatus(requestId: string): Promise<AccessRequestStatus>;\n\n /**\n * Read the approved data from the user's Personal Server.\n *\n * @remarks\n * Resolves the request to its grant + Personal Server and performs a Web3Signed\n * read. Hides the `402 Payment Required` flow by default: if a read needs\n * payment, it signs the Personal Server's payment challenge, retries with\n * `X-PAYMENT`, and attaches shape-validated but unauthenticated\n * {@link DirectPaymentResponseMetadata} under `payment` when the Personal\n * Server returns it. After a successful read, the controller acknowledges\n * the DCR so Vana Web can close/redirect the approval tab.\n *\n * A request can approve several scopes. This reads **one** of them — `scope`\n * when given, otherwise the first approved scope. Use\n * {@link DirectDataController.readAllApprovedData} to read them all.\n *\n * Acknowledging moves the DCR to `completed`, which is terminal and no longer\n * read-ready. To read several scopes with your own loop, pass\n * `acknowledge: false` on every call but the last.\n *\n * @param input - The `dcr_*` request id, the optional `scope` to read, and an\n * optional `acknowledge` flag (default `true`).\n * @returns `{ scope, data, payment? }`.\n * @throws {@link AccessNotApprovedError} if the request is not approved.\n * @throws {@link ScopeNotApprovedError} if `scope` is not an approved scope.\n * @throws {@link PaymentRequiredError} if payment is required but unsettled.\n */\n readApprovedData<T = unknown>(input: {\n requestId: string;\n scope?: string;\n acknowledge?: boolean;\n }): Promise<ApprovedDataResult<T>>;\n\n /**\n * Read every scope the user approved on a request.\n *\n * @remarks\n * Reads the scopes in approval order, then acknowledges the DCR **once**,\n * after the last read — acknowledging earlier would move the request to\n * `completed` and make the remaining scopes unreadable.\n *\n * Each scope is a separate Personal Server read that settles its own\n * `data_access` fee from escrow, so reading N scopes costs N times a\n * single-scope read. The one-off registration fee is charged per grant, not\n * per scope.\n *\n * A scope that fails does not abort the rest: successes land in `results` and\n * failures in `errors`, because the fees for earlier scopes are already spent.\n * If any scope fails the request is left unacknowledged, so the scopes that\n * failed stay retryable — read them with `readApprovedData({ scope })` and\n * acknowledge on the last one.\n *\n * @param input - The `dcr_*` request id to read.\n * @returns `{ results, errors }`, both keyed by scope.\n * @throws {@link AccessNotApprovedError} if the request is not approved.\n */\n readAllApprovedData<T = unknown>(input: {\n requestId: string;\n }): Promise<MultiScopeDataResult<T>>;\n}\n\nfunction isHexPrivateKey(value: string): value is Hex {\n return /^0x[0-9a-fA-F]{64}$/.test(value);\n}\n\n// A DCR is read-ready only while the grant exists and the Personal Server is\n// still serving it: `approved` (durable PS) or `ready_for_read` (browser PS).\n// `completed` is terminal — the app already read and acknowledged, and the\n// browser PS may be gone — so it is deliberately excluded here.\nfunction isReadReadyStatus(status: AccessRequestStatusValue): boolean {\n return status === \"approved\" || status === \"ready_for_read\";\n}\n\n/**\n * Create a {@link DirectDataController}.\n *\n * @param config - Controller configuration (env, key, app identity, source, scopes).\n * @returns A ready-to-use controller.\n * @throws {@link DirectConfigError} when the key is missing or malformed, when\n * `scopes` is empty, or when no escrow contract can be resolved.\n * @throws InvalidScopeEntryError when a `scopes` entry does not fit the\n * `[operation:]scope` grammar (an unknown operation prefix such as `delete:`).\n * @throws ZodError when the scope part of an entry is not a valid scope.\n */\nexport function createDirectDataController(\n config: DirectDataControllerConfig,\n): DirectDataController {\n // `appPrivateKey` is the documented field; `builderPrivateKey` is a\n // deprecated alias kept for backwards compatibility.\n const privateKey = config.appPrivateKey ?? config.builderPrivateKey;\n if (!privateKey || !isHexPrivateKey(privateKey)) {\n throw new DirectConfigError(\n \"appPrivateKey must be a 0x-prefixed 32-byte hex string\",\n );\n }\n if (!config.scopes || config.scopes.length === 0) {\n throw new DirectConfigError(\"At least one scope is required\");\n }\n // Validate scopes eagerly so misconfiguration fails at construction. Each\n // element is a grant scope entry (`[operation:]scope`), so the operation\n // prefix is stripped first and only the scope part is checked against the\n // scope grammar — `write:coach.weekly` is a valid write-grant request, and\n // an unknown operation (`delete:x`) throws rather than being taken as read.\n // The entries themselves are passed through to the access request verbatim,\n // prefix included.\n for (const entry of config.scopes) {\n parseScope(parseScopeEntry(entry).scope);\n }\n\n const env: DirectEnv = config.env ?? \"production\";\n const network: DirectNetwork = config.network ?? getDirectDefaultNetwork(env);\n const defaultEndpoints = getDirectEndpoints(env);\n const chainId = config.endpoints?.chainId ?? getDirectNetworkChainId(network);\n const endpoints: DirectServiceEndpoints = {\n ...defaultEndpoints,\n ...config.endpoints,\n chainId,\n };\n\n const account = privateKeyToAccount(privateKey as Hex);\n const signMessage: Web3SignedSignFn = (message: string) =>\n account.signMessage({ message });\n // viem's account.signTypedData satisfies the structural SignTypedDataFn used\n // by the escrow GenericPayment signer.\n const signTypedData = account.signTypedData as unknown as SignTypedDataFn;\n const accessRequestClient: AccessRequestClient =\n config.accessRequestClient ??\n createDefaultAccessRequestClient({\n baseUrl: endpoints.accessRequestBaseUrl,\n approvalBaseUrl: endpoints.approvalAppBaseUrl,\n env,\n fetchFn: config.fetchFn,\n appAddress: account.address,\n signMessage,\n });\n\n // Build the escrow payment config, defaulting from the per-network endpoints\n // table and the contract registry when `config.escrow` is omitted or partial.\n const escrowChainId = config.escrow?.chainId ?? chainId;\n const defaultEscrowContract =\n CONTRACTS.DataPortabilityEscrow.addresses[\n escrowChainId as keyof typeof CONTRACTS.DataPortabilityEscrow.addresses\n ] ?? undefined;\n if (!config.escrow?.escrowContract && !defaultEscrowContract) {\n throw new DirectConfigError(\n `No DataPortabilityEscrow address found in the registry for chainId ${escrowChainId}. ` +\n `Provide an explicit escrow.escrowContract in the controller config.`,\n );\n }\n const escrow: EscrowPaymentConfig = {\n client:\n config.escrow?.client ??\n createEscrowGatewayClient(endpoints.escrowGatewayUrl),\n escrowContract:\n config.escrow?.escrowContract ?? (defaultEscrowContract as `0x${string}`),\n chainId: escrowChainId,\n nonceSource: config.escrow?.nonceSource,\n signTypedData,\n };\n\n return {\n appAddress: account.address,\n\n getAppAddress(): string {\n return account.address;\n },\n\n getAppIdentity(): AppIdentity {\n return {\n id: config.app.id,\n name: config.app.name,\n homepageUrl: config.app.homepageUrl,\n address: account.address,\n };\n },\n\n async createAccessRequest(input): Promise<AccessRequest> {\n // Fail fast even with an injected client: a malformed question would be\n // rejected by the access-request service anyway, after a signed round\n // trip.\n if (input.questions !== undefined) {\n validateAccessRequestQuestions(input.questions, config.scopes);\n }\n return accessRequestClient.createAccessRequest({\n appAddress: account.address,\n app: config.app,\n source: config.source,\n scopes: config.scopes,\n returnUrl: input.returnUrl,\n network,\n ...(input.foregroundDelivery !== undefined\n ? { foregroundDelivery: input.foregroundDelivery }\n : {}),\n ...(input.questions !== undefined\n ? { questions: input.questions }\n : {}),\n ...(input.idempotencyKey !== undefined\n ? { idempotencyKey: input.idempotencyKey }\n : {}),\n });\n },\n\n async getAccessRequestStatus(\n requestId: string,\n ): Promise<AccessRequestStatus> {\n return accessRequestClient.getAccessRequestStatus(requestId);\n },\n\n async readApprovedData<T = unknown>(input: {\n requestId: string;\n scope?: string;\n acknowledge?: boolean;\n }): Promise<ApprovedDataResult<T>> {\n const status = await requireReadReady(input.requestId);\n const scope = resolveRequestedScope(status, input.scope);\n\n const result = await readScope<T>(status, scope);\n if (input.acknowledge !== false) {\n await acknowledgeQuietly(input.requestId);\n }\n return result;\n },\n\n async readAllApprovedData<T = unknown>(input: {\n requestId: string;\n }): Promise<MultiScopeDataResult<T>> {\n const status = await requireReadReady(input.requestId);\n const scopes = approvedScopes(status);\n\n const results: Record<string, ApprovedDataResult<T>> = {};\n const errors: Record<string, Error> = {};\n // Sequential, not parallel: each read settles its own escrow payment and\n // the default nonce source is process-local, so concurrent reads would\n // race on the payment nonce.\n for (const scope of scopes) {\n try {\n results[scope] = await readScope<T>(status, scope);\n } catch (error) {\n errors[scope] =\n error instanceof Error ? error : new Error(String(error));\n }\n }\n\n // Acknowledge only after the last read, and only if every scope read —\n // acking moves the DCR to `completed`, which is terminal and no longer\n // read-ready, so acking on a partial failure would make the scope that\n // failed impossible to retry.\n if (Object.keys(errors).length === 0) {\n await acknowledgeQuietly(input.requestId);\n }\n\n return { results, errors };\n },\n };\n\n async function requireReadReady(\n requestId: string,\n ): Promise<AccessRequestStatus> {\n const status = await accessRequestClient.getAccessRequestStatus(requestId);\n // `scope` and `scopes` are both optional on the public status type, and a\n // client may return either one — require at least one approved scope rather\n // than the singular field specifically.\n if (\n !isReadReadyStatus(status.status) ||\n !status.personalServerUrl ||\n !status.grantId ||\n approvedScopes(status).length === 0\n ) {\n throw new AccessNotApprovedError(\n \"Request is not approved or is missing grantId/scope/personalServerUrl\",\n {\n requestId,\n status: status.status,\n hasPersonalServerUrl: Boolean(status.personalServerUrl),\n hasGrantId: Boolean(status.grantId),\n hasScope: approvedScopes(status).length > 0,\n },\n );\n }\n return status;\n }\n\n /** Approved scopes in approval order, falling back to the single `scope`. */\n function approvedScopes(status: AccessRequestStatus): string[] {\n if (status.scopes && status.scopes.length > 0) return status.scopes;\n return status.scope ? [status.scope] : [];\n }\n\n /**\n * Resolve which scope to read. Rejects an unapproved scope up front so it\n * never reaches the Personal Server and never settles a fee.\n */\n function resolveRequestedScope(\n status: AccessRequestStatus,\n requested?: string,\n ): string {\n const scopes = approvedScopes(status);\n if (requested === undefined) return scopes[0];\n if (!scopes.includes(requested)) {\n throw new ScopeNotApprovedError(\n `Scope \"${requested}\" is not approved on this request`,\n { requestedScope: requested, approvedScopes: scopes },\n );\n }\n return requested;\n }\n\n async function readScope<T>(\n status: AccessRequestStatus,\n scope: string,\n ): Promise<ApprovedDataResult<T>> {\n const result = await readPersonalServerData({\n personalServerUrl: status.personalServerUrl as string,\n scope,\n grantId: status.grantId as string,\n payerAddress: account.address,\n signMessage,\n escrow,\n fetchFn: config.personalServerFetch,\n transportRetry: config.personalServerTransportRetry,\n });\n return { scope, data: result.data as T, payment: result.payment };\n }\n\n async function acknowledgeQuietly(requestId: string): Promise<void> {\n try {\n await accessRequestClient.acknowledgeRead?.(requestId);\n } catch {\n // The read already succeeded; ack only drives Vana Web completion UX.\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/direct/types.ts"],"sourcesContent":["import type { EscrowAccessRecord } from \"../protocol/escrow\";\nimport type { ProtocolNetwork } from \"../protocol/networks\";\n\n/**\n * Shared types for the Direct Data Controller and the browser connect helper.\n *\n * @remarks\n * These types describe the \"two-tab\" Data Portability flow documented in the\n * builder guide: a backend controller creates an access request, the browser\n * opens Vana for user approval, and the backend reads the approved data from\n * the user's Personal Server (handling 402 Payment Required).\n *\n * @category Direct\n * @module direct/types\n */\n\n/**\n * Target environment for a {@link DirectDataController}.\n *\n * - `\"production\"` — Vana mainnet stack (default service URLs).\n * - `\"dev\"` — Vana internal dev stack. Use only when testing against\n * Vana's dev infrastructure.\n */\nexport type DirectEnv = \"dev\" | \"production\";\n\n/**\n * Vana network used for chain-aware Direct defaults.\n *\n * - `\"mainnet\"` — Vana mainnet (`chainId` 1480).\n * - `\"moksha\"` — Moksha testnet (`chainId` 14800).\n */\nexport type DirectNetwork = ProtocolNetwork;\n\n/**\n * App identity advertised to users during approval and attributed in Builder\n * League activity reports.\n */\nexport interface DirectAppConfig {\n /** Stable, human-readable app id (e.g. `\"notes-lens\"`). */\n id: string;\n /** Display name shown to the user in the Vana approval UI. */\n name: string;\n /** Public homepage URL for the app. */\n homepageUrl: string;\n}\n\n/**\n * Resolved app identity: the configured {@link DirectAppConfig} plus the app's\n * derived on-chain address (the address to fund and inspect).\n */\nexport interface AppIdentity extends DirectAppConfig {\n /** The app's `0x`-prefixed on-chain address (derived from `appPrivateKey`). */\n address: string;\n}\n\n/** One-time HTTPS callback used to deliver a foreground mobile Direct read. */\nexport interface ForegroundDelivery {\n /** Fixed, same-origin consumer callback URL. */\n url: string;\n /** High-entropy bearer capability, generated and retained by the consumer. */\n token: string;\n}\n\n/**\n * Resolved service URLs and chain id for a given {@link DirectEnv}.\n *\n * @remarks\n * Centralizes the per-environment base URLs the controller talks to. Each can\n * be overridden via {@link DirectDataControllerConfig.endpoints} when pointing\n * at a non-standard deployment.\n */\nexport interface DirectServiceEndpoints {\n /** Vana chain id for this environment (1480 mainnet, 14800 moksha). */\n chainId: number;\n /** Base URL of the Vana Account access-request API that issues `dcr_*` ids. */\n accessRequestBaseUrl: string;\n /** Base URL users are sent to for approval (the Vana app). */\n approvalAppBaseUrl: string;\n /** Base URL of the DP RPC escrow gateway used to settle `402 Payment Required`. */\n escrowGatewayUrl: string;\n}\n\n/** Result of {@link DirectDataController.createAccessRequest}. */\nexport interface AccessRequest {\n /** Opaque request id (e.g. `\"dcr_123\"`). */\n requestId: string;\n /** URL the browser opens so the user can approve the requested scopes. */\n approvalUrl: string;\n /** On-chain address of the (registered or reused) app. */\n appAddress: string;\n /** Protocol network echoed by the access-request service. */\n network?: DirectNetwork;\n /** Authoritative ISO-8601 expiry for the access request. */\n expiresAt?: string;\n /**\n * HTTPS continuation URL for a deep Direct request on a mobile browser.\n *\n * @remarks\n * Present only for a server-classified deep Direct DCR while it remains\n * pending, and only when Mobile continuation is enabled. It is an ordinary\n * `https://open[-dev].vana.org/continue#<ticket>` link the mobile UI renders\n * as a primary \"Open Vana\" tap — iOS Universal Links / Android App Links\n * deliver it to Vana Mobile, and its web fallback recovers an absent app. The\n * SDK never launches it automatically and owns no persistence.\n */\n mobileContinuationUrl?: string;\n}\n\n/** Canonical mobile continuation link host per {@link DirectEnv}. */\nconst MOBILE_CONTINUATION_HOSTS: Record<DirectEnv, string> = {\n production: \"open.vana.org\",\n dev: \"open-dev.vana.org\",\n};\n\n/** URL-fragment-safe opaque ticket: no separators, query, or scheme chars. */\nconst MOBILE_CONTINUATION_TICKET = /^[A-Za-z0-9._~-]+$/;\n\n/**\n * @internal Strictly validate a mobile HTTPS continuation URL at the SDK\n * boundary.\n *\n * @remarks\n * Accepts only `https://open[-dev].vana.org/continue#<ticket>` with exactly one\n * well-formed opaque fragment ticket and no user info, port, or query. When\n * `env` is supplied only that environment's host is allowed; otherwise both\n * canonical hosts are accepted for structural (defense-in-depth) validation.\n *\n * @param value - The candidate URL from a create or status response.\n * @param env - Optional environment to pin the allowed host to.\n * @returns The canonical URL string, or `undefined` when it fails validation.\n */\nexport function normalizeMobileContinuationUrl(\n value: unknown,\n env?: DirectEnv,\n): string | undefined {\n if (typeof value !== \"string\" || value.length === 0) return undefined;\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n return undefined;\n }\n if (url.protocol !== \"https:\") return undefined;\n const allowedHosts = env\n ? [MOBILE_CONTINUATION_HOSTS[env]]\n : Object.values(MOBILE_CONTINUATION_HOSTS);\n if (!allowedHosts.includes(url.hostname)) return undefined;\n if (url.pathname !== \"/continue\") return undefined;\n if (url.username !== \"\" || url.password !== \"\") return undefined;\n if (url.port !== \"\") return undefined;\n if (url.search !== \"\") return undefined;\n const ticket = url.hash.startsWith(\"#\") ? url.hash.slice(1) : \"\";\n if (!MOBILE_CONTINUATION_TICKET.test(ticket)) return undefined;\n return url.toString();\n}\n\n/**\n * Lifecycle status of an access request.\n *\n * @remarks\n * - `\"pending\"` — created, awaiting user approval.\n * - `\"approved\"` / `\"ready_for_read\"` — the grant exists and the Personal\n * Server is reachable; the data is read-ready (see {@link DirectDataController.readApprovedData}).\n * - `\"completed\"` — the app has already read the data and acknowledged it, so\n * the DCR is terminal. A `\"completed\"` request is **not** read-ready — the\n * browser Personal Server may no longer be serving it.\n * - `\"denied\"` / `\"expired\"` — terminal, no data was delivered.\n */\nexport type AccessRequestStatusValue =\n | \"pending\"\n | \"approved\"\n | \"ready_for_read\"\n | \"completed\"\n | \"denied\"\n | \"expired\";\n\n/** Result of {@link DirectDataController.getAccessRequestStatus}. */\nexport interface AccessRequestStatus {\n /** Current lifecycle status of the request. */\n status: AccessRequestStatusValue;\n /** Personal Server base URL — present once data is ready to read. */\n personalServerUrl?: string;\n /** Grant id covering the approved scope — present once data is ready to read. */\n grantId?: string;\n /**\n * The first approved scope — present once data is ready to read.\n *\n * @remarks\n * Kept for backwards compatibility. A request can approve many scopes; read\n * {@link AccessRequestStatus.scopes} to see all of them.\n */\n scope?: string;\n /**\n * Fresh HTTPS mobile continuation URL, returned only while the deep Direct\n * DCR is still pending. Its embedded ticket may rotate between polls.\n */\n mobileContinuationUrl?: string;\n /**\n * Every scope the user approved on this request — present once data is ready\n * to read.\n *\n * @remarks\n * A grant is keyed by `(user, app)` and carries a list of scopes, so a single\n * approval can cover several. Against an older Vana Account deployment that\n * only returns `scope`, this falls back to `[scope]`.\n */\n scopes?: string[];\n}\n\n/** Result of {@link DirectDataController.readApprovedData}. */\nexport interface ApprovedDataResult<T = unknown> {\n /** The scope the data was read for. */\n scope: string;\n /** The decoded payload returned by the Personal Server. */\n data: T;\n /**\n * Shape-validated but unauthenticated payment metadata echoed by the\n * Personal Server. Use for display/debugging, not accounting proof.\n */\n payment?: DirectPaymentResponseMetadata;\n}\n\n/**\n * Result of {@link DirectDataController.readApprovedData} across every approved\n * scope.\n *\n * @remarks\n * Successes and failures are reported side by side rather than as a thrown\n * error, because each scope read settles its own fee: throwing on the third\n * scope would discard data the app has already paid for. Check `errors` before\n * treating the read as complete.\n */\nexport interface MultiScopeDataResult<T = unknown> {\n /** Scopes that read successfully, keyed by scope. */\n results: Record<string, ApprovedDataResult<T>>;\n /** Scopes that failed, keyed by scope. Empty when every scope read. */\n errors: Record<string, Error>;\n}\n\n/**\n * Client for the Vana Account access-request API — the service that turns a\n * registered app + scopes into a `dcr_*` id and approval URL.\n *\n * @remarks\n * The controller uses a default client against the Vana Account endpoints. You\n * can inject your own implementation to point at a custom deployment or to\n * supply a test double.\n */\nexport interface AccessRequestClient {\n /**\n * Create an access request for the given app + scopes.\n *\n * @param input - App identity, source, scopes, network, and the post-approval return URL.\n * @returns The created {@link AccessRequest}.\n */\n createAccessRequest(input: {\n appAddress: string;\n app: DirectAppConfig;\n source: string;\n scopes: string[];\n returnUrl: string;\n /** Vana protocol network for this request (`\"mainnet\"` or `\"moksha\"`). */\n network: DirectNetwork;\n /** Optional foreground mobile delivery callback. */\n foregroundDelivery?: ForegroundDelivery;\n /**\n * Optional retry key. The default client generates a fresh key per call\n * when omitted; pass a stable key to retry a create whose response was\n * lost without risking a duplicate DCR.\n */\n idempotencyKey?: string;\n }): Promise<AccessRequest>;\n\n /**\n * Fetch the current status of a previously created access request.\n *\n * @param requestId - The `dcr_*` id returned by {@link AccessRequestClient.createAccessRequest}.\n * @returns The current {@link AccessRequestStatus}.\n */\n getAccessRequestStatus(requestId: string): Promise<AccessRequestStatus>;\n\n /**\n * Acknowledge that the app successfully read the approved data.\n *\n * @remarks\n * Direct Vana Web DCRs remain in `ready_for_read` while the browser Personal\n * Server is serving the app. After a successful Personal Server read, the\n * controller calls this hook so Vana Web can mark the request completed and\n * close/redirect the approval tab.\n *\n * Optional so injected clients from older SDK integrations keep compiling;\n * the default HTTP client implements it.\n */\n acknowledgeRead?(requestId: string): Promise<void>;\n}\n\n/**\n * Op-type vocabulary used by the DPv2 escrow payment surface.\n *\n * @remarks\n * These are the operations the gateway prices and settles via\n * `POST /v1/escrow/pay` (`opType` field of the `GenericPayment` message). A\n * direct data read settles the {@link DirectOpType.DataAccess} op for the\n * approved grant; the other op types are listed here for completeness and to\n * give builders a typed vocabulary when inspecting fee breakdowns.\n *\n * GenericPayment uses `\"grant\"` for legacy grant lifecycle payments and\n * `\"data_access\"` for standalone receipt-bound reads.\n */\nexport const DirectOpType = {\n GrantRegistration: \"grant_registration\",\n DataAccess: \"data_access\",\n DataRegistration: \"data_registration\",\n ServerRegistration: \"server_registration\",\n BuilderRegistration: \"builder_registration\",\n} as const;\n\n/** A direct-flow op type (see {@link DirectOpType}). */\nexport type DirectOpTypeValue =\n (typeof DirectOpType)[keyof typeof DirectOpType];\n\n/**\n * What a Personal Server `402 Payment Required` tells the controller is owed for\n * a data read.\n *\n * @remarks\n * The PS read 402 body identifies the challenged operation and amount/asset.\n * The controller settles it via the DPv2 escrow gateway (`/v1/escrow/pay`). The\n * full unmodified body is preserved under\n * {@link PersonalServerPaymentRequired.raw}.\n */\nexport interface PersonalServerPaymentRequired {\n /** Grant id authorizing the Personal Server read. */\n grantId: string;\n /** X402 network advertised by the Personal Server challenge. */\n network?: string;\n /** Payment nonce requested by the 402 challenge. */\n paymentNonce?: string;\n /** Data-access receipt carrying a signature for the gateway to verify. */\n accessRecord?: EscrowAccessRecord;\n /** Asset address owed (zero address = native VANA). */\n asset: string;\n /** Amount owed, as a decimal base-unit string (preserves uint256 precision). */\n amount: string;\n /** The full, unmodified 402 response body. */\n raw: unknown;\n}\n\n/** A validated legacy grant payment challenge. */\nexport interface PersonalServerGrantPaymentOperation extends PersonalServerPaymentRequired {\n /** Escrow operation discriminator. */\n opType: \"grant\";\n /** Grant id settled by the escrow payment. */\n opId: string;\n}\n\n/** A validated receipt-bound data-access payment challenge. */\nexport interface PersonalServerDataAccessPaymentOperation extends PersonalServerPaymentRequired {\n /** Escrow operation discriminator. */\n opType: \"data_access\";\n /** Access-record id settled by the escrow payment. */\n opId: string;\n /** Complete receipt whose signature is verified later by the gateway. */\n accessRecord: EscrowAccessRecord;\n /** Positive uint256 nonce supplied by the Personal Server challenge. */\n paymentNonce: string;\n}\n\n/**\n * A Personal Server payment challenge whose escrow operation has been\n * validated.\n *\n * @remarks\n * Validation here is structural and binds operation ids to their receipt. It\n * does not cryptographically verify the receipt signature; the Personal\n * Server and Data Gateway perform that verification.\n */\nexport type PersonalServerPaymentOperation =\n | PersonalServerGrantPaymentOperation\n | PersonalServerDataAccessPaymentOperation;\n\n/** Shape-validated payment response returned directly by the escrow gateway. */\nexport interface DirectPaymentReceipt {\n /** Op type settled (the gateway `opType`, e.g. `\"grant\"`). */\n opType: string;\n /** Op id settled (a grant id or access-record id). */\n opId: string;\n /** Asset paid in (zero address = native VANA). */\n asset: string;\n /** Total amount paid, as a decimal base-unit string. */\n amount: string;\n /** Payment nonce used for this settlement. */\n paymentNonce: string;\n /** Fee breakdown reported by the gateway (registration vs data-access fee). */\n breakdown: DirectFeeBreakdown;\n /** ISO timestamp the gateway recorded the payment. */\n paidAt: string;\n}\n\n/**\n * Untrusted payment response metadata echoed by a Personal Server.\n *\n * @remarks\n * The SDK validates every field before exposing this shape, but the response\n * header is not signed by the gateway. Use it for display and debugging only,\n * never as accounting proof that a payment occurred.\n */\nexport type DirectPaymentResponseMetadata = DirectPaymentReceipt;\n\n/**\n * Per-op fee breakdown reported by the gateway.\n *\n * @remarks\n * Mirrors the escrow {@link PaymentBreakdown}: a one-time registration fee plus\n * the per-read data-access fee, and whether this settlement covered the\n * registration fee.\n */\nexport interface DirectFeeBreakdown {\n /** One-time registration fee for the op, as a decimal base-unit string. */\n registrationFee: string;\n /** Per-read data-access fee, as a decimal base-unit string. */\n dataAccessFee: string;\n /** True when this settlement paid the registration fee. */\n registrationPaid: boolean;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA6GA,MAAM,4BAAuD;AAAA,EAC3D,YAAY;AAAA,EACZ,KAAK;AACP;AAGA,MAAM,6BAA6B;AAgB5B,SAAS,+BACd,OACA,KACoB;AACpB,MAAI,OAAO,UAAU,YAAY,MAAM,WAAW,EAAG,QAAO;AAC5D,MAAI;AACJ,MAAI;AACF,UAAM,IAAI,IAAI,KAAK;AAAA,EACrB,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,IAAI,aAAa,SAAU,QAAO;AACtC,QAAM,eAAe,MACjB,CAAC,0BAA0B,GAAG,CAAC,IAC/B,OAAO,OAAO,yBAAyB;AAC3C,MAAI,CAAC,aAAa,SAAS,IAAI,QAAQ,EAAG,QAAO;AACjD,MAAI,IAAI,aAAa,YAAa,QAAO;AACzC,MAAI,IAAI,aAAa,MAAM,IAAI,aAAa,GAAI,QAAO;AACvD,MAAI,IAAI,SAAS,GAAI,QAAO;AAC5B,MAAI,IAAI,WAAW,GAAI,QAAO;AAC9B,QAAM,SAAS,IAAI,KAAK,WAAW,GAAG,IAAI,IAAI,KAAK,MAAM,CAAC,IAAI;AAC9D,MAAI,CAAC,2BAA2B,KAAK,MAAM,EAAG,QAAO;AACrD,SAAO,IAAI,SAAS;AACtB;AA2JO,MAAM,eAAe;AAAA,EAC1B,mBAAmB;AAAA,EACnB,YAAY;AAAA,EACZ,kBAAkB;AAAA,EAClB,oBAAoB;AAAA,EACpB,qBAAqB;AACvB;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/direct/types.ts"],"sourcesContent":["import type { EscrowAccessRecord } from \"../protocol/escrow\";\nimport type { ProtocolNetwork } from \"../protocol/networks\";\n\n/**\n * Shared types for the Direct Data Controller and the browser connect helper.\n *\n * @remarks\n * These types describe the \"two-tab\" Data Portability flow documented in the\n * builder guide: a backend controller creates an access request, the browser\n * opens Vana for user approval, and the backend reads the approved data from\n * the user's Personal Server (handling 402 Payment Required).\n *\n * @category Direct\n * @module direct/types\n */\n\n/**\n * Target environment for a {@link DirectDataController}.\n *\n * - `\"production\"` — Vana mainnet stack (default service URLs).\n * - `\"dev\"` — Vana internal dev stack. Use only when testing against\n * Vana's dev infrastructure.\n */\nexport type DirectEnv = \"dev\" | \"production\";\n\n/**\n * Vana network used for chain-aware Direct defaults.\n *\n * - `\"mainnet\"` — Vana mainnet (`chainId` 1480).\n * - `\"moksha\"` — Moksha testnet (`chainId` 14800).\n */\nexport type DirectNetwork = ProtocolNetwork;\n\n/**\n * App identity advertised to users during approval and attributed in Builder\n * League activity reports.\n */\nexport interface DirectAppConfig {\n /** Stable, human-readable app id (e.g. `\"notes-lens\"`). */\n id: string;\n /** Display name shown to the user in the Vana approval UI. */\n name: string;\n /** Public homepage URL for the app. */\n homepageUrl: string;\n}\n\n/**\n * Resolved app identity: the configured {@link DirectAppConfig} plus the app's\n * derived on-chain address (the address to fund and inspect).\n */\nexport interface AppIdentity extends DirectAppConfig {\n /** The app's `0x`-prefixed on-chain address (derived from `appPrivateKey`). */\n address: string;\n}\n\n/** One-time HTTPS callback used to deliver a foreground mobile Direct read. */\nexport interface ForegroundDelivery {\n /** Fixed, same-origin consumer callback URL. */\n url: string;\n /** High-entropy bearer capability, generated and retained by the consumer. */\n token: string;\n}\n\n/**\n * One derivative question carried on an access request.\n *\n * @remarks\n * A question asks the user's Personal Server to compute an answer scope\n * (`derivedScope`) from source scopes the app never reads. On approval, the\n * Vana app registers the question on the Personal Server as the owner and the\n * grant covers only the derived scope, so the raw sources stay private to the\n * user. The SDK validates each question before the create request is signed\n * (see `validateAccessRequestQuestions`); the access-request service remains\n * authoritative and re-validates server-side.\n */\nexport interface AccessRequestQuestion {\n /**\n * Concrete scope the computed answer is written to and read from (no\n * wildcards, no operation prefix). It must also appear verbatim in the\n * request `scopes` as a bare read entry, and its first dot-segment must\n * differ from the first dot-segment of every entry in\n * {@link AccessRequestQuestion.sourceScopes}.\n */\n derivedScope: string;\n /**\n * Concrete scopes the answer is computed from: 1 to 16 entries, no\n * duplicates, none equal to {@link AccessRequestQuestion.derivedScope}.\n * These scopes are never granted to the app.\n */\n sourceScopes: string[];\n /**\n * Natural-language question the Personal Server answers from the source\n * scopes. Must be 1 to 4000 characters after trimming.\n */\n question: string;\n /**\n * When the Personal Server recomputes the answer. `\"snapshot\"` computes\n * once at registration; `\"on-change\"` also recomputes when a source scope\n * changes. When omitted, the server-side default applies.\n */\n recompute?: \"snapshot\" | \"on-change\";\n}\n\n/**\n * Resolved service URLs and chain id for a given {@link DirectEnv}.\n *\n * @remarks\n * Centralizes the per-environment base URLs the controller talks to. Each can\n * be overridden via {@link DirectDataControllerConfig.endpoints} when pointing\n * at a non-standard deployment.\n */\nexport interface DirectServiceEndpoints {\n /** Vana chain id for this environment (1480 mainnet, 14800 moksha). */\n chainId: number;\n /** Base URL of the Vana Account access-request API that issues `dcr_*` ids. */\n accessRequestBaseUrl: string;\n /** Base URL users are sent to for approval (the Vana app). */\n approvalAppBaseUrl: string;\n /** Base URL of the DP RPC escrow gateway used to settle `402 Payment Required`. */\n escrowGatewayUrl: string;\n}\n\n/** Result of {@link DirectDataController.createAccessRequest}. */\nexport interface AccessRequest {\n /** Opaque request id (e.g. `\"dcr_123\"`). */\n requestId: string;\n /** URL the browser opens so the user can approve the requested scopes. */\n approvalUrl: string;\n /** On-chain address of the (registered or reused) app. */\n appAddress: string;\n /** Protocol network echoed by the access-request service. */\n network?: DirectNetwork;\n /** Authoritative ISO-8601 expiry for the access request. */\n expiresAt?: string;\n /**\n * HTTPS continuation URL for a deep Direct request on a mobile browser.\n *\n * @remarks\n * Present only for a server-classified deep Direct DCR while it remains\n * pending, and only when Mobile continuation is enabled. It is an ordinary\n * `https://open[-dev].vana.org/continue#<ticket>` link the mobile UI renders\n * as a primary \"Open Vana\" tap — iOS Universal Links / Android App Links\n * deliver it to Vana Mobile, and its web fallback recovers an absent app. The\n * SDK never launches it automatically and owns no persistence.\n */\n mobileContinuationUrl?: string;\n}\n\n/** Canonical mobile continuation link host per {@link DirectEnv}. */\nconst MOBILE_CONTINUATION_HOSTS: Record<DirectEnv, string> = {\n production: \"open.vana.org\",\n dev: \"open-dev.vana.org\",\n};\n\n/** URL-fragment-safe opaque ticket: no separators, query, or scheme chars. */\nconst MOBILE_CONTINUATION_TICKET = /^[A-Za-z0-9._~-]+$/;\n\n/**\n * @internal Strictly validate a mobile HTTPS continuation URL at the SDK\n * boundary.\n *\n * @remarks\n * Accepts only `https://open[-dev].vana.org/continue#<ticket>` with exactly one\n * well-formed opaque fragment ticket and no user info, port, or query. When\n * `env` is supplied only that environment's host is allowed; otherwise both\n * canonical hosts are accepted for structural (defense-in-depth) validation.\n *\n * @param value - The candidate URL from a create or status response.\n * @param env - Optional environment to pin the allowed host to.\n * @returns The canonical URL string, or `undefined` when it fails validation.\n */\nexport function normalizeMobileContinuationUrl(\n value: unknown,\n env?: DirectEnv,\n): string | undefined {\n if (typeof value !== \"string\" || value.length === 0) return undefined;\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n return undefined;\n }\n if (url.protocol !== \"https:\") return undefined;\n const allowedHosts = env\n ? [MOBILE_CONTINUATION_HOSTS[env]]\n : Object.values(MOBILE_CONTINUATION_HOSTS);\n if (!allowedHosts.includes(url.hostname)) return undefined;\n if (url.pathname !== \"/continue\") return undefined;\n if (url.username !== \"\" || url.password !== \"\") return undefined;\n if (url.port !== \"\") return undefined;\n if (url.search !== \"\") return undefined;\n const ticket = url.hash.startsWith(\"#\") ? url.hash.slice(1) : \"\";\n if (!MOBILE_CONTINUATION_TICKET.test(ticket)) return undefined;\n return url.toString();\n}\n\n/**\n * Lifecycle status of an access request.\n *\n * @remarks\n * - `\"pending\"` — created, awaiting user approval.\n * - `\"approved\"` / `\"ready_for_read\"` — the grant exists and the Personal\n * Server is reachable; the data is read-ready (see {@link DirectDataController.readApprovedData}).\n * - `\"completed\"` — the app has already read the data and acknowledged it, so\n * the DCR is terminal. A `\"completed\"` request is **not** read-ready — the\n * browser Personal Server may no longer be serving it.\n * - `\"denied\"` / `\"expired\"` — terminal, no data was delivered.\n */\nexport type AccessRequestStatusValue =\n | \"pending\"\n | \"approved\"\n | \"ready_for_read\"\n | \"completed\"\n | \"denied\"\n | \"expired\";\n\n/** Result of {@link DirectDataController.getAccessRequestStatus}. */\nexport interface AccessRequestStatus {\n /** Current lifecycle status of the request. */\n status: AccessRequestStatusValue;\n /** Personal Server base URL — present once data is ready to read. */\n personalServerUrl?: string;\n /** Grant id covering the approved scope — present once data is ready to read. */\n grantId?: string;\n /**\n * The first approved scope — present once data is ready to read.\n *\n * @remarks\n * Kept for backwards compatibility. A request can approve many scopes; read\n * {@link AccessRequestStatus.scopes} to see all of them.\n */\n scope?: string;\n /**\n * Fresh HTTPS mobile continuation URL, returned only while the deep Direct\n * DCR is still pending. Its embedded ticket may rotate between polls.\n */\n mobileContinuationUrl?: string;\n /**\n * Every scope the user approved on this request — present once data is ready\n * to read.\n *\n * @remarks\n * A grant is keyed by `(user, app)` and carries a list of scopes, so a single\n * approval can cover several. Against an older Vana Account deployment that\n * only returns `scope`, this falls back to `[scope]`.\n */\n scopes?: string[];\n}\n\n/** Result of {@link DirectDataController.readApprovedData}. */\nexport interface ApprovedDataResult<T = unknown> {\n /** The scope the data was read for. */\n scope: string;\n /** The decoded payload returned by the Personal Server. */\n data: T;\n /**\n * Shape-validated but unauthenticated payment metadata echoed by the\n * Personal Server. Use for display/debugging, not accounting proof.\n */\n payment?: DirectPaymentResponseMetadata;\n}\n\n/**\n * Result of {@link DirectDataController.readApprovedData} across every approved\n * scope.\n *\n * @remarks\n * Successes and failures are reported side by side rather than as a thrown\n * error, because each scope read settles its own fee: throwing on the third\n * scope would discard data the app has already paid for. Check `errors` before\n * treating the read as complete.\n */\nexport interface MultiScopeDataResult<T = unknown> {\n /** Scopes that read successfully, keyed by scope. */\n results: Record<string, ApprovedDataResult<T>>;\n /** Scopes that failed, keyed by scope. Empty when every scope read. */\n errors: Record<string, Error>;\n}\n\n/**\n * Client for the Vana Account access-request API — the service that turns a\n * registered app + scopes into a `dcr_*` id and approval URL.\n *\n * @remarks\n * The controller uses a default client against the Vana Account endpoints. You\n * can inject your own implementation to point at a custom deployment or to\n * supply a test double.\n */\nexport interface AccessRequestClient {\n /**\n * Create an access request for the given app + scopes.\n *\n * @param input - App identity, source, scopes, network, and the post-approval return URL.\n * @returns The created {@link AccessRequest}.\n */\n createAccessRequest(input: {\n appAddress: string;\n app: DirectAppConfig;\n source: string;\n scopes: string[];\n returnUrl: string;\n /** Vana protocol network for this request (`\"mainnet\"` or `\"moksha\"`). */\n network: DirectNetwork;\n /** Optional foreground mobile delivery callback. */\n foregroundDelivery?: ForegroundDelivery;\n /**\n * Derivative questions to carry on the request (1 to 4). When present,\n * the array is validated client-side and serialized into the signed\n * create body verbatim. See {@link AccessRequestQuestion}.\n */\n questions?: AccessRequestQuestion[];\n /**\n * Optional retry key. The default client generates a fresh key per call\n * when omitted; pass a stable key to retry a create whose response was\n * lost without risking a duplicate DCR.\n */\n idempotencyKey?: string;\n }): Promise<AccessRequest>;\n\n /**\n * Fetch the current status of a previously created access request.\n *\n * @param requestId - The `dcr_*` id returned by {@link AccessRequestClient.createAccessRequest}.\n * @returns The current {@link AccessRequestStatus}.\n */\n getAccessRequestStatus(requestId: string): Promise<AccessRequestStatus>;\n\n /**\n * Acknowledge that the app successfully read the approved data.\n *\n * @remarks\n * Direct Vana Web DCRs remain in `ready_for_read` while the browser Personal\n * Server is serving the app. After a successful Personal Server read, the\n * controller calls this hook so Vana Web can mark the request completed and\n * close/redirect the approval tab.\n *\n * Optional so injected clients from older SDK integrations keep compiling;\n * the default HTTP client implements it.\n */\n acknowledgeRead?(requestId: string): Promise<void>;\n}\n\n/**\n * Op-type vocabulary used by the DPv2 escrow payment surface.\n *\n * @remarks\n * These are the operations the gateway prices and settles via\n * `POST /v1/escrow/pay` (`opType` field of the `GenericPayment` message). A\n * direct data read settles the {@link DirectOpType.DataAccess} op for the\n * approved grant; the other op types are listed here for completeness and to\n * give builders a typed vocabulary when inspecting fee breakdowns.\n *\n * GenericPayment uses `\"grant\"` for legacy grant lifecycle payments and\n * `\"data_access\"` for standalone receipt-bound reads.\n */\nexport const DirectOpType = {\n GrantRegistration: \"grant_registration\",\n DataAccess: \"data_access\",\n DataRegistration: \"data_registration\",\n ServerRegistration: \"server_registration\",\n BuilderRegistration: \"builder_registration\",\n} as const;\n\n/** A direct-flow op type (see {@link DirectOpType}). */\nexport type DirectOpTypeValue =\n (typeof DirectOpType)[keyof typeof DirectOpType];\n\n/**\n * What a Personal Server `402 Payment Required` tells the controller is owed for\n * a data read.\n *\n * @remarks\n * The PS read 402 body identifies the challenged operation and amount/asset.\n * The controller settles it via the DPv2 escrow gateway (`/v1/escrow/pay`). The\n * full unmodified body is preserved under\n * {@link PersonalServerPaymentRequired.raw}.\n */\nexport interface PersonalServerPaymentRequired {\n /** Grant id authorizing the Personal Server read. */\n grantId: string;\n /** X402 network advertised by the Personal Server challenge. */\n network?: string;\n /** Payment nonce requested by the 402 challenge. */\n paymentNonce?: string;\n /** Data-access receipt carrying a signature for the gateway to verify. */\n accessRecord?: EscrowAccessRecord;\n /** Asset address owed (zero address = native VANA). */\n asset: string;\n /** Amount owed, as a decimal base-unit string (preserves uint256 precision). */\n amount: string;\n /** The full, unmodified 402 response body. */\n raw: unknown;\n}\n\n/** A validated legacy grant payment challenge. */\nexport interface PersonalServerGrantPaymentOperation extends PersonalServerPaymentRequired {\n /** Escrow operation discriminator. */\n opType: \"grant\";\n /** Grant id settled by the escrow payment. */\n opId: string;\n}\n\n/** A validated receipt-bound data-access payment challenge. */\nexport interface PersonalServerDataAccessPaymentOperation extends PersonalServerPaymentRequired {\n /** Escrow operation discriminator. */\n opType: \"data_access\";\n /** Access-record id settled by the escrow payment. */\n opId: string;\n /** Complete receipt whose signature is verified later by the gateway. */\n accessRecord: EscrowAccessRecord;\n /** Positive uint256 nonce supplied by the Personal Server challenge. */\n paymentNonce: string;\n}\n\n/**\n * A Personal Server payment challenge whose escrow operation has been\n * validated.\n *\n * @remarks\n * Validation here is structural and binds operation ids to their receipt. It\n * does not cryptographically verify the receipt signature; the Personal\n * Server and Data Gateway perform that verification.\n */\nexport type PersonalServerPaymentOperation =\n | PersonalServerGrantPaymentOperation\n | PersonalServerDataAccessPaymentOperation;\n\n/** Shape-validated payment response returned directly by the escrow gateway. */\nexport interface DirectPaymentReceipt {\n /** Op type settled (the gateway `opType`, e.g. `\"grant\"`). */\n opType: string;\n /** Op id settled (a grant id or access-record id). */\n opId: string;\n /** Asset paid in (zero address = native VANA). */\n asset: string;\n /** Total amount paid, as a decimal base-unit string. */\n amount: string;\n /** Payment nonce used for this settlement. */\n paymentNonce: string;\n /** Fee breakdown reported by the gateway (registration vs data-access fee). */\n breakdown: DirectFeeBreakdown;\n /** ISO timestamp the gateway recorded the payment. */\n paidAt: string;\n}\n\n/**\n * Untrusted payment response metadata echoed by a Personal Server.\n *\n * @remarks\n * The SDK validates every field before exposing this shape, but the response\n * header is not signed by the gateway. Use it for display and debugging only,\n * never as accounting proof that a payment occurred.\n */\nexport type DirectPaymentResponseMetadata = DirectPaymentReceipt;\n\n/**\n * Per-op fee breakdown reported by the gateway.\n *\n * @remarks\n * Mirrors the escrow {@link PaymentBreakdown}: a one-time registration fee plus\n * the per-read data-access fee, and whether this settlement covered the\n * registration fee.\n */\nexport interface DirectFeeBreakdown {\n /** One-time registration fee for the op, as a decimal base-unit string. */\n registrationFee: string;\n /** Per-read data-access fee, as a decimal base-unit string. */\n dataAccessFee: string;\n /** True when this settlement paid the registration fee. */\n registrationPaid: boolean;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAqJA,MAAM,4BAAuD;AAAA,EAC3D,YAAY;AAAA,EACZ,KAAK;AACP;AAGA,MAAM,6BAA6B;AAgB5B,SAAS,+BACd,OACA,KACoB;AACpB,MAAI,OAAO,UAAU,YAAY,MAAM,WAAW,EAAG,QAAO;AAC5D,MAAI;AACJ,MAAI;AACF,UAAM,IAAI,IAAI,KAAK;AAAA,EACrB,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,IAAI,aAAa,SAAU,QAAO;AACtC,QAAM,eAAe,MACjB,CAAC,0BAA0B,GAAG,CAAC,IAC/B,OAAO,OAAO,yBAAyB;AAC3C,MAAI,CAAC,aAAa,SAAS,IAAI,QAAQ,EAAG,QAAO;AACjD,MAAI,IAAI,aAAa,YAAa,QAAO;AACzC,MAAI,IAAI,aAAa,MAAM,IAAI,aAAa,GAAI,QAAO;AACvD,MAAI,IAAI,SAAS,GAAI,QAAO;AAC5B,MAAI,IAAI,WAAW,GAAI,QAAO;AAC9B,QAAM,SAAS,IAAI,KAAK,WAAW,GAAG,IAAI,IAAI,KAAK,MAAM,CAAC,IAAI;AAC9D,MAAI,CAAC,2BAA2B,KAAK,MAAM,EAAG,QAAO;AACrD,SAAO,IAAI,SAAS;AACtB;AAiKO,MAAM,eAAe;AAAA,EAC1B,mBAAmB;AAAA,EACnB,YAAY;AAAA,EACZ,kBAAkB;AAAA,EAClB,oBAAoB;AAAA,EACpB,qBAAqB;AACvB;","names":[]}
|
package/dist/direct/types.d.ts
CHANGED
|
@@ -54,6 +54,45 @@ export interface ForegroundDelivery {
|
|
|
54
54
|
/** High-entropy bearer capability, generated and retained by the consumer. */
|
|
55
55
|
token: string;
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* One derivative question carried on an access request.
|
|
59
|
+
*
|
|
60
|
+
* @remarks
|
|
61
|
+
* A question asks the user's Personal Server to compute an answer scope
|
|
62
|
+
* (`derivedScope`) from source scopes the app never reads. On approval, the
|
|
63
|
+
* Vana app registers the question on the Personal Server as the owner and the
|
|
64
|
+
* grant covers only the derived scope, so the raw sources stay private to the
|
|
65
|
+
* user. The SDK validates each question before the create request is signed
|
|
66
|
+
* (see `validateAccessRequestQuestions`); the access-request service remains
|
|
67
|
+
* authoritative and re-validates server-side.
|
|
68
|
+
*/
|
|
69
|
+
export interface AccessRequestQuestion {
|
|
70
|
+
/**
|
|
71
|
+
* Concrete scope the computed answer is written to and read from (no
|
|
72
|
+
* wildcards, no operation prefix). It must also appear verbatim in the
|
|
73
|
+
* request `scopes` as a bare read entry, and its first dot-segment must
|
|
74
|
+
* differ from the first dot-segment of every entry in
|
|
75
|
+
* {@link AccessRequestQuestion.sourceScopes}.
|
|
76
|
+
*/
|
|
77
|
+
derivedScope: string;
|
|
78
|
+
/**
|
|
79
|
+
* Concrete scopes the answer is computed from: 1 to 16 entries, no
|
|
80
|
+
* duplicates, none equal to {@link AccessRequestQuestion.derivedScope}.
|
|
81
|
+
* These scopes are never granted to the app.
|
|
82
|
+
*/
|
|
83
|
+
sourceScopes: string[];
|
|
84
|
+
/**
|
|
85
|
+
* Natural-language question the Personal Server answers from the source
|
|
86
|
+
* scopes. Must be 1 to 4000 characters after trimming.
|
|
87
|
+
*/
|
|
88
|
+
question: string;
|
|
89
|
+
/**
|
|
90
|
+
* When the Personal Server recomputes the answer. `"snapshot"` computes
|
|
91
|
+
* once at registration; `"on-change"` also recomputes when a source scope
|
|
92
|
+
* changes. When omitted, the server-side default applies.
|
|
93
|
+
*/
|
|
94
|
+
recompute?: "snapshot" | "on-change";
|
|
95
|
+
}
|
|
57
96
|
/**
|
|
58
97
|
* Resolved service URLs and chain id for a given {@link DirectEnv}.
|
|
59
98
|
*
|
|
@@ -211,6 +250,12 @@ export interface AccessRequestClient {
|
|
|
211
250
|
network: DirectNetwork;
|
|
212
251
|
/** Optional foreground mobile delivery callback. */
|
|
213
252
|
foregroundDelivery?: ForegroundDelivery;
|
|
253
|
+
/**
|
|
254
|
+
* Derivative questions to carry on the request (1 to 4). When present,
|
|
255
|
+
* the array is validated client-side and serialized into the signed
|
|
256
|
+
* create body verbatim. See {@link AccessRequestQuestion}.
|
|
257
|
+
*/
|
|
258
|
+
questions?: AccessRequestQuestion[];
|
|
214
259
|
/**
|
|
215
260
|
* Optional retry key. The default client generates a fresh key per call
|
|
216
261
|
* when omitted; pass a stable key to retry a create whose response was
|
package/dist/direct/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/direct/types.ts"],"sourcesContent":["import type { EscrowAccessRecord } from \"../protocol/escrow\";\nimport type { ProtocolNetwork } from \"../protocol/networks\";\n\n/**\n * Shared types for the Direct Data Controller and the browser connect helper.\n *\n * @remarks\n * These types describe the \"two-tab\" Data Portability flow documented in the\n * builder guide: a backend controller creates an access request, the browser\n * opens Vana for user approval, and the backend reads the approved data from\n * the user's Personal Server (handling 402 Payment Required).\n *\n * @category Direct\n * @module direct/types\n */\n\n/**\n * Target environment for a {@link DirectDataController}.\n *\n * - `\"production\"` — Vana mainnet stack (default service URLs).\n * - `\"dev\"` — Vana internal dev stack. Use only when testing against\n * Vana's dev infrastructure.\n */\nexport type DirectEnv = \"dev\" | \"production\";\n\n/**\n * Vana network used for chain-aware Direct defaults.\n *\n * - `\"mainnet\"` — Vana mainnet (`chainId` 1480).\n * - `\"moksha\"` — Moksha testnet (`chainId` 14800).\n */\nexport type DirectNetwork = ProtocolNetwork;\n\n/**\n * App identity advertised to users during approval and attributed in Builder\n * League activity reports.\n */\nexport interface DirectAppConfig {\n /** Stable, human-readable app id (e.g. `\"notes-lens\"`). */\n id: string;\n /** Display name shown to the user in the Vana approval UI. */\n name: string;\n /** Public homepage URL for the app. */\n homepageUrl: string;\n}\n\n/**\n * Resolved app identity: the configured {@link DirectAppConfig} plus the app's\n * derived on-chain address (the address to fund and inspect).\n */\nexport interface AppIdentity extends DirectAppConfig {\n /** The app's `0x`-prefixed on-chain address (derived from `appPrivateKey`). */\n address: string;\n}\n\n/** One-time HTTPS callback used to deliver a foreground mobile Direct read. */\nexport interface ForegroundDelivery {\n /** Fixed, same-origin consumer callback URL. */\n url: string;\n /** High-entropy bearer capability, generated and retained by the consumer. */\n token: string;\n}\n\n/**\n * Resolved service URLs and chain id for a given {@link DirectEnv}.\n *\n * @remarks\n * Centralizes the per-environment base URLs the controller talks to. Each can\n * be overridden via {@link DirectDataControllerConfig.endpoints} when pointing\n * at a non-standard deployment.\n */\nexport interface DirectServiceEndpoints {\n /** Vana chain id for this environment (1480 mainnet, 14800 moksha). */\n chainId: number;\n /** Base URL of the Vana Account access-request API that issues `dcr_*` ids. */\n accessRequestBaseUrl: string;\n /** Base URL users are sent to for approval (the Vana app). */\n approvalAppBaseUrl: string;\n /** Base URL of the DP RPC escrow gateway used to settle `402 Payment Required`. */\n escrowGatewayUrl: string;\n}\n\n/** Result of {@link DirectDataController.createAccessRequest}. */\nexport interface AccessRequest {\n /** Opaque request id (e.g. `\"dcr_123\"`). */\n requestId: string;\n /** URL the browser opens so the user can approve the requested scopes. */\n approvalUrl: string;\n /** On-chain address of the (registered or reused) app. */\n appAddress: string;\n /** Protocol network echoed by the access-request service. */\n network?: DirectNetwork;\n /** Authoritative ISO-8601 expiry for the access request. */\n expiresAt?: string;\n /**\n * HTTPS continuation URL for a deep Direct request on a mobile browser.\n *\n * @remarks\n * Present only for a server-classified deep Direct DCR while it remains\n * pending, and only when Mobile continuation is enabled. It is an ordinary\n * `https://open[-dev].vana.org/continue#<ticket>` link the mobile UI renders\n * as a primary \"Open Vana\" tap — iOS Universal Links / Android App Links\n * deliver it to Vana Mobile, and its web fallback recovers an absent app. The\n * SDK never launches it automatically and owns no persistence.\n */\n mobileContinuationUrl?: string;\n}\n\n/** Canonical mobile continuation link host per {@link DirectEnv}. */\nconst MOBILE_CONTINUATION_HOSTS: Record<DirectEnv, string> = {\n production: \"open.vana.org\",\n dev: \"open-dev.vana.org\",\n};\n\n/** URL-fragment-safe opaque ticket: no separators, query, or scheme chars. */\nconst MOBILE_CONTINUATION_TICKET = /^[A-Za-z0-9._~-]+$/;\n\n/**\n * @internal Strictly validate a mobile HTTPS continuation URL at the SDK\n * boundary.\n *\n * @remarks\n * Accepts only `https://open[-dev].vana.org/continue#<ticket>` with exactly one\n * well-formed opaque fragment ticket and no user info, port, or query. When\n * `env` is supplied only that environment's host is allowed; otherwise both\n * canonical hosts are accepted for structural (defense-in-depth) validation.\n *\n * @param value - The candidate URL from a create or status response.\n * @param env - Optional environment to pin the allowed host to.\n * @returns The canonical URL string, or `undefined` when it fails validation.\n */\nexport function normalizeMobileContinuationUrl(\n value: unknown,\n env?: DirectEnv,\n): string | undefined {\n if (typeof value !== \"string\" || value.length === 0) return undefined;\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n return undefined;\n }\n if (url.protocol !== \"https:\") return undefined;\n const allowedHosts = env\n ? [MOBILE_CONTINUATION_HOSTS[env]]\n : Object.values(MOBILE_CONTINUATION_HOSTS);\n if (!allowedHosts.includes(url.hostname)) return undefined;\n if (url.pathname !== \"/continue\") return undefined;\n if (url.username !== \"\" || url.password !== \"\") return undefined;\n if (url.port !== \"\") return undefined;\n if (url.search !== \"\") return undefined;\n const ticket = url.hash.startsWith(\"#\") ? url.hash.slice(1) : \"\";\n if (!MOBILE_CONTINUATION_TICKET.test(ticket)) return undefined;\n return url.toString();\n}\n\n/**\n * Lifecycle status of an access request.\n *\n * @remarks\n * - `\"pending\"` — created, awaiting user approval.\n * - `\"approved\"` / `\"ready_for_read\"` — the grant exists and the Personal\n * Server is reachable; the data is read-ready (see {@link DirectDataController.readApprovedData}).\n * - `\"completed\"` — the app has already read the data and acknowledged it, so\n * the DCR is terminal. A `\"completed\"` request is **not** read-ready — the\n * browser Personal Server may no longer be serving it.\n * - `\"denied\"` / `\"expired\"` — terminal, no data was delivered.\n */\nexport type AccessRequestStatusValue =\n | \"pending\"\n | \"approved\"\n | \"ready_for_read\"\n | \"completed\"\n | \"denied\"\n | \"expired\";\n\n/** Result of {@link DirectDataController.getAccessRequestStatus}. */\nexport interface AccessRequestStatus {\n /** Current lifecycle status of the request. */\n status: AccessRequestStatusValue;\n /** Personal Server base URL — present once data is ready to read. */\n personalServerUrl?: string;\n /** Grant id covering the approved scope — present once data is ready to read. */\n grantId?: string;\n /**\n * The first approved scope — present once data is ready to read.\n *\n * @remarks\n * Kept for backwards compatibility. A request can approve many scopes; read\n * {@link AccessRequestStatus.scopes} to see all of them.\n */\n scope?: string;\n /**\n * Fresh HTTPS mobile continuation URL, returned only while the deep Direct\n * DCR is still pending. Its embedded ticket may rotate between polls.\n */\n mobileContinuationUrl?: string;\n /**\n * Every scope the user approved on this request — present once data is ready\n * to read.\n *\n * @remarks\n * A grant is keyed by `(user, app)` and carries a list of scopes, so a single\n * approval can cover several. Against an older Vana Account deployment that\n * only returns `scope`, this falls back to `[scope]`.\n */\n scopes?: string[];\n}\n\n/** Result of {@link DirectDataController.readApprovedData}. */\nexport interface ApprovedDataResult<T = unknown> {\n /** The scope the data was read for. */\n scope: string;\n /** The decoded payload returned by the Personal Server. */\n data: T;\n /**\n * Shape-validated but unauthenticated payment metadata echoed by the\n * Personal Server. Use for display/debugging, not accounting proof.\n */\n payment?: DirectPaymentResponseMetadata;\n}\n\n/**\n * Result of {@link DirectDataController.readApprovedData} across every approved\n * scope.\n *\n * @remarks\n * Successes and failures are reported side by side rather than as a thrown\n * error, because each scope read settles its own fee: throwing on the third\n * scope would discard data the app has already paid for. Check `errors` before\n * treating the read as complete.\n */\nexport interface MultiScopeDataResult<T = unknown> {\n /** Scopes that read successfully, keyed by scope. */\n results: Record<string, ApprovedDataResult<T>>;\n /** Scopes that failed, keyed by scope. Empty when every scope read. */\n errors: Record<string, Error>;\n}\n\n/**\n * Client for the Vana Account access-request API — the service that turns a\n * registered app + scopes into a `dcr_*` id and approval URL.\n *\n * @remarks\n * The controller uses a default client against the Vana Account endpoints. You\n * can inject your own implementation to point at a custom deployment or to\n * supply a test double.\n */\nexport interface AccessRequestClient {\n /**\n * Create an access request for the given app + scopes.\n *\n * @param input - App identity, source, scopes, network, and the post-approval return URL.\n * @returns The created {@link AccessRequest}.\n */\n createAccessRequest(input: {\n appAddress: string;\n app: DirectAppConfig;\n source: string;\n scopes: string[];\n returnUrl: string;\n /** Vana protocol network for this request (`\"mainnet\"` or `\"moksha\"`). */\n network: DirectNetwork;\n /** Optional foreground mobile delivery callback. */\n foregroundDelivery?: ForegroundDelivery;\n /**\n * Optional retry key. The default client generates a fresh key per call\n * when omitted; pass a stable key to retry a create whose response was\n * lost without risking a duplicate DCR.\n */\n idempotencyKey?: string;\n }): Promise<AccessRequest>;\n\n /**\n * Fetch the current status of a previously created access request.\n *\n * @param requestId - The `dcr_*` id returned by {@link AccessRequestClient.createAccessRequest}.\n * @returns The current {@link AccessRequestStatus}.\n */\n getAccessRequestStatus(requestId: string): Promise<AccessRequestStatus>;\n\n /**\n * Acknowledge that the app successfully read the approved data.\n *\n * @remarks\n * Direct Vana Web DCRs remain in `ready_for_read` while the browser Personal\n * Server is serving the app. After a successful Personal Server read, the\n * controller calls this hook so Vana Web can mark the request completed and\n * close/redirect the approval tab.\n *\n * Optional so injected clients from older SDK integrations keep compiling;\n * the default HTTP client implements it.\n */\n acknowledgeRead?(requestId: string): Promise<void>;\n}\n\n/**\n * Op-type vocabulary used by the DPv2 escrow payment surface.\n *\n * @remarks\n * These are the operations the gateway prices and settles via\n * `POST /v1/escrow/pay` (`opType` field of the `GenericPayment` message). A\n * direct data read settles the {@link DirectOpType.DataAccess} op for the\n * approved grant; the other op types are listed here for completeness and to\n * give builders a typed vocabulary when inspecting fee breakdowns.\n *\n * GenericPayment uses `\"grant\"` for legacy grant lifecycle payments and\n * `\"data_access\"` for standalone receipt-bound reads.\n */\nexport const DirectOpType = {\n GrantRegistration: \"grant_registration\",\n DataAccess: \"data_access\",\n DataRegistration: \"data_registration\",\n ServerRegistration: \"server_registration\",\n BuilderRegistration: \"builder_registration\",\n} as const;\n\n/** A direct-flow op type (see {@link DirectOpType}). */\nexport type DirectOpTypeValue =\n (typeof DirectOpType)[keyof typeof DirectOpType];\n\n/**\n * What a Personal Server `402 Payment Required` tells the controller is owed for\n * a data read.\n *\n * @remarks\n * The PS read 402 body identifies the challenged operation and amount/asset.\n * The controller settles it via the DPv2 escrow gateway (`/v1/escrow/pay`). The\n * full unmodified body is preserved under\n * {@link PersonalServerPaymentRequired.raw}.\n */\nexport interface PersonalServerPaymentRequired {\n /** Grant id authorizing the Personal Server read. */\n grantId: string;\n /** X402 network advertised by the Personal Server challenge. */\n network?: string;\n /** Payment nonce requested by the 402 challenge. */\n paymentNonce?: string;\n /** Data-access receipt carrying a signature for the gateway to verify. */\n accessRecord?: EscrowAccessRecord;\n /** Asset address owed (zero address = native VANA). */\n asset: string;\n /** Amount owed, as a decimal base-unit string (preserves uint256 precision). */\n amount: string;\n /** The full, unmodified 402 response body. */\n raw: unknown;\n}\n\n/** A validated legacy grant payment challenge. */\nexport interface PersonalServerGrantPaymentOperation extends PersonalServerPaymentRequired {\n /** Escrow operation discriminator. */\n opType: \"grant\";\n /** Grant id settled by the escrow payment. */\n opId: string;\n}\n\n/** A validated receipt-bound data-access payment challenge. */\nexport interface PersonalServerDataAccessPaymentOperation extends PersonalServerPaymentRequired {\n /** Escrow operation discriminator. */\n opType: \"data_access\";\n /** Access-record id settled by the escrow payment. */\n opId: string;\n /** Complete receipt whose signature is verified later by the gateway. */\n accessRecord: EscrowAccessRecord;\n /** Positive uint256 nonce supplied by the Personal Server challenge. */\n paymentNonce: string;\n}\n\n/**\n * A Personal Server payment challenge whose escrow operation has been\n * validated.\n *\n * @remarks\n * Validation here is structural and binds operation ids to their receipt. It\n * does not cryptographically verify the receipt signature; the Personal\n * Server and Data Gateway perform that verification.\n */\nexport type PersonalServerPaymentOperation =\n | PersonalServerGrantPaymentOperation\n | PersonalServerDataAccessPaymentOperation;\n\n/** Shape-validated payment response returned directly by the escrow gateway. */\nexport interface DirectPaymentReceipt {\n /** Op type settled (the gateway `opType`, e.g. `\"grant\"`). */\n opType: string;\n /** Op id settled (a grant id or access-record id). */\n opId: string;\n /** Asset paid in (zero address = native VANA). */\n asset: string;\n /** Total amount paid, as a decimal base-unit string. */\n amount: string;\n /** Payment nonce used for this settlement. */\n paymentNonce: string;\n /** Fee breakdown reported by the gateway (registration vs data-access fee). */\n breakdown: DirectFeeBreakdown;\n /** ISO timestamp the gateway recorded the payment. */\n paidAt: string;\n}\n\n/**\n * Untrusted payment response metadata echoed by a Personal Server.\n *\n * @remarks\n * The SDK validates every field before exposing this shape, but the response\n * header is not signed by the gateway. Use it for display and debugging only,\n * never as accounting proof that a payment occurred.\n */\nexport type DirectPaymentResponseMetadata = DirectPaymentReceipt;\n\n/**\n * Per-op fee breakdown reported by the gateway.\n *\n * @remarks\n * Mirrors the escrow {@link PaymentBreakdown}: a one-time registration fee plus\n * the per-read data-access fee, and whether this settlement covered the\n * registration fee.\n */\nexport interface DirectFeeBreakdown {\n /** One-time registration fee for the op, as a decimal base-unit string. */\n registrationFee: string;\n /** Per-read data-access fee, as a decimal base-unit string. */\n dataAccessFee: string;\n /** True when this settlement paid the registration fee. */\n registrationPaid: boolean;\n}\n"],"mappings":"AA6GA,MAAM,4BAAuD;AAAA,EAC3D,YAAY;AAAA,EACZ,KAAK;AACP;AAGA,MAAM,6BAA6B;AAgB5B,SAAS,+BACd,OACA,KACoB;AACpB,MAAI,OAAO,UAAU,YAAY,MAAM,WAAW,EAAG,QAAO;AAC5D,MAAI;AACJ,MAAI;AACF,UAAM,IAAI,IAAI,KAAK;AAAA,EACrB,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,IAAI,aAAa,SAAU,QAAO;AACtC,QAAM,eAAe,MACjB,CAAC,0BAA0B,GAAG,CAAC,IAC/B,OAAO,OAAO,yBAAyB;AAC3C,MAAI,CAAC,aAAa,SAAS,IAAI,QAAQ,EAAG,QAAO;AACjD,MAAI,IAAI,aAAa,YAAa,QAAO;AACzC,MAAI,IAAI,aAAa,MAAM,IAAI,aAAa,GAAI,QAAO;AACvD,MAAI,IAAI,SAAS,GAAI,QAAO;AAC5B,MAAI,IAAI,WAAW,GAAI,QAAO;AAC9B,QAAM,SAAS,IAAI,KAAK,WAAW,GAAG,IAAI,IAAI,KAAK,MAAM,CAAC,IAAI;AAC9D,MAAI,CAAC,2BAA2B,KAAK,MAAM,EAAG,QAAO;AACrD,SAAO,IAAI,SAAS;AACtB;AA2JO,MAAM,eAAe;AAAA,EAC1B,mBAAmB;AAAA,EACnB,YAAY;AAAA,EACZ,kBAAkB;AAAA,EAClB,oBAAoB;AAAA,EACpB,qBAAqB;AACvB;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/direct/types.ts"],"sourcesContent":["import type { EscrowAccessRecord } from \"../protocol/escrow\";\nimport type { ProtocolNetwork } from \"../protocol/networks\";\n\n/**\n * Shared types for the Direct Data Controller and the browser connect helper.\n *\n * @remarks\n * These types describe the \"two-tab\" Data Portability flow documented in the\n * builder guide: a backend controller creates an access request, the browser\n * opens Vana for user approval, and the backend reads the approved data from\n * the user's Personal Server (handling 402 Payment Required).\n *\n * @category Direct\n * @module direct/types\n */\n\n/**\n * Target environment for a {@link DirectDataController}.\n *\n * - `\"production\"` — Vana mainnet stack (default service URLs).\n * - `\"dev\"` — Vana internal dev stack. Use only when testing against\n * Vana's dev infrastructure.\n */\nexport type DirectEnv = \"dev\" | \"production\";\n\n/**\n * Vana network used for chain-aware Direct defaults.\n *\n * - `\"mainnet\"` — Vana mainnet (`chainId` 1480).\n * - `\"moksha\"` — Moksha testnet (`chainId` 14800).\n */\nexport type DirectNetwork = ProtocolNetwork;\n\n/**\n * App identity advertised to users during approval and attributed in Builder\n * League activity reports.\n */\nexport interface DirectAppConfig {\n /** Stable, human-readable app id (e.g. `\"notes-lens\"`). */\n id: string;\n /** Display name shown to the user in the Vana approval UI. */\n name: string;\n /** Public homepage URL for the app. */\n homepageUrl: string;\n}\n\n/**\n * Resolved app identity: the configured {@link DirectAppConfig} plus the app's\n * derived on-chain address (the address to fund and inspect).\n */\nexport interface AppIdentity extends DirectAppConfig {\n /** The app's `0x`-prefixed on-chain address (derived from `appPrivateKey`). */\n address: string;\n}\n\n/** One-time HTTPS callback used to deliver a foreground mobile Direct read. */\nexport interface ForegroundDelivery {\n /** Fixed, same-origin consumer callback URL. */\n url: string;\n /** High-entropy bearer capability, generated and retained by the consumer. */\n token: string;\n}\n\n/**\n * One derivative question carried on an access request.\n *\n * @remarks\n * A question asks the user's Personal Server to compute an answer scope\n * (`derivedScope`) from source scopes the app never reads. On approval, the\n * Vana app registers the question on the Personal Server as the owner and the\n * grant covers only the derived scope, so the raw sources stay private to the\n * user. The SDK validates each question before the create request is signed\n * (see `validateAccessRequestQuestions`); the access-request service remains\n * authoritative and re-validates server-side.\n */\nexport interface AccessRequestQuestion {\n /**\n * Concrete scope the computed answer is written to and read from (no\n * wildcards, no operation prefix). It must also appear verbatim in the\n * request `scopes` as a bare read entry, and its first dot-segment must\n * differ from the first dot-segment of every entry in\n * {@link AccessRequestQuestion.sourceScopes}.\n */\n derivedScope: string;\n /**\n * Concrete scopes the answer is computed from: 1 to 16 entries, no\n * duplicates, none equal to {@link AccessRequestQuestion.derivedScope}.\n * These scopes are never granted to the app.\n */\n sourceScopes: string[];\n /**\n * Natural-language question the Personal Server answers from the source\n * scopes. Must be 1 to 4000 characters after trimming.\n */\n question: string;\n /**\n * When the Personal Server recomputes the answer. `\"snapshot\"` computes\n * once at registration; `\"on-change\"` also recomputes when a source scope\n * changes. When omitted, the server-side default applies.\n */\n recompute?: \"snapshot\" | \"on-change\";\n}\n\n/**\n * Resolved service URLs and chain id for a given {@link DirectEnv}.\n *\n * @remarks\n * Centralizes the per-environment base URLs the controller talks to. Each can\n * be overridden via {@link DirectDataControllerConfig.endpoints} when pointing\n * at a non-standard deployment.\n */\nexport interface DirectServiceEndpoints {\n /** Vana chain id for this environment (1480 mainnet, 14800 moksha). */\n chainId: number;\n /** Base URL of the Vana Account access-request API that issues `dcr_*` ids. */\n accessRequestBaseUrl: string;\n /** Base URL users are sent to for approval (the Vana app). */\n approvalAppBaseUrl: string;\n /** Base URL of the DP RPC escrow gateway used to settle `402 Payment Required`. */\n escrowGatewayUrl: string;\n}\n\n/** Result of {@link DirectDataController.createAccessRequest}. */\nexport interface AccessRequest {\n /** Opaque request id (e.g. `\"dcr_123\"`). */\n requestId: string;\n /** URL the browser opens so the user can approve the requested scopes. */\n approvalUrl: string;\n /** On-chain address of the (registered or reused) app. */\n appAddress: string;\n /** Protocol network echoed by the access-request service. */\n network?: DirectNetwork;\n /** Authoritative ISO-8601 expiry for the access request. */\n expiresAt?: string;\n /**\n * HTTPS continuation URL for a deep Direct request on a mobile browser.\n *\n * @remarks\n * Present only for a server-classified deep Direct DCR while it remains\n * pending, and only when Mobile continuation is enabled. It is an ordinary\n * `https://open[-dev].vana.org/continue#<ticket>` link the mobile UI renders\n * as a primary \"Open Vana\" tap — iOS Universal Links / Android App Links\n * deliver it to Vana Mobile, and its web fallback recovers an absent app. The\n * SDK never launches it automatically and owns no persistence.\n */\n mobileContinuationUrl?: string;\n}\n\n/** Canonical mobile continuation link host per {@link DirectEnv}. */\nconst MOBILE_CONTINUATION_HOSTS: Record<DirectEnv, string> = {\n production: \"open.vana.org\",\n dev: \"open-dev.vana.org\",\n};\n\n/** URL-fragment-safe opaque ticket: no separators, query, or scheme chars. */\nconst MOBILE_CONTINUATION_TICKET = /^[A-Za-z0-9._~-]+$/;\n\n/**\n * @internal Strictly validate a mobile HTTPS continuation URL at the SDK\n * boundary.\n *\n * @remarks\n * Accepts only `https://open[-dev].vana.org/continue#<ticket>` with exactly one\n * well-formed opaque fragment ticket and no user info, port, or query. When\n * `env` is supplied only that environment's host is allowed; otherwise both\n * canonical hosts are accepted for structural (defense-in-depth) validation.\n *\n * @param value - The candidate URL from a create or status response.\n * @param env - Optional environment to pin the allowed host to.\n * @returns The canonical URL string, or `undefined` when it fails validation.\n */\nexport function normalizeMobileContinuationUrl(\n value: unknown,\n env?: DirectEnv,\n): string | undefined {\n if (typeof value !== \"string\" || value.length === 0) return undefined;\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n return undefined;\n }\n if (url.protocol !== \"https:\") return undefined;\n const allowedHosts = env\n ? [MOBILE_CONTINUATION_HOSTS[env]]\n : Object.values(MOBILE_CONTINUATION_HOSTS);\n if (!allowedHosts.includes(url.hostname)) return undefined;\n if (url.pathname !== \"/continue\") return undefined;\n if (url.username !== \"\" || url.password !== \"\") return undefined;\n if (url.port !== \"\") return undefined;\n if (url.search !== \"\") return undefined;\n const ticket = url.hash.startsWith(\"#\") ? url.hash.slice(1) : \"\";\n if (!MOBILE_CONTINUATION_TICKET.test(ticket)) return undefined;\n return url.toString();\n}\n\n/**\n * Lifecycle status of an access request.\n *\n * @remarks\n * - `\"pending\"` — created, awaiting user approval.\n * - `\"approved\"` / `\"ready_for_read\"` — the grant exists and the Personal\n * Server is reachable; the data is read-ready (see {@link DirectDataController.readApprovedData}).\n * - `\"completed\"` — the app has already read the data and acknowledged it, so\n * the DCR is terminal. A `\"completed\"` request is **not** read-ready — the\n * browser Personal Server may no longer be serving it.\n * - `\"denied\"` / `\"expired\"` — terminal, no data was delivered.\n */\nexport type AccessRequestStatusValue =\n | \"pending\"\n | \"approved\"\n | \"ready_for_read\"\n | \"completed\"\n | \"denied\"\n | \"expired\";\n\n/** Result of {@link DirectDataController.getAccessRequestStatus}. */\nexport interface AccessRequestStatus {\n /** Current lifecycle status of the request. */\n status: AccessRequestStatusValue;\n /** Personal Server base URL — present once data is ready to read. */\n personalServerUrl?: string;\n /** Grant id covering the approved scope — present once data is ready to read. */\n grantId?: string;\n /**\n * The first approved scope — present once data is ready to read.\n *\n * @remarks\n * Kept for backwards compatibility. A request can approve many scopes; read\n * {@link AccessRequestStatus.scopes} to see all of them.\n */\n scope?: string;\n /**\n * Fresh HTTPS mobile continuation URL, returned only while the deep Direct\n * DCR is still pending. Its embedded ticket may rotate between polls.\n */\n mobileContinuationUrl?: string;\n /**\n * Every scope the user approved on this request — present once data is ready\n * to read.\n *\n * @remarks\n * A grant is keyed by `(user, app)` and carries a list of scopes, so a single\n * approval can cover several. Against an older Vana Account deployment that\n * only returns `scope`, this falls back to `[scope]`.\n */\n scopes?: string[];\n}\n\n/** Result of {@link DirectDataController.readApprovedData}. */\nexport interface ApprovedDataResult<T = unknown> {\n /** The scope the data was read for. */\n scope: string;\n /** The decoded payload returned by the Personal Server. */\n data: T;\n /**\n * Shape-validated but unauthenticated payment metadata echoed by the\n * Personal Server. Use for display/debugging, not accounting proof.\n */\n payment?: DirectPaymentResponseMetadata;\n}\n\n/**\n * Result of {@link DirectDataController.readApprovedData} across every approved\n * scope.\n *\n * @remarks\n * Successes and failures are reported side by side rather than as a thrown\n * error, because each scope read settles its own fee: throwing on the third\n * scope would discard data the app has already paid for. Check `errors` before\n * treating the read as complete.\n */\nexport interface MultiScopeDataResult<T = unknown> {\n /** Scopes that read successfully, keyed by scope. */\n results: Record<string, ApprovedDataResult<T>>;\n /** Scopes that failed, keyed by scope. Empty when every scope read. */\n errors: Record<string, Error>;\n}\n\n/**\n * Client for the Vana Account access-request API — the service that turns a\n * registered app + scopes into a `dcr_*` id and approval URL.\n *\n * @remarks\n * The controller uses a default client against the Vana Account endpoints. You\n * can inject your own implementation to point at a custom deployment or to\n * supply a test double.\n */\nexport interface AccessRequestClient {\n /**\n * Create an access request for the given app + scopes.\n *\n * @param input - App identity, source, scopes, network, and the post-approval return URL.\n * @returns The created {@link AccessRequest}.\n */\n createAccessRequest(input: {\n appAddress: string;\n app: DirectAppConfig;\n source: string;\n scopes: string[];\n returnUrl: string;\n /** Vana protocol network for this request (`\"mainnet\"` or `\"moksha\"`). */\n network: DirectNetwork;\n /** Optional foreground mobile delivery callback. */\n foregroundDelivery?: ForegroundDelivery;\n /**\n * Derivative questions to carry on the request (1 to 4). When present,\n * the array is validated client-side and serialized into the signed\n * create body verbatim. See {@link AccessRequestQuestion}.\n */\n questions?: AccessRequestQuestion[];\n /**\n * Optional retry key. The default client generates a fresh key per call\n * when omitted; pass a stable key to retry a create whose response was\n * lost without risking a duplicate DCR.\n */\n idempotencyKey?: string;\n }): Promise<AccessRequest>;\n\n /**\n * Fetch the current status of a previously created access request.\n *\n * @param requestId - The `dcr_*` id returned by {@link AccessRequestClient.createAccessRequest}.\n * @returns The current {@link AccessRequestStatus}.\n */\n getAccessRequestStatus(requestId: string): Promise<AccessRequestStatus>;\n\n /**\n * Acknowledge that the app successfully read the approved data.\n *\n * @remarks\n * Direct Vana Web DCRs remain in `ready_for_read` while the browser Personal\n * Server is serving the app. After a successful Personal Server read, the\n * controller calls this hook so Vana Web can mark the request completed and\n * close/redirect the approval tab.\n *\n * Optional so injected clients from older SDK integrations keep compiling;\n * the default HTTP client implements it.\n */\n acknowledgeRead?(requestId: string): Promise<void>;\n}\n\n/**\n * Op-type vocabulary used by the DPv2 escrow payment surface.\n *\n * @remarks\n * These are the operations the gateway prices and settles via\n * `POST /v1/escrow/pay` (`opType` field of the `GenericPayment` message). A\n * direct data read settles the {@link DirectOpType.DataAccess} op for the\n * approved grant; the other op types are listed here for completeness and to\n * give builders a typed vocabulary when inspecting fee breakdowns.\n *\n * GenericPayment uses `\"grant\"` for legacy grant lifecycle payments and\n * `\"data_access\"` for standalone receipt-bound reads.\n */\nexport const DirectOpType = {\n GrantRegistration: \"grant_registration\",\n DataAccess: \"data_access\",\n DataRegistration: \"data_registration\",\n ServerRegistration: \"server_registration\",\n BuilderRegistration: \"builder_registration\",\n} as const;\n\n/** A direct-flow op type (see {@link DirectOpType}). */\nexport type DirectOpTypeValue =\n (typeof DirectOpType)[keyof typeof DirectOpType];\n\n/**\n * What a Personal Server `402 Payment Required` tells the controller is owed for\n * a data read.\n *\n * @remarks\n * The PS read 402 body identifies the challenged operation and amount/asset.\n * The controller settles it via the DPv2 escrow gateway (`/v1/escrow/pay`). The\n * full unmodified body is preserved under\n * {@link PersonalServerPaymentRequired.raw}.\n */\nexport interface PersonalServerPaymentRequired {\n /** Grant id authorizing the Personal Server read. */\n grantId: string;\n /** X402 network advertised by the Personal Server challenge. */\n network?: string;\n /** Payment nonce requested by the 402 challenge. */\n paymentNonce?: string;\n /** Data-access receipt carrying a signature for the gateway to verify. */\n accessRecord?: EscrowAccessRecord;\n /** Asset address owed (zero address = native VANA). */\n asset: string;\n /** Amount owed, as a decimal base-unit string (preserves uint256 precision). */\n amount: string;\n /** The full, unmodified 402 response body. */\n raw: unknown;\n}\n\n/** A validated legacy grant payment challenge. */\nexport interface PersonalServerGrantPaymentOperation extends PersonalServerPaymentRequired {\n /** Escrow operation discriminator. */\n opType: \"grant\";\n /** Grant id settled by the escrow payment. */\n opId: string;\n}\n\n/** A validated receipt-bound data-access payment challenge. */\nexport interface PersonalServerDataAccessPaymentOperation extends PersonalServerPaymentRequired {\n /** Escrow operation discriminator. */\n opType: \"data_access\";\n /** Access-record id settled by the escrow payment. */\n opId: string;\n /** Complete receipt whose signature is verified later by the gateway. */\n accessRecord: EscrowAccessRecord;\n /** Positive uint256 nonce supplied by the Personal Server challenge. */\n paymentNonce: string;\n}\n\n/**\n * A Personal Server payment challenge whose escrow operation has been\n * validated.\n *\n * @remarks\n * Validation here is structural and binds operation ids to their receipt. It\n * does not cryptographically verify the receipt signature; the Personal\n * Server and Data Gateway perform that verification.\n */\nexport type PersonalServerPaymentOperation =\n | PersonalServerGrantPaymentOperation\n | PersonalServerDataAccessPaymentOperation;\n\n/** Shape-validated payment response returned directly by the escrow gateway. */\nexport interface DirectPaymentReceipt {\n /** Op type settled (the gateway `opType`, e.g. `\"grant\"`). */\n opType: string;\n /** Op id settled (a grant id or access-record id). */\n opId: string;\n /** Asset paid in (zero address = native VANA). */\n asset: string;\n /** Total amount paid, as a decimal base-unit string. */\n amount: string;\n /** Payment nonce used for this settlement. */\n paymentNonce: string;\n /** Fee breakdown reported by the gateway (registration vs data-access fee). */\n breakdown: DirectFeeBreakdown;\n /** ISO timestamp the gateway recorded the payment. */\n paidAt: string;\n}\n\n/**\n * Untrusted payment response metadata echoed by a Personal Server.\n *\n * @remarks\n * The SDK validates every field before exposing this shape, but the response\n * header is not signed by the gateway. Use it for display and debugging only,\n * never as accounting proof that a payment occurred.\n */\nexport type DirectPaymentResponseMetadata = DirectPaymentReceipt;\n\n/**\n * Per-op fee breakdown reported by the gateway.\n *\n * @remarks\n * Mirrors the escrow {@link PaymentBreakdown}: a one-time registration fee plus\n * the per-read data-access fee, and whether this settlement covered the\n * registration fee.\n */\nexport interface DirectFeeBreakdown {\n /** One-time registration fee for the op, as a decimal base-unit string. */\n registrationFee: string;\n /** Per-read data-access fee, as a decimal base-unit string. */\n dataAccessFee: string;\n /** True when this settlement paid the registration fee. */\n registrationPaid: boolean;\n}\n"],"mappings":"AAqJA,MAAM,4BAAuD;AAAA,EAC3D,YAAY;AAAA,EACZ,KAAK;AACP;AAGA,MAAM,6BAA6B;AAgB5B,SAAS,+BACd,OACA,KACoB;AACpB,MAAI,OAAO,UAAU,YAAY,MAAM,WAAW,EAAG,QAAO;AAC5D,MAAI;AACJ,MAAI;AACF,UAAM,IAAI,IAAI,KAAK;AAAA,EACrB,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,IAAI,aAAa,SAAU,QAAO;AACtC,QAAM,eAAe,MACjB,CAAC,0BAA0B,GAAG,CAAC,IAC/B,OAAO,OAAO,yBAAyB;AAC3C,MAAI,CAAC,aAAa,SAAS,IAAI,QAAQ,EAAG,QAAO;AACjD,MAAI,IAAI,aAAa,YAAa,QAAO;AACzC,MAAI,IAAI,aAAa,MAAM,IAAI,aAAa,GAAI,QAAO;AACvD,MAAI,IAAI,SAAS,GAAI,QAAO;AAC5B,MAAI,IAAI,WAAW,GAAI,QAAO;AAC9B,QAAM,SAAS,IAAI,KAAK,WAAW,GAAG,IAAI,IAAI,KAAK,MAAM,CAAC,IAAI;AAC9D,MAAI,CAAC,2BAA2B,KAAK,MAAM,EAAG,QAAO;AACrD,SAAO,IAAI,SAAS;AACtB;AAiKO,MAAM,eAAe;AAAA,EAC1B,mBAAmB;AAAA,EACnB,YAAY;AAAA,EACZ,kBAAkB;AAAA,EAClB,oBAAoB;AAAA,EACpB,qBAAqB;AACvB;","names":[]}
|
package/dist/server.cjs
CHANGED
|
@@ -55,7 +55,8 @@ __export(server_exports, {
|
|
|
55
55
|
scopeMatchesPattern: () => import_scopes.scopeMatchesPattern,
|
|
56
56
|
toDirectFeeBreakdown: () => import_escrow_payment.toDirectFeeBreakdown,
|
|
57
57
|
toDirectPaymentReceipt: () => import_escrow_payment.toDirectPaymentReceipt,
|
|
58
|
-
tryGrantPermissions: () => import_scope_actions.tryGrantPermissions
|
|
58
|
+
tryGrantPermissions: () => import_scope_actions.tryGrantPermissions,
|
|
59
|
+
validateAccessRequestQuestions: () => import_access_request_client.validateAccessRequestQuestions
|
|
59
60
|
});
|
|
60
61
|
module.exports = __toCommonJS(server_exports);
|
|
61
62
|
var import_controller = require("./direct/controller");
|
|
@@ -106,6 +107,7 @@ var import_types = require("./direct/types");
|
|
|
106
107
|
scopeMatchesPattern,
|
|
107
108
|
toDirectFeeBreakdown,
|
|
108
109
|
toDirectPaymentReceipt,
|
|
109
|
-
tryGrantPermissions
|
|
110
|
+
tryGrantPermissions,
|
|
111
|
+
validateAccessRequestQuestions
|
|
110
112
|
});
|
|
111
113
|
//# sourceMappingURL=server.cjs.map
|
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// Grant scope entries: the `[operation:]scope` vocabulary the controller's\n// `scopes` are written in, so a backend can build and read them without\n// reaching for a platform entry point.\nexport {\n ScopeSchema,\n parseScope,\n scopeMatchesPattern,\n scopeCoveredByGrant,\n type Scope,\n type ParsedScope,\n} from \"./protocol/scopes\";\nexport {\n SCOPE_ACTIONS,\n InvalidScopeEntryError,\n parseScopeEntry,\n formatScopeEntry,\n grantPermissions,\n permissionsToScopes,\n tryGrantPermissions,\n hasAction,\n type ScopeAction,\n type ParsedScopeEntry,\n type GrantPermission,\n} from \"./protocol/scope-actions\";\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;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAyBA,wBAKO;AAGP,
|
|
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 validateAccessRequestQuestions,\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// Grant scope entries: the `[operation:]scope` vocabulary the controller's\n// `scopes` are written in, so a backend can build and read them without\n// reaching for a platform entry point.\nexport {\n ScopeSchema,\n parseScope,\n scopeMatchesPattern,\n scopeCoveredByGrant,\n type Scope,\n type ParsedScope,\n} from \"./protocol/scopes\";\nexport {\n SCOPE_ACTIONS,\n InvalidScopeEntryError,\n parseScopeEntry,\n formatScopeEntry,\n grantPermissions,\n permissionsToScopes,\n tryGrantPermissions,\n hasAction,\n type ScopeAction,\n type ParsedScopeEntry,\n type GrantPermission,\n} from \"./protocol/scope-actions\";\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 AccessRequestQuestion,\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;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAyBA,wBAKO;AAGP,mCAMO;AACP,kCAUO;AAEP,4BAeO;AACP,uBAIO;AAIP,oBAOO;AACP,2BAYO;AAGP,oBAMO;AA4BP,mBAA6B;","names":[]}
|
package/dist/server.d.ts
CHANGED
|
@@ -23,12 +23,12 @@
|
|
|
23
23
|
* @module server
|
|
24
24
|
*/
|
|
25
25
|
export { createDirectDataController, type DirectDataController, type DirectDataControllerConfig, type DirectEscrowConfig, } from "./direct/controller.js";
|
|
26
|
-
export { createDefaultAccessRequestClient, buildApprovalUrl, type DefaultAccessRequestClientOptions, type FetchLike, } from "./direct/access-request-client.js";
|
|
26
|
+
export { createDefaultAccessRequestClient, buildApprovalUrl, validateAccessRequestQuestions, type DefaultAccessRequestClientOptions, type FetchLike, } from "./direct/access-request-client.js";
|
|
27
27
|
export { buildPersonalServerDataReadRequest, readPersonalServerData, parsePersonalServerPaymentRequired, dataPathForScope, type PersonalServerDataReadRequest, type PersonalServerReadResult, type PersonalServerFetch, type PersonalServerTransportRetryOptions, type FetchResponseLike, } from "./direct/personal-server-read.js";
|
|
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 { ScopeSchema, parseScope, scopeMatchesPattern, scopeCoveredByGrant, type Scope, type ParsedScope, } from "./protocol/scopes.js";
|
|
31
31
|
export { SCOPE_ACTIONS, InvalidScopeEntryError, parseScopeEntry, formatScopeEntry, grantPermissions, permissionsToScopes, tryGrantPermissions, hasAction, type ScopeAction, type ParsedScopeEntry, type GrantPermission, } from "./protocol/scope-actions.js";
|
|
32
32
|
export { DirectConfigError, AccessNotApprovedError, ScopeNotApprovedError, PersonalServerReadError, PaymentRequiredError, } from "./direct/errors.js";
|
|
33
|
-
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";
|
|
33
|
+
export type { DirectEnv, DirectNetwork, DirectAppConfig, ForegroundDelivery, AppIdentity, DirectServiceEndpoints, AccessRequest, AccessRequestQuestion, AccessRequestStatus, AccessRequestStatusValue, ApprovedDataResult, MultiScopeDataResult, AccessRequestClient, DirectOpTypeValue, PersonalServerDataAccessPaymentOperation, PersonalServerGrantPaymentOperation, PersonalServerPaymentOperation, PersonalServerPaymentRequired, DirectPaymentReceipt, DirectPaymentResponseMetadata, DirectFeeBreakdown, } from "./direct/types.js";
|
|
34
34
|
export { DirectOpType } from "./direct/types.js";
|
package/dist/server.js
CHANGED
|
@@ -3,7 +3,8 @@ import {
|
|
|
3
3
|
} from "./direct/controller.js";
|
|
4
4
|
import {
|
|
5
5
|
createDefaultAccessRequestClient,
|
|
6
|
-
buildApprovalUrl
|
|
6
|
+
buildApprovalUrl,
|
|
7
|
+
validateAccessRequestQuestions
|
|
7
8
|
} from "./direct/access-request-client.js";
|
|
8
9
|
import {
|
|
9
10
|
buildPersonalServerDataReadRequest,
|
|
@@ -90,6 +91,7 @@ export {
|
|
|
90
91
|
scopeMatchesPattern,
|
|
91
92
|
toDirectFeeBreakdown,
|
|
92
93
|
toDirectPaymentReceipt,
|
|
93
|
-
tryGrantPermissions
|
|
94
|
+
tryGrantPermissions,
|
|
95
|
+
validateAccessRequestQuestions
|
|
94
96
|
};
|
|
95
97
|
//# sourceMappingURL=server.js.map
|
package/dist/server.js.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// Grant scope entries: the `[operation:]scope` vocabulary the controller's\n// `scopes` are written in, so a backend can build and read them without\n// reaching for a platform entry point.\nexport {\n ScopeSchema,\n parseScope,\n scopeMatchesPattern,\n scopeCoveredByGrant,\n type Scope,\n type ParsedScope,\n} from \"./protocol/scopes\";\nexport {\n SCOPE_ACTIONS,\n InvalidScopeEntryError,\n parseScopeEntry,\n formatScopeEntry,\n grantPermissions,\n permissionsToScopes,\n tryGrantPermissions,\n hasAction,\n type ScopeAction,\n type ParsedScopeEntry,\n type GrantPermission,\n} from \"./protocol/scope-actions\";\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":"AAyBA;AAAA,EACE;AAAA,OAIK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,OAGK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAMK;AAEP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAKK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAIP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAIK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;
|
|
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 validateAccessRequestQuestions,\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// Grant scope entries: the `[operation:]scope` vocabulary the controller's\n// `scopes` are written in, so a backend can build and read them without\n// reaching for a platform entry point.\nexport {\n ScopeSchema,\n parseScope,\n scopeMatchesPattern,\n scopeCoveredByGrant,\n type Scope,\n type ParsedScope,\n} from \"./protocol/scopes\";\nexport {\n SCOPE_ACTIONS,\n InvalidScopeEntryError,\n parseScopeEntry,\n formatScopeEntry,\n grantPermissions,\n permissionsToScopes,\n tryGrantPermissions,\n hasAction,\n type ScopeAction,\n type ParsedScopeEntry,\n type GrantPermission,\n} from \"./protocol/scope-actions\";\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 AccessRequestQuestion,\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":"AAyBA;AAAA,EACE;AAAA,OAIK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAMK;AAEP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAKK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAIP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAIK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AA4BP,SAAS,oBAAoB;","names":[]}
|