@opendatalabs/vana-sdk 3.20.1 → 3.22.0-pr.207.0a5d07b

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.
Files changed (69) hide show
  1. package/README.md +64 -0
  2. package/dist/crypto/envelope/job.cjs +263 -0
  3. package/dist/crypto/envelope/job.cjs.map +1 -0
  4. package/dist/crypto/envelope/job.d.ts +122 -0
  5. package/dist/crypto/envelope/job.js +240 -0
  6. package/dist/crypto/envelope/job.js.map +1 -0
  7. package/dist/direct/access-request-client.cjs +100 -2
  8. package/dist/direct/access-request-client.cjs.map +1 -1
  9. package/dist/direct/access-request-client.d.ts +23 -1
  10. package/dist/direct/access-request-client.js +98 -1
  11. package/dist/direct/access-request-client.js.map +1 -1
  12. package/dist/direct/controller.cjs +4 -0
  13. package/dist/direct/controller.cjs.map +1 -1
  14. package/dist/direct/controller.d.ts +10 -1
  15. package/dist/direct/controller.js +6 -1
  16. package/dist/direct/controller.js.map +1 -1
  17. package/dist/direct/types.cjs.map +1 -1
  18. package/dist/direct/types.d.ts +45 -0
  19. package/dist/direct/types.js.map +1 -1
  20. package/dist/errors.cjs +77 -0
  21. package/dist/errors.cjs.map +1 -1
  22. package/dist/errors.d.ts +123 -0
  23. package/dist/errors.js +67 -0
  24. package/dist/errors.js.map +1 -1
  25. package/dist/index.browser.d.ts +5 -1
  26. package/dist/index.browser.js +721 -24
  27. package/dist/index.browser.js.map +4 -4
  28. package/dist/index.node.cjs +1309 -107
  29. package/dist/index.node.cjs.map +4 -4
  30. package/dist/index.node.d.ts +6 -1
  31. package/dist/index.node.js +1261 -97
  32. package/dist/index.node.js.map +4 -4
  33. package/dist/protocol/derivative-questions.cjs +22 -0
  34. package/dist/protocol/derivative-questions.cjs.map +1 -1
  35. package/dist/protocol/derivative-questions.d.ts +53 -0
  36. package/dist/protocol/derivative-questions.js +19 -0
  37. package/dist/protocol/derivative-questions.js.map +1 -1
  38. package/dist/protocol/derivative-status.cjs +209 -0
  39. package/dist/protocol/derivative-status.cjs.map +1 -0
  40. package/dist/protocol/derivative-status.d.ts +196 -0
  41. package/dist/protocol/derivative-status.js +190 -0
  42. package/dist/protocol/derivative-status.js.map +1 -0
  43. package/dist/protocol/derivative-status.test.d.ts +1 -0
  44. package/dist/protocol/identity.cjs +219 -0
  45. package/dist/protocol/identity.cjs.map +1 -0
  46. package/dist/protocol/identity.d.ts +193 -0
  47. package/dist/protocol/identity.js +183 -0
  48. package/dist/protocol/identity.js.map +1 -0
  49. package/dist/protocol/identity.test.d.ts +1 -0
  50. package/dist/protocol/identity.vector.test.d.ts +1 -0
  51. package/dist/protocol/jobs-client.cjs +493 -0
  52. package/dist/protocol/jobs-client.cjs.map +1 -0
  53. package/dist/protocol/jobs-client.d.ts +167 -0
  54. package/dist/protocol/jobs-client.js +498 -0
  55. package/dist/protocol/jobs-client.js.map +1 -0
  56. package/dist/protocol/jobs-client.test.d.ts +1 -0
  57. package/dist/protocol/jobs.cjs +67 -0
  58. package/dist/protocol/jobs.cjs.map +1 -0
  59. package/dist/protocol/jobs.d.ts +185 -0
  60. package/dist/protocol/jobs.js +33 -0
  61. package/dist/protocol/jobs.js.map +1 -0
  62. package/dist/protocol/jobs.test.d.ts +1 -0
  63. package/dist/server.cjs +4 -2
  64. package/dist/server.cjs.map +1 -1
  65. package/dist/server.d.ts +2 -2
  66. package/dist/server.js +4 -2
  67. package/dist/server.js.map +1 -1
  68. package/dist/tests/mock-personal-server.d.ts +9 -0
  69. package/package.json +7 -1
@@ -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":"AAuBA,SAAS,2BAA2B;AAGpC,SAAS,kBAAkB;AAC3B,SAAS,uBAAuB;AAChC,SAAS,iCAAiC;AAC1C,SAAS,iBAAiB;AAC1B;AAAA,EACE;AAAA,OAEK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAKP;AAAA,EACE;AAAA,OAGK;AAkOP,SAAS,gBAAgB,OAA6B;AACpD,SAAO,sBAAsB,KAAK,KAAK;AACzC;AAMA,SAAS,kBAAkB,QAA2C;AACpE,SAAO,WAAW,cAAc,WAAW;AAC7C;AAaO,SAAS,2BACd,QACsB;AAGtB,QAAM,aAAa,OAAO,iBAAiB,OAAO;AAClD,MAAI,CAAC,cAAc,CAAC,gBAAgB,UAAU,GAAG;AAC/C,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,MAAI,CAAC,OAAO,UAAU,OAAO,OAAO,WAAW,GAAG;AAChD,UAAM,IAAI,kBAAkB,gCAAgC;AAAA,EAC9D;AAQA,aAAW,SAAS,OAAO,QAAQ;AACjC,eAAW,gBAAgB,KAAK,EAAE,KAAK;AAAA,EACzC;AAEA,QAAM,MAAiB,OAAO,OAAO;AACrC,QAAM,UAAyB,OAAO,WAAW,wBAAwB,GAAG;AAC5E,QAAM,mBAAmB,mBAAmB,GAAG;AAC/C,QAAM,UAAU,OAAO,WAAW,WAAW,wBAAwB,OAAO;AAC5E,QAAM,YAAoC;AAAA,IACxC,GAAG;AAAA,IACH,GAAG,OAAO;AAAA,IACV;AAAA,EACF;AAEA,QAAM,UAAU,oBAAoB,UAAiB;AACrD,QAAM,cAAgC,CAAC,YACrC,QAAQ,YAAY,EAAE,QAAQ,CAAC;AAGjC,QAAM,gBAAgB,QAAQ;AAC9B,QAAM,sBACJ,OAAO,uBACP,iCAAiC;AAAA,IAC/B,SAAS,UAAU;AAAA,IACnB,iBAAiB,UAAU;AAAA,IAC3B;AAAA,IACA,SAAS,OAAO;AAAA,IAChB,YAAY,QAAQ;AAAA,IACpB;AAAA,EACF,CAAC;AAIH,QAAM,gBAAgB,OAAO,QAAQ,WAAW;AAChD,QAAM,wBACJ,UAAU,sBAAsB,UAC9B,aACF,KAAK;AACP,MAAI,CAAC,OAAO,QAAQ,kBAAkB,CAAC,uBAAuB;AAC5D,UAAM,IAAI;AAAA,MACR,sEAAsE,aAAa;AAAA,IAErF;AAAA,EACF;AACA,QAAM,SAA8B;AAAA,IAClC,QACE,OAAO,QAAQ,UACf,0BAA0B,UAAU,gBAAgB;AAAA,IACtD,gBACE,OAAO,QAAQ,kBAAmB;AAAA,IACpC,SAAS;AAAA,IACT,aAAa,OAAO,QAAQ;AAAA,IAC5B;AAAA,EACF;AAEA,SAAO;AAAA,IACL,YAAY,QAAQ;AAAA,IAEpB,gBAAwB;AACtB,aAAO,QAAQ;AAAA,IACjB;AAAA,IAEA,iBAA8B;AAC5B,aAAO;AAAA,QACL,IAAI,OAAO,IAAI;AAAA,QACf,MAAM,OAAO,IAAI;AAAA,QACjB,aAAa,OAAO,IAAI;AAAA,QACxB,SAAS,QAAQ;AAAA,MACnB;AAAA,IACF;AAAA,IAEA,MAAM,oBAAoB,OAA+B;AACvD,aAAO,oBAAoB,oBAAoB;AAAA,QAC7C,YAAY,QAAQ;AAAA,QACpB,KAAK,OAAO;AAAA,QACZ,QAAQ,OAAO;AAAA,QACf,QAAQ,OAAO;AAAA,QACf,WAAW,MAAM;AAAA,QACjB;AAAA,QACA,GAAI,MAAM,uBAAuB,SAC7B,EAAE,oBAAoB,MAAM,mBAAmB,IAC/C,CAAC;AAAA,QACL,GAAI,MAAM,mBAAmB,SACzB,EAAE,gBAAgB,MAAM,eAAe,IACvC,CAAC;AAAA,MACP,CAAC;AAAA,IACH;AAAA,IAEA,MAAM,uBACJ,WAC8B;AAC9B,aAAO,oBAAoB,uBAAuB,SAAS;AAAA,IAC7D;AAAA,IAEA,MAAM,iBAA8B,OAID;AACjC,YAAM,SAAS,MAAM,iBAAiB,MAAM,SAAS;AACrD,YAAM,QAAQ,sBAAsB,QAAQ,MAAM,KAAK;AAEvD,YAAM,SAAS,MAAM,UAAa,QAAQ,KAAK;AAC/C,UAAI,MAAM,gBAAgB,OAAO;AAC/B,cAAM,mBAAmB,MAAM,SAAS;AAAA,MAC1C;AACA,aAAO;AAAA,IACT;AAAA,IAEA,MAAM,oBAAiC,OAEF;AACnC,YAAM,SAAS,MAAM,iBAAiB,MAAM,SAAS;AACrD,YAAM,SAAS,eAAe,MAAM;AAEpC,YAAM,UAAiD,CAAC;AACxD,YAAM,SAAgC,CAAC;AAIvC,iBAAW,SAAS,QAAQ;AAC1B,YAAI;AACF,kBAAQ,KAAK,IAAI,MAAM,UAAa,QAAQ,KAAK;AAAA,QACnD,SAAS,OAAO;AACd,iBAAO,KAAK,IACV,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC;AAAA,QAC5D;AAAA,MACF;AAMA,UAAI,OAAO,KAAK,MAAM,EAAE,WAAW,GAAG;AACpC,cAAM,mBAAmB,MAAM,SAAS;AAAA,MAC1C;AAEA,aAAO,EAAE,SAAS,OAAO;AAAA,IAC3B;AAAA,EACF;AAEA,iBAAe,iBACb,WAC8B;AAC9B,UAAM,SAAS,MAAM,oBAAoB,uBAAuB,SAAS;AAIzE,QACE,CAAC,kBAAkB,OAAO,MAAM,KAChC,CAAC,OAAO,qBACR,CAAC,OAAO,WACR,eAAe,MAAM,EAAE,WAAW,GAClC;AACA,YAAM,IAAI;AAAA,QACR;AAAA,QACA;AAAA,UACE;AAAA,UACA,QAAQ,OAAO;AAAA,UACf,sBAAsB,QAAQ,OAAO,iBAAiB;AAAA,UACtD,YAAY,QAAQ,OAAO,OAAO;AAAA,UAClC,UAAU,eAAe,MAAM,EAAE,SAAS;AAAA,QAC5C;AAAA,MACF;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAGA,WAAS,eAAe,QAAuC;AAC7D,QAAI,OAAO,UAAU,OAAO,OAAO,SAAS,EAAG,QAAO,OAAO;AAC7D,WAAO,OAAO,QAAQ,CAAC,OAAO,KAAK,IAAI,CAAC;AAAA,EAC1C;AAMA,WAAS,sBACP,QACA,WACQ;AACR,UAAM,SAAS,eAAe,MAAM;AACpC,QAAI,cAAc,OAAW,QAAO,OAAO,CAAC;AAC5C,QAAI,CAAC,OAAO,SAAS,SAAS,GAAG;AAC/B,YAAM,IAAI;AAAA,QACR,UAAU,SAAS;AAAA,QACnB,EAAE,gBAAgB,WAAW,gBAAgB,OAAO;AAAA,MACtD;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,iBAAe,UACb,QACA,OACgC;AAChC,UAAM,SAAS,MAAM,uBAAuB;AAAA,MAC1C,mBAAmB,OAAO;AAAA,MAC1B;AAAA,MACA,SAAS,OAAO;AAAA,MAChB,cAAc,QAAQ;AAAA,MACtB;AAAA,MACA;AAAA,MACA,SAAS,OAAO;AAAA,MAChB,gBAAgB,OAAO;AAAA,IACzB,CAAC;AACD,WAAO,EAAE,OAAO,MAAM,OAAO,MAAW,SAAS,OAAO,QAAQ;AAAA,EAClE;AAEA,iBAAe,mBAAmB,WAAkC;AAClE,QAAI;AACF,YAAM,oBAAoB,kBAAkB,SAAS;AAAA,IACvD,QAAQ;AAAA,IAER;AAAA,EACF;AACF;","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":[]}