@opendatalabs/vana-sdk 3.13.1 → 3.13.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/direct/access-request-client.cjs +1 -0
- package/dist/direct/access-request-client.cjs.map +1 -1
- package/dist/direct/access-request-client.js +1 -0
- package/dist/direct/access-request-client.js.map +1 -1
- package/dist/direct/connect-flow.cjs +1 -1
- package/dist/direct/connect-flow.cjs.map +1 -1
- package/dist/direct/connect-flow.js +1 -1
- package/dist/direct/connect-flow.js.map +1 -1
- package/dist/direct/controller.cjs.map +1 -1
- package/dist/direct/controller.js.map +1 -1
- package/dist/direct/endpoints.cjs +1 -1
- package/dist/direct/endpoints.cjs.map +1 -1
- package/dist/direct/endpoints.js +1 -1
- package/dist/direct/endpoints.js.map +1 -1
- package/dist/direct/types.cjs.map +1 -1
- package/dist/direct/types.d.ts +13 -2
- package/dist/direct/types.js.map +1 -1
- package/package.json +1 -1
|
@@ -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} 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 /** `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\nconst VALID_STATUSES: readonly AccessRequestStatusValue[] = [\n \"pending\",\n \"approved\",\n \"ready_for_read\",\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 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 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 });\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 };\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 };\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 };\n return {\n status: normalizeStatus(body.status),\n personalServerUrl: body.personalServerUrl,\n grantId: body.grantId,\n scope: body.scope,\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 AccessRequestStatus,\n AccessRequestStatusValue,\n} 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 /** `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\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 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 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 });\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 };\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 };\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 };\n return {\n status: normalizeStatus(body.status),\n personalServerUrl: body.personalServerUrl,\n grantId: body.grantId,\n scope: body.scope,\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 +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} 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 /** `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\nconst VALID_STATUSES: readonly AccessRequestStatusValue[] = [\n \"pending\",\n \"approved\",\n \"ready_for_read\",\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 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 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 });\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 };\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 };\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 };\n return {\n status: normalizeStatus(body.status),\n personalServerUrl: body.personalServerUrl,\n grantId: body.grantId,\n scope: body.scope,\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 AccessRequestStatus,\n AccessRequestStatusValue,\n} 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 /** `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\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 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 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 });\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 };\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 };\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 };\n return {\n status: normalizeStatus(body.status),\n personalServerUrl: body.personalServerUrl,\n grantId: body.grantId,\n scope: body.scope,\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":[]}
|
|
@@ -134,7 +134,7 @@ function createDirectConnectFlow(transports, options = {}) {
|
|
|
134
134
|
await readAndFinish(request);
|
|
135
135
|
return;
|
|
136
136
|
}
|
|
137
|
-
if (status.status === "denied" || status.status === "expired") {
|
|
137
|
+
if (status.status === "completed" || status.status === "denied" || status.status === "expired") {
|
|
138
138
|
running = false;
|
|
139
139
|
setState({
|
|
140
140
|
type: "error",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/direct/connect-flow.ts"],"sourcesContent":["/**\n * Framework-agnostic connect-flow state machine for the browser two-tab helper.\n *\n * @remarks\n * This is the testable core behind {@link useDirectVanaConnect}. It is pure\n * TypeScript (no React, no DOM-only APIs beyond an injectable window opener and\n * timers) so the full flow — create request, open Vana, poll status, read data —\n * can be exercised in a Node test environment.\n *\n * The React hook is a thin `useSyncExternalStore` binding over this store.\n *\n * @category Direct\n * @module direct/connect-flow\n */\n\nimport type {\n AccessRequest,\n AccessRequestStatus,\n AccessRequestStatusValue,\n ApprovedDataResult,\n} from \"./types\";\n\n/**\n * Caller-supplied transports. These typically `fetch` the app's own backend\n * routes, which in turn delegate to a {@link DirectDataController}.\n */\nexport interface DirectConnectTransports<T = unknown> {\n /** Ask the backend to create an access request. */\n createRequest: () => Promise<AccessRequest>;\n /** Ask the backend for the current status of a request. */\n getStatus: (requestId: string) => Promise<AccessRequestStatus>;\n /** Ask the backend to read the approved data. */\n readResult: (requestId: string) => Promise<ApprovedDataResult<T>>;\n}\n\n/**\n * A handle to a tab opened synchronously under the user's click gesture.\n *\n * @remarks\n * The flow opens this tab *before* it knows the approval URL (popup blockers\n * only allow `window.open()` during the click's transient activation), then\n * navigates it once `createRequest` resolves.\n */\nexport interface ConnectWindow {\n /** Point the already-open tab at the approval URL. */\n navigate(url: string): void;\n /** Close the tab (used to clean up an un-navigated tab on failure/reset). */\n close(): void;\n}\n\n/** Tunables for the connect flow. */\nexport interface DirectConnectOptions {\n /** Status poll interval in ms. Defaults to 1500. */\n pollIntervalMs?: number;\n /** Overall timeout in ms before giving up. Defaults to 300000 (5 min). */\n timeoutMs?: number;\n /**\n * Synchronously open a blank tab under the click's transient activation and\n * return a handle to navigate later, or `null` if the browser blocked it.\n * Defaults to `window.open(\"\", \"_blank\")` (with `opener` severed). Injectable\n * for tests.\n *\n * @remarks\n * Renamed from the pre-3.8 `openWindow?: (url) => void`. The old contract was\n * the BUI-622 bug itself (it was called with the URL *after* an `await`, so\n * the popup blocker suppressed it); it cannot be preserved while fixing the\n * bug. Custom openers must now open synchronously and return a navigable\n * handle.\n */\n openApprovalWindow?: () => ConnectWindow | null;\n /** `setTimeout`. Injectable for tests. Defaults to `globalThis.setTimeout`. */\n setTimeoutFn?: (cb: () => void, ms: number) => unknown;\n /** `clearTimeout`. Injectable for tests. Defaults to `globalThis.clearTimeout`. */\n clearTimeoutFn?: (handle: unknown) => void;\n /** Clock source in ms. Injectable for tests. Defaults to `Date.now`. */\n now?: () => number;\n}\n\n/**\n * Discriminated connect-flow state.\n *\n * @remarks\n * `type` matches the builder guide: it starts at `\"idle\"` and is non-idle while\n * connecting. The intermediate phases give richer UIs something to render.\n */\nexport type DirectConnectState<T = unknown> =\n | { type: \"idle\" }\n | { type: \"creating\" }\n | {\n type: \"awaiting_approval\";\n request: AccessRequest;\n /**\n * `true` when the browser blocked the approval popup. The UI should\n * render `request.approvalUrl` as a visible \"Open approval\" link so the\n * user can open it manually instead of the flow silently hanging.\n */\n popupBlocked: boolean;\n }\n | { type: \"reading\"; request: AccessRequest }\n | { type: \"done\"; result: ApprovedDataResult<T> }\n | { type: \"error\"; error: Error };\n\n/** The store returned by {@link createDirectConnectFlow}. */\nexport interface DirectConnectFlow<T = unknown> {\n /** Current state. */\n getState(): DirectConnectState<T>;\n /** Subscribe to state changes; returns an unsubscribe function. */\n subscribe(listener: () => void): () => void;\n /** Begin the flow. No-op if already running. */\n start(): Promise<void>;\n /** Reset to `idle` and stop any in-flight polling. */\n reset(): void;\n}\n\nconst DEFAULT_POLL_INTERVAL_MS = 1500;\nconst DEFAULT_TIMEOUT_MS = 300_000;\n\nfunction toError(value: unknown): Error {\n return value instanceof Error ? value : new Error(String(value));\n}\n\nfunction isReadReadyStatus(status: AccessRequestStatusValue): boolean {\n return status === \"approved\" || status === \"ready_for_read\";\n}\n\n/**\n * Default {@link DirectConnectOptions.openApprovalWindow}: open a blank tab\n * synchronously (inside the click gesture) and return a handle to navigate\n * once the approval URL is known. Returns `null` when blocked or non-DOM.\n */\nfunction defaultOpenApprovalWindow(): ConnectWindow | null {\n if (typeof window === \"undefined\" || !window.open) return null;\n // We can't pass the \"noopener\"/\"noreferrer\" feature string here: it makes\n // window.open() return null, which would throw away the handle we need to\n // navigate later. So we open plain and re-create both protections by hand.\n const opened = window.open(\"\", \"_blank\");\n if (!opened) return null;\n // Sever the opener link while the tab is still about:blank, so the approval\n // page can't reach back into the app (reverse tab-nabbing).\n try {\n opened.opener = null;\n } catch {\n // Some environments make `opener` read-only; best-effort only.\n }\n return {\n navigate(url: string) {\n // Restore the no-referrer protection the old \"noreferrer\" feature gave:\n // tag the blank document so the upcoming navigation sends no Referer to\n // the approval page (best-effort; the blank doc is same-origin here).\n try {\n const meta = opened.document.createElement(\"meta\");\n meta.name = \"referrer\";\n meta.content = \"no-referrer\";\n (opened.document.head ?? opened.document.documentElement)?.appendChild(\n meta,\n );\n } catch {\n // Cross-origin/unavailable document: skip, navigation still proceeds.\n }\n opened.location.href = url;\n },\n close() {\n opened.close();\n },\n };\n}\n\n/**\n * Create a connect-flow store.\n *\n * @param transports - Backend transports (`createRequest`, `getStatus`, `readResult`).\n * @param options - Polling/timeout tunables and injectable side effects.\n * @returns A {@link DirectConnectFlow} store.\n */\nexport function createDirectConnectFlow<T = unknown>(\n transports: DirectConnectTransports<T>,\n options: DirectConnectOptions = {},\n): DirectConnectFlow<T> {\n const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n // Resolved lazily at start() (see below) so a custom opener swapped in after\n // construction is still honoured — matching the latest-callback pattern the\n // React hook uses for its transports.\n const setTimeoutFn =\n options.setTimeoutFn ??\n ((cb: () => void, ms: number) => globalThis.setTimeout(cb, ms));\n const clearTimeoutFn =\n options.clearTimeoutFn ??\n ((handle: unknown) => {\n globalThis.clearTimeout(handle as never);\n });\n const now = options.now ?? (() => Date.now());\n\n let state: DirectConnectState<T> = { type: \"idle\" };\n const listeners = new Set<() => void>();\n let pollHandle: unknown = null;\n let running = false;\n // Monotonic id for the current start() invocation. reset() (and an\n // immediately following start()) bumps it, so a previous run whose async\n // createRequest is still in flight can detect it has been superseded and\n // avoid touching shared state / the newer run's tab.\n let activeRunId = 0;\n // Holds the tab we opened only while it is still blank (un-navigated). Once\n // navigated to the approval URL we drop the reference so reset/cleanup never\n // closes the live approval tab the user is interacting with.\n let openedWindow: ConnectWindow | null = null;\n\n function emit(): void {\n for (const listener of listeners) listener();\n }\n\n function setState(next: DirectConnectState<T>): void {\n state = next;\n emit();\n }\n\n function clearPoll(): void {\n if (pollHandle !== null) {\n clearTimeoutFn(pollHandle);\n pollHandle = null;\n }\n }\n\n /** Close the opened tab if it is still blank (never navigated). */\n function closeUnnavigatedWindow(): void {\n if (openedWindow) {\n openedWindow.close();\n openedWindow = null;\n }\n }\n\n function isRunningPhase(): boolean {\n return (\n state.type === \"creating\" ||\n state.type === \"awaiting_approval\" ||\n state.type === \"reading\"\n );\n }\n\n async function readAndFinish(request: AccessRequest): Promise<void> {\n setState({ type: \"reading\", request });\n try {\n const result = await transports.readResult(request.requestId);\n if (!running) return;\n setState({ type: \"done\", result });\n } catch (err) {\n if (!running) return;\n setState({ type: \"error\", error: toError(err) });\n } finally {\n running = false;\n }\n }\n\n function scheduleNextPoll(request: AccessRequest, deadline: number): void {\n pollHandle = setTimeoutFn(() => {\n void poll(request, deadline);\n }, pollIntervalMs);\n }\n\n async function poll(request: AccessRequest, deadline: number): Promise<void> {\n if (!running) return;\n if (now() >= deadline) {\n running = false;\n setState({\n type: \"error\",\n error: new Error(\"Timed out waiting for approval\"),\n });\n return;\n }\n let status: AccessRequestStatus;\n try {\n status = await transports.getStatus(request.requestId);\n } catch (err) {\n if (!running) return;\n running = false;\n setState({ type: \"error\", error: toError(err) });\n return;\n }\n if (!running) return;\n\n if (isReadReadyStatus(status.status)) {\n clearPoll();\n await readAndFinish(request);\n return;\n }\n if (status.status === \"denied\" || status.status === \"expired\") {\n running = false;\n setState({\n type: \"error\",\n error: new Error(`Access request ${status.status}`),\n });\n return;\n }\n scheduleNextPoll(request, deadline);\n }\n\n return {\n getState() {\n return state;\n },\n\n subscribe(listener: () => void) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n\n async start(): Promise<void> {\n if (running || isRunningPhase()) return;\n running = true;\n const runId = ++activeRunId;\n\n // Open the approval tab *synchronously*, while the click's transient\n // activation is still live. The approval URL isn't known yet (it comes\n // from createRequest below), so open a blank tab now and navigate it\n // once the URL arrives. Opening *after* the await — as this flow used to\n // — runs outside the gesture, so the browser suppresses it as an\n // unsolicited popup and the flow stalls forever (BUI-622).\n // Read the opener option *now* (not at construction) so a custom opener\n // swapped in after the flow was created is still used.\n const openApprovalWindow =\n options.openApprovalWindow ?? defaultOpenApprovalWindow;\n const approvalWindow = openApprovalWindow();\n openedWindow = approvalWindow;\n\n setState({ type: \"creating\" });\n\n let request: AccessRequest;\n try {\n request = await transports.createRequest();\n } catch (err) {\n // If we were superseded (reset, possibly + a newer start()) while this\n // request was in flight, only clean up our own tab — never the shared\n // state or the newer run's window.\n if (runId !== activeRunId) {\n approvalWindow?.close();\n return;\n }\n running = false;\n closeUnnavigatedWindow();\n setState({ type: \"error\", error: toError(err) });\n return;\n }\n if (runId !== activeRunId) {\n approvalWindow?.close();\n return;\n }\n\n if (approvalWindow) {\n approvalWindow.navigate(request.approvalUrl);\n // Hand the tab off to the user; we no longer own/close it.\n openedWindow = null;\n }\n // `approvalWindow === null` means the popup was blocked. Surface it so\n // the UI renders request.approvalUrl as a visible \"Open approval\" link\n // instead of hanging. We poll either way, so a manual open still\n // resolves the flow, and the timeout still bounds the wait.\n setState({\n type: \"awaiting_approval\",\n request,\n popupBlocked: approvalWindow === null,\n });\n\n const deadline = now() + timeoutMs;\n scheduleNextPoll(request, deadline);\n },\n\n reset(): void {\n running = false;\n // Invalidate any in-flight start() so a late createRequest can't clobber\n // a subsequent run.\n activeRunId++;\n clearPoll();\n closeUnnavigatedWindow();\n setState({ type: \"idle\" });\n },\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/direct/connect-flow.ts"],"sourcesContent":["/**\n * Framework-agnostic connect-flow state machine for the browser two-tab helper.\n *\n * @remarks\n * This is the testable core behind {@link useDirectVanaConnect}. It is pure\n * TypeScript (no React, no DOM-only APIs beyond an injectable window opener and\n * timers) so the full flow — create request, open Vana, poll status, read data —\n * can be exercised in a Node test environment.\n *\n * The React hook is a thin `useSyncExternalStore` binding over this store.\n *\n * @category Direct\n * @module direct/connect-flow\n */\n\nimport type {\n AccessRequest,\n AccessRequestStatus,\n AccessRequestStatusValue,\n ApprovedDataResult,\n} from \"./types\";\n\n/**\n * Caller-supplied transports. These typically `fetch` the app's own backend\n * routes, which in turn delegate to a {@link DirectDataController}.\n */\nexport interface DirectConnectTransports<T = unknown> {\n /** Ask the backend to create an access request. */\n createRequest: () => Promise<AccessRequest>;\n /** Ask the backend for the current status of a request. */\n getStatus: (requestId: string) => Promise<AccessRequestStatus>;\n /** Ask the backend to read the approved data. */\n readResult: (requestId: string) => Promise<ApprovedDataResult<T>>;\n}\n\n/**\n * A handle to a tab opened synchronously under the user's click gesture.\n *\n * @remarks\n * The flow opens this tab *before* it knows the approval URL (popup blockers\n * only allow `window.open()` during the click's transient activation), then\n * navigates it once `createRequest` resolves.\n */\nexport interface ConnectWindow {\n /** Point the already-open tab at the approval URL. */\n navigate(url: string): void;\n /** Close the tab (used to clean up an un-navigated tab on failure/reset). */\n close(): void;\n}\n\n/** Tunables for the connect flow. */\nexport interface DirectConnectOptions {\n /** Status poll interval in ms. Defaults to 1500. */\n pollIntervalMs?: number;\n /** Overall timeout in ms before giving up. Defaults to 300000 (5 min). */\n timeoutMs?: number;\n /**\n * Synchronously open a blank tab under the click's transient activation and\n * return a handle to navigate later, or `null` if the browser blocked it.\n * Defaults to `window.open(\"\", \"_blank\")` (with `opener` severed). Injectable\n * for tests.\n *\n * @remarks\n * Renamed from the pre-3.8 `openWindow?: (url) => void`. The old contract was\n * the BUI-622 bug itself (it was called with the URL *after* an `await`, so\n * the popup blocker suppressed it); it cannot be preserved while fixing the\n * bug. Custom openers must now open synchronously and return a navigable\n * handle.\n */\n openApprovalWindow?: () => ConnectWindow | null;\n /** `setTimeout`. Injectable for tests. Defaults to `globalThis.setTimeout`. */\n setTimeoutFn?: (cb: () => void, ms: number) => unknown;\n /** `clearTimeout`. Injectable for tests. Defaults to `globalThis.clearTimeout`. */\n clearTimeoutFn?: (handle: unknown) => void;\n /** Clock source in ms. Injectable for tests. Defaults to `Date.now`. */\n now?: () => number;\n}\n\n/**\n * Discriminated connect-flow state.\n *\n * @remarks\n * `type` matches the builder guide: it starts at `\"idle\"` and is non-idle while\n * connecting. The intermediate phases give richer UIs something to render.\n */\nexport type DirectConnectState<T = unknown> =\n | { type: \"idle\" }\n | { type: \"creating\" }\n | {\n type: \"awaiting_approval\";\n request: AccessRequest;\n /**\n * `true` when the browser blocked the approval popup. The UI should\n * render `request.approvalUrl` as a visible \"Open approval\" link so the\n * user can open it manually instead of the flow silently hanging.\n */\n popupBlocked: boolean;\n }\n | { type: \"reading\"; request: AccessRequest }\n | { type: \"done\"; result: ApprovedDataResult<T> }\n | { type: \"error\"; error: Error };\n\n/** The store returned by {@link createDirectConnectFlow}. */\nexport interface DirectConnectFlow<T = unknown> {\n /** Current state. */\n getState(): DirectConnectState<T>;\n /** Subscribe to state changes; returns an unsubscribe function. */\n subscribe(listener: () => void): () => void;\n /** Begin the flow. No-op if already running. */\n start(): Promise<void>;\n /** Reset to `idle` and stop any in-flight polling. */\n reset(): void;\n}\n\nconst DEFAULT_POLL_INTERVAL_MS = 1500;\nconst DEFAULT_TIMEOUT_MS = 300_000;\n\nfunction toError(value: unknown): Error {\n return value instanceof Error ? value : new Error(String(value));\n}\n\nfunction isReadReadyStatus(status: AccessRequestStatusValue): boolean {\n return status === \"approved\" || status === \"ready_for_read\";\n}\n\n/**\n * Default {@link DirectConnectOptions.openApprovalWindow}: open a blank tab\n * synchronously (inside the click gesture) and return a handle to navigate\n * once the approval URL is known. Returns `null` when blocked or non-DOM.\n */\nfunction defaultOpenApprovalWindow(): ConnectWindow | null {\n if (typeof window === \"undefined\" || !window.open) return null;\n // We can't pass the \"noopener\"/\"noreferrer\" feature string here: it makes\n // window.open() return null, which would throw away the handle we need to\n // navigate later. So we open plain and re-create both protections by hand.\n const opened = window.open(\"\", \"_blank\");\n if (!opened) return null;\n // Sever the opener link while the tab is still about:blank, so the approval\n // page can't reach back into the app (reverse tab-nabbing).\n try {\n opened.opener = null;\n } catch {\n // Some environments make `opener` read-only; best-effort only.\n }\n return {\n navigate(url: string) {\n // Restore the no-referrer protection the old \"noreferrer\" feature gave:\n // tag the blank document so the upcoming navigation sends no Referer to\n // the approval page (best-effort; the blank doc is same-origin here).\n try {\n const meta = opened.document.createElement(\"meta\");\n meta.name = \"referrer\";\n meta.content = \"no-referrer\";\n (opened.document.head ?? opened.document.documentElement)?.appendChild(\n meta,\n );\n } catch {\n // Cross-origin/unavailable document: skip, navigation still proceeds.\n }\n opened.location.href = url;\n },\n close() {\n opened.close();\n },\n };\n}\n\n/**\n * Create a connect-flow store.\n *\n * @param transports - Backend transports (`createRequest`, `getStatus`, `readResult`).\n * @param options - Polling/timeout tunables and injectable side effects.\n * @returns A {@link DirectConnectFlow} store.\n */\nexport function createDirectConnectFlow<T = unknown>(\n transports: DirectConnectTransports<T>,\n options: DirectConnectOptions = {},\n): DirectConnectFlow<T> {\n const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n // Resolved lazily at start() (see below) so a custom opener swapped in after\n // construction is still honoured — matching the latest-callback pattern the\n // React hook uses for its transports.\n const setTimeoutFn =\n options.setTimeoutFn ??\n ((cb: () => void, ms: number) => globalThis.setTimeout(cb, ms));\n const clearTimeoutFn =\n options.clearTimeoutFn ??\n ((handle: unknown) => {\n globalThis.clearTimeout(handle as never);\n });\n const now = options.now ?? (() => Date.now());\n\n let state: DirectConnectState<T> = { type: \"idle\" };\n const listeners = new Set<() => void>();\n let pollHandle: unknown = null;\n let running = false;\n // Monotonic id for the current start() invocation. reset() (and an\n // immediately following start()) bumps it, so a previous run whose async\n // createRequest is still in flight can detect it has been superseded and\n // avoid touching shared state / the newer run's tab.\n let activeRunId = 0;\n // Holds the tab we opened only while it is still blank (un-navigated). Once\n // navigated to the approval URL we drop the reference so reset/cleanup never\n // closes the live approval tab the user is interacting with.\n let openedWindow: ConnectWindow | null = null;\n\n function emit(): void {\n for (const listener of listeners) listener();\n }\n\n function setState(next: DirectConnectState<T>): void {\n state = next;\n emit();\n }\n\n function clearPoll(): void {\n if (pollHandle !== null) {\n clearTimeoutFn(pollHandle);\n pollHandle = null;\n }\n }\n\n /** Close the opened tab if it is still blank (never navigated). */\n function closeUnnavigatedWindow(): void {\n if (openedWindow) {\n openedWindow.close();\n openedWindow = null;\n }\n }\n\n function isRunningPhase(): boolean {\n return (\n state.type === \"creating\" ||\n state.type === \"awaiting_approval\" ||\n state.type === \"reading\"\n );\n }\n\n async function readAndFinish(request: AccessRequest): Promise<void> {\n setState({ type: \"reading\", request });\n try {\n const result = await transports.readResult(request.requestId);\n if (!running) return;\n setState({ type: \"done\", result });\n } catch (err) {\n if (!running) return;\n setState({ type: \"error\", error: toError(err) });\n } finally {\n running = false;\n }\n }\n\n function scheduleNextPoll(request: AccessRequest, deadline: number): void {\n pollHandle = setTimeoutFn(() => {\n void poll(request, deadline);\n }, pollIntervalMs);\n }\n\n async function poll(request: AccessRequest, deadline: number): Promise<void> {\n if (!running) return;\n if (now() >= deadline) {\n running = false;\n setState({\n type: \"error\",\n error: new Error(\"Timed out waiting for approval\"),\n });\n return;\n }\n let status: AccessRequestStatus;\n try {\n status = await transports.getStatus(request.requestId);\n } catch (err) {\n if (!running) return;\n running = false;\n setState({ type: \"error\", error: toError(err) });\n return;\n }\n if (!running) return;\n\n if (isReadReadyStatus(status.status)) {\n clearPoll();\n await readAndFinish(request);\n return;\n }\n if (\n status.status === \"completed\" ||\n status.status === \"denied\" ||\n status.status === \"expired\"\n ) {\n running = false;\n setState({\n type: \"error\",\n error: new Error(`Access request ${status.status}`),\n });\n return;\n }\n scheduleNextPoll(request, deadline);\n }\n\n return {\n getState() {\n return state;\n },\n\n subscribe(listener: () => void) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n\n async start(): Promise<void> {\n if (running || isRunningPhase()) return;\n running = true;\n const runId = ++activeRunId;\n\n // Open the approval tab *synchronously*, while the click's transient\n // activation is still live. The approval URL isn't known yet (it comes\n // from createRequest below), so open a blank tab now and navigate it\n // once the URL arrives. Opening *after* the await — as this flow used to\n // — runs outside the gesture, so the browser suppresses it as an\n // unsolicited popup and the flow stalls forever (BUI-622).\n // Read the opener option *now* (not at construction) so a custom opener\n // swapped in after the flow was created is still used.\n const openApprovalWindow =\n options.openApprovalWindow ?? defaultOpenApprovalWindow;\n const approvalWindow = openApprovalWindow();\n openedWindow = approvalWindow;\n\n setState({ type: \"creating\" });\n\n let request: AccessRequest;\n try {\n request = await transports.createRequest();\n } catch (err) {\n // If we were superseded (reset, possibly + a newer start()) while this\n // request was in flight, only clean up our own tab — never the shared\n // state or the newer run's window.\n if (runId !== activeRunId) {\n approvalWindow?.close();\n return;\n }\n running = false;\n closeUnnavigatedWindow();\n setState({ type: \"error\", error: toError(err) });\n return;\n }\n if (runId !== activeRunId) {\n approvalWindow?.close();\n return;\n }\n\n if (approvalWindow) {\n approvalWindow.navigate(request.approvalUrl);\n // Hand the tab off to the user; we no longer own/close it.\n openedWindow = null;\n }\n // `approvalWindow === null` means the popup was blocked. Surface it so\n // the UI renders request.approvalUrl as a visible \"Open approval\" link\n // instead of hanging. We poll either way, so a manual open still\n // resolves the flow, and the timeout still bounds the wait.\n setState({\n type: \"awaiting_approval\",\n request,\n popupBlocked: approvalWindow === null,\n });\n\n const deadline = now() + timeoutMs;\n scheduleNextPoll(request, deadline);\n },\n\n reset(): void {\n running = false;\n // Invalidate any in-flight start() so a late createRequest can't clobber\n // a subsequent run.\n activeRunId++;\n clearPoll();\n closeUnnavigatedWindow();\n setState({ type: \"idle\" });\n },\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -111,7 +111,7 @@ function createDirectConnectFlow(transports, options = {}) {
|
|
|
111
111
|
await readAndFinish(request);
|
|
112
112
|
return;
|
|
113
113
|
}
|
|
114
|
-
if (status.status === "denied" || status.status === "expired") {
|
|
114
|
+
if (status.status === "completed" || status.status === "denied" || status.status === "expired") {
|
|
115
115
|
running = false;
|
|
116
116
|
setState({
|
|
117
117
|
type: "error",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/direct/connect-flow.ts"],"sourcesContent":["/**\n * Framework-agnostic connect-flow state machine for the browser two-tab helper.\n *\n * @remarks\n * This is the testable core behind {@link useDirectVanaConnect}. It is pure\n * TypeScript (no React, no DOM-only APIs beyond an injectable window opener and\n * timers) so the full flow — create request, open Vana, poll status, read data —\n * can be exercised in a Node test environment.\n *\n * The React hook is a thin `useSyncExternalStore` binding over this store.\n *\n * @category Direct\n * @module direct/connect-flow\n */\n\nimport type {\n AccessRequest,\n AccessRequestStatus,\n AccessRequestStatusValue,\n ApprovedDataResult,\n} from \"./types\";\n\n/**\n * Caller-supplied transports. These typically `fetch` the app's own backend\n * routes, which in turn delegate to a {@link DirectDataController}.\n */\nexport interface DirectConnectTransports<T = unknown> {\n /** Ask the backend to create an access request. */\n createRequest: () => Promise<AccessRequest>;\n /** Ask the backend for the current status of a request. */\n getStatus: (requestId: string) => Promise<AccessRequestStatus>;\n /** Ask the backend to read the approved data. */\n readResult: (requestId: string) => Promise<ApprovedDataResult<T>>;\n}\n\n/**\n * A handle to a tab opened synchronously under the user's click gesture.\n *\n * @remarks\n * The flow opens this tab *before* it knows the approval URL (popup blockers\n * only allow `window.open()` during the click's transient activation), then\n * navigates it once `createRequest` resolves.\n */\nexport interface ConnectWindow {\n /** Point the already-open tab at the approval URL. */\n navigate(url: string): void;\n /** Close the tab (used to clean up an un-navigated tab on failure/reset). */\n close(): void;\n}\n\n/** Tunables for the connect flow. */\nexport interface DirectConnectOptions {\n /** Status poll interval in ms. Defaults to 1500. */\n pollIntervalMs?: number;\n /** Overall timeout in ms before giving up. Defaults to 300000 (5 min). */\n timeoutMs?: number;\n /**\n * Synchronously open a blank tab under the click's transient activation and\n * return a handle to navigate later, or `null` if the browser blocked it.\n * Defaults to `window.open(\"\", \"_blank\")` (with `opener` severed). Injectable\n * for tests.\n *\n * @remarks\n * Renamed from the pre-3.8 `openWindow?: (url) => void`. The old contract was\n * the BUI-622 bug itself (it was called with the URL *after* an `await`, so\n * the popup blocker suppressed it); it cannot be preserved while fixing the\n * bug. Custom openers must now open synchronously and return a navigable\n * handle.\n */\n openApprovalWindow?: () => ConnectWindow | null;\n /** `setTimeout`. Injectable for tests. Defaults to `globalThis.setTimeout`. */\n setTimeoutFn?: (cb: () => void, ms: number) => unknown;\n /** `clearTimeout`. Injectable for tests. Defaults to `globalThis.clearTimeout`. */\n clearTimeoutFn?: (handle: unknown) => void;\n /** Clock source in ms. Injectable for tests. Defaults to `Date.now`. */\n now?: () => number;\n}\n\n/**\n * Discriminated connect-flow state.\n *\n * @remarks\n * `type` matches the builder guide: it starts at `\"idle\"` and is non-idle while\n * connecting. The intermediate phases give richer UIs something to render.\n */\nexport type DirectConnectState<T = unknown> =\n | { type: \"idle\" }\n | { type: \"creating\" }\n | {\n type: \"awaiting_approval\";\n request: AccessRequest;\n /**\n * `true` when the browser blocked the approval popup. The UI should\n * render `request.approvalUrl` as a visible \"Open approval\" link so the\n * user can open it manually instead of the flow silently hanging.\n */\n popupBlocked: boolean;\n }\n | { type: \"reading\"; request: AccessRequest }\n | { type: \"done\"; result: ApprovedDataResult<T> }\n | { type: \"error\"; error: Error };\n\n/** The store returned by {@link createDirectConnectFlow}. */\nexport interface DirectConnectFlow<T = unknown> {\n /** Current state. */\n getState(): DirectConnectState<T>;\n /** Subscribe to state changes; returns an unsubscribe function. */\n subscribe(listener: () => void): () => void;\n /** Begin the flow. No-op if already running. */\n start(): Promise<void>;\n /** Reset to `idle` and stop any in-flight polling. */\n reset(): void;\n}\n\nconst DEFAULT_POLL_INTERVAL_MS = 1500;\nconst DEFAULT_TIMEOUT_MS = 300_000;\n\nfunction toError(value: unknown): Error {\n return value instanceof Error ? value : new Error(String(value));\n}\n\nfunction isReadReadyStatus(status: AccessRequestStatusValue): boolean {\n return status === \"approved\" || status === \"ready_for_read\";\n}\n\n/**\n * Default {@link DirectConnectOptions.openApprovalWindow}: open a blank tab\n * synchronously (inside the click gesture) and return a handle to navigate\n * once the approval URL is known. Returns `null` when blocked or non-DOM.\n */\nfunction defaultOpenApprovalWindow(): ConnectWindow | null {\n if (typeof window === \"undefined\" || !window.open) return null;\n // We can't pass the \"noopener\"/\"noreferrer\" feature string here: it makes\n // window.open() return null, which would throw away the handle we need to\n // navigate later. So we open plain and re-create both protections by hand.\n const opened = window.open(\"\", \"_blank\");\n if (!opened) return null;\n // Sever the opener link while the tab is still about:blank, so the approval\n // page can't reach back into the app (reverse tab-nabbing).\n try {\n opened.opener = null;\n } catch {\n // Some environments make `opener` read-only; best-effort only.\n }\n return {\n navigate(url: string) {\n // Restore the no-referrer protection the old \"noreferrer\" feature gave:\n // tag the blank document so the upcoming navigation sends no Referer to\n // the approval page (best-effort; the blank doc is same-origin here).\n try {\n const meta = opened.document.createElement(\"meta\");\n meta.name = \"referrer\";\n meta.content = \"no-referrer\";\n (opened.document.head ?? opened.document.documentElement)?.appendChild(\n meta,\n );\n } catch {\n // Cross-origin/unavailable document: skip, navigation still proceeds.\n }\n opened.location.href = url;\n },\n close() {\n opened.close();\n },\n };\n}\n\n/**\n * Create a connect-flow store.\n *\n * @param transports - Backend transports (`createRequest`, `getStatus`, `readResult`).\n * @param options - Polling/timeout tunables and injectable side effects.\n * @returns A {@link DirectConnectFlow} store.\n */\nexport function createDirectConnectFlow<T = unknown>(\n transports: DirectConnectTransports<T>,\n options: DirectConnectOptions = {},\n): DirectConnectFlow<T> {\n const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n // Resolved lazily at start() (see below) so a custom opener swapped in after\n // construction is still honoured — matching the latest-callback pattern the\n // React hook uses for its transports.\n const setTimeoutFn =\n options.setTimeoutFn ??\n ((cb: () => void, ms: number) => globalThis.setTimeout(cb, ms));\n const clearTimeoutFn =\n options.clearTimeoutFn ??\n ((handle: unknown) => {\n globalThis.clearTimeout(handle as never);\n });\n const now = options.now ?? (() => Date.now());\n\n let state: DirectConnectState<T> = { type: \"idle\" };\n const listeners = new Set<() => void>();\n let pollHandle: unknown = null;\n let running = false;\n // Monotonic id for the current start() invocation. reset() (and an\n // immediately following start()) bumps it, so a previous run whose async\n // createRequest is still in flight can detect it has been superseded and\n // avoid touching shared state / the newer run's tab.\n let activeRunId = 0;\n // Holds the tab we opened only while it is still blank (un-navigated). Once\n // navigated to the approval URL we drop the reference so reset/cleanup never\n // closes the live approval tab the user is interacting with.\n let openedWindow: ConnectWindow | null = null;\n\n function emit(): void {\n for (const listener of listeners) listener();\n }\n\n function setState(next: DirectConnectState<T>): void {\n state = next;\n emit();\n }\n\n function clearPoll(): void {\n if (pollHandle !== null) {\n clearTimeoutFn(pollHandle);\n pollHandle = null;\n }\n }\n\n /** Close the opened tab if it is still blank (never navigated). */\n function closeUnnavigatedWindow(): void {\n if (openedWindow) {\n openedWindow.close();\n openedWindow = null;\n }\n }\n\n function isRunningPhase(): boolean {\n return (\n state.type === \"creating\" ||\n state.type === \"awaiting_approval\" ||\n state.type === \"reading\"\n );\n }\n\n async function readAndFinish(request: AccessRequest): Promise<void> {\n setState({ type: \"reading\", request });\n try {\n const result = await transports.readResult(request.requestId);\n if (!running) return;\n setState({ type: \"done\", result });\n } catch (err) {\n if (!running) return;\n setState({ type: \"error\", error: toError(err) });\n } finally {\n running = false;\n }\n }\n\n function scheduleNextPoll(request: AccessRequest, deadline: number): void {\n pollHandle = setTimeoutFn(() => {\n void poll(request, deadline);\n }, pollIntervalMs);\n }\n\n async function poll(request: AccessRequest, deadline: number): Promise<void> {\n if (!running) return;\n if (now() >= deadline) {\n running = false;\n setState({\n type: \"error\",\n error: new Error(\"Timed out waiting for approval\"),\n });\n return;\n }\n let status: AccessRequestStatus;\n try {\n status = await transports.getStatus(request.requestId);\n } catch (err) {\n if (!running) return;\n running = false;\n setState({ type: \"error\", error: toError(err) });\n return;\n }\n if (!running) return;\n\n if (isReadReadyStatus(status.status)) {\n clearPoll();\n await readAndFinish(request);\n return;\n }\n if (status.status === \"denied\" || status.status === \"expired\") {\n running = false;\n setState({\n type: \"error\",\n error: new Error(`Access request ${status.status}`),\n });\n return;\n }\n scheduleNextPoll(request, deadline);\n }\n\n return {\n getState() {\n return state;\n },\n\n subscribe(listener: () => void) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n\n async start(): Promise<void> {\n if (running || isRunningPhase()) return;\n running = true;\n const runId = ++activeRunId;\n\n // Open the approval tab *synchronously*, while the click's transient\n // activation is still live. The approval URL isn't known yet (it comes\n // from createRequest below), so open a blank tab now and navigate it\n // once the URL arrives. Opening *after* the await — as this flow used to\n // — runs outside the gesture, so the browser suppresses it as an\n // unsolicited popup and the flow stalls forever (BUI-622).\n // Read the opener option *now* (not at construction) so a custom opener\n // swapped in after the flow was created is still used.\n const openApprovalWindow =\n options.openApprovalWindow ?? defaultOpenApprovalWindow;\n const approvalWindow = openApprovalWindow();\n openedWindow = approvalWindow;\n\n setState({ type: \"creating\" });\n\n let request: AccessRequest;\n try {\n request = await transports.createRequest();\n } catch (err) {\n // If we were superseded (reset, possibly + a newer start()) while this\n // request was in flight, only clean up our own tab — never the shared\n // state or the newer run's window.\n if (runId !== activeRunId) {\n approvalWindow?.close();\n return;\n }\n running = false;\n closeUnnavigatedWindow();\n setState({ type: \"error\", error: toError(err) });\n return;\n }\n if (runId !== activeRunId) {\n approvalWindow?.close();\n return;\n }\n\n if (approvalWindow) {\n approvalWindow.navigate(request.approvalUrl);\n // Hand the tab off to the user; we no longer own/close it.\n openedWindow = null;\n }\n // `approvalWindow === null` means the popup was blocked. Surface it so\n // the UI renders request.approvalUrl as a visible \"Open approval\" link\n // instead of hanging. We poll either way, so a manual open still\n // resolves the flow, and the timeout still bounds the wait.\n setState({\n type: \"awaiting_approval\",\n request,\n popupBlocked: approvalWindow === null,\n });\n\n const deadline = now() + timeoutMs;\n scheduleNextPoll(request, deadline);\n },\n\n reset(): void {\n running = false;\n // Invalidate any in-flight start() so a late createRequest can't clobber\n // a subsequent run.\n activeRunId++;\n clearPoll();\n closeUnnavigatedWindow();\n setState({ type: \"idle\" });\n },\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/direct/connect-flow.ts"],"sourcesContent":["/**\n * Framework-agnostic connect-flow state machine for the browser two-tab helper.\n *\n * @remarks\n * This is the testable core behind {@link useDirectVanaConnect}. It is pure\n * TypeScript (no React, no DOM-only APIs beyond an injectable window opener and\n * timers) so the full flow — create request, open Vana, poll status, read data —\n * can be exercised in a Node test environment.\n *\n * The React hook is a thin `useSyncExternalStore` binding over this store.\n *\n * @category Direct\n * @module direct/connect-flow\n */\n\nimport type {\n AccessRequest,\n AccessRequestStatus,\n AccessRequestStatusValue,\n ApprovedDataResult,\n} from \"./types\";\n\n/**\n * Caller-supplied transports. These typically `fetch` the app's own backend\n * routes, which in turn delegate to a {@link DirectDataController}.\n */\nexport interface DirectConnectTransports<T = unknown> {\n /** Ask the backend to create an access request. */\n createRequest: () => Promise<AccessRequest>;\n /** Ask the backend for the current status of a request. */\n getStatus: (requestId: string) => Promise<AccessRequestStatus>;\n /** Ask the backend to read the approved data. */\n readResult: (requestId: string) => Promise<ApprovedDataResult<T>>;\n}\n\n/**\n * A handle to a tab opened synchronously under the user's click gesture.\n *\n * @remarks\n * The flow opens this tab *before* it knows the approval URL (popup blockers\n * only allow `window.open()` during the click's transient activation), then\n * navigates it once `createRequest` resolves.\n */\nexport interface ConnectWindow {\n /** Point the already-open tab at the approval URL. */\n navigate(url: string): void;\n /** Close the tab (used to clean up an un-navigated tab on failure/reset). */\n close(): void;\n}\n\n/** Tunables for the connect flow. */\nexport interface DirectConnectOptions {\n /** Status poll interval in ms. Defaults to 1500. */\n pollIntervalMs?: number;\n /** Overall timeout in ms before giving up. Defaults to 300000 (5 min). */\n timeoutMs?: number;\n /**\n * Synchronously open a blank tab under the click's transient activation and\n * return a handle to navigate later, or `null` if the browser blocked it.\n * Defaults to `window.open(\"\", \"_blank\")` (with `opener` severed). Injectable\n * for tests.\n *\n * @remarks\n * Renamed from the pre-3.8 `openWindow?: (url) => void`. The old contract was\n * the BUI-622 bug itself (it was called with the URL *after* an `await`, so\n * the popup blocker suppressed it); it cannot be preserved while fixing the\n * bug. Custom openers must now open synchronously and return a navigable\n * handle.\n */\n openApprovalWindow?: () => ConnectWindow | null;\n /** `setTimeout`. Injectable for tests. Defaults to `globalThis.setTimeout`. */\n setTimeoutFn?: (cb: () => void, ms: number) => unknown;\n /** `clearTimeout`. Injectable for tests. Defaults to `globalThis.clearTimeout`. */\n clearTimeoutFn?: (handle: unknown) => void;\n /** Clock source in ms. Injectable for tests. Defaults to `Date.now`. */\n now?: () => number;\n}\n\n/**\n * Discriminated connect-flow state.\n *\n * @remarks\n * `type` matches the builder guide: it starts at `\"idle\"` and is non-idle while\n * connecting. The intermediate phases give richer UIs something to render.\n */\nexport type DirectConnectState<T = unknown> =\n | { type: \"idle\" }\n | { type: \"creating\" }\n | {\n type: \"awaiting_approval\";\n request: AccessRequest;\n /**\n * `true` when the browser blocked the approval popup. The UI should\n * render `request.approvalUrl` as a visible \"Open approval\" link so the\n * user can open it manually instead of the flow silently hanging.\n */\n popupBlocked: boolean;\n }\n | { type: \"reading\"; request: AccessRequest }\n | { type: \"done\"; result: ApprovedDataResult<T> }\n | { type: \"error\"; error: Error };\n\n/** The store returned by {@link createDirectConnectFlow}. */\nexport interface DirectConnectFlow<T = unknown> {\n /** Current state. */\n getState(): DirectConnectState<T>;\n /** Subscribe to state changes; returns an unsubscribe function. */\n subscribe(listener: () => void): () => void;\n /** Begin the flow. No-op if already running. */\n start(): Promise<void>;\n /** Reset to `idle` and stop any in-flight polling. */\n reset(): void;\n}\n\nconst DEFAULT_POLL_INTERVAL_MS = 1500;\nconst DEFAULT_TIMEOUT_MS = 300_000;\n\nfunction toError(value: unknown): Error {\n return value instanceof Error ? value : new Error(String(value));\n}\n\nfunction isReadReadyStatus(status: AccessRequestStatusValue): boolean {\n return status === \"approved\" || status === \"ready_for_read\";\n}\n\n/**\n * Default {@link DirectConnectOptions.openApprovalWindow}: open a blank tab\n * synchronously (inside the click gesture) and return a handle to navigate\n * once the approval URL is known. Returns `null` when blocked or non-DOM.\n */\nfunction defaultOpenApprovalWindow(): ConnectWindow | null {\n if (typeof window === \"undefined\" || !window.open) return null;\n // We can't pass the \"noopener\"/\"noreferrer\" feature string here: it makes\n // window.open() return null, which would throw away the handle we need to\n // navigate later. So we open plain and re-create both protections by hand.\n const opened = window.open(\"\", \"_blank\");\n if (!opened) return null;\n // Sever the opener link while the tab is still about:blank, so the approval\n // page can't reach back into the app (reverse tab-nabbing).\n try {\n opened.opener = null;\n } catch {\n // Some environments make `opener` read-only; best-effort only.\n }\n return {\n navigate(url: string) {\n // Restore the no-referrer protection the old \"noreferrer\" feature gave:\n // tag the blank document so the upcoming navigation sends no Referer to\n // the approval page (best-effort; the blank doc is same-origin here).\n try {\n const meta = opened.document.createElement(\"meta\");\n meta.name = \"referrer\";\n meta.content = \"no-referrer\";\n (opened.document.head ?? opened.document.documentElement)?.appendChild(\n meta,\n );\n } catch {\n // Cross-origin/unavailable document: skip, navigation still proceeds.\n }\n opened.location.href = url;\n },\n close() {\n opened.close();\n },\n };\n}\n\n/**\n * Create a connect-flow store.\n *\n * @param transports - Backend transports (`createRequest`, `getStatus`, `readResult`).\n * @param options - Polling/timeout tunables and injectable side effects.\n * @returns A {@link DirectConnectFlow} store.\n */\nexport function createDirectConnectFlow<T = unknown>(\n transports: DirectConnectTransports<T>,\n options: DirectConnectOptions = {},\n): DirectConnectFlow<T> {\n const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n // Resolved lazily at start() (see below) so a custom opener swapped in after\n // construction is still honoured — matching the latest-callback pattern the\n // React hook uses for its transports.\n const setTimeoutFn =\n options.setTimeoutFn ??\n ((cb: () => void, ms: number) => globalThis.setTimeout(cb, ms));\n const clearTimeoutFn =\n options.clearTimeoutFn ??\n ((handle: unknown) => {\n globalThis.clearTimeout(handle as never);\n });\n const now = options.now ?? (() => Date.now());\n\n let state: DirectConnectState<T> = { type: \"idle\" };\n const listeners = new Set<() => void>();\n let pollHandle: unknown = null;\n let running = false;\n // Monotonic id for the current start() invocation. reset() (and an\n // immediately following start()) bumps it, so a previous run whose async\n // createRequest is still in flight can detect it has been superseded and\n // avoid touching shared state / the newer run's tab.\n let activeRunId = 0;\n // Holds the tab we opened only while it is still blank (un-navigated). Once\n // navigated to the approval URL we drop the reference so reset/cleanup never\n // closes the live approval tab the user is interacting with.\n let openedWindow: ConnectWindow | null = null;\n\n function emit(): void {\n for (const listener of listeners) listener();\n }\n\n function setState(next: DirectConnectState<T>): void {\n state = next;\n emit();\n }\n\n function clearPoll(): void {\n if (pollHandle !== null) {\n clearTimeoutFn(pollHandle);\n pollHandle = null;\n }\n }\n\n /** Close the opened tab if it is still blank (never navigated). */\n function closeUnnavigatedWindow(): void {\n if (openedWindow) {\n openedWindow.close();\n openedWindow = null;\n }\n }\n\n function isRunningPhase(): boolean {\n return (\n state.type === \"creating\" ||\n state.type === \"awaiting_approval\" ||\n state.type === \"reading\"\n );\n }\n\n async function readAndFinish(request: AccessRequest): Promise<void> {\n setState({ type: \"reading\", request });\n try {\n const result = await transports.readResult(request.requestId);\n if (!running) return;\n setState({ type: \"done\", result });\n } catch (err) {\n if (!running) return;\n setState({ type: \"error\", error: toError(err) });\n } finally {\n running = false;\n }\n }\n\n function scheduleNextPoll(request: AccessRequest, deadline: number): void {\n pollHandle = setTimeoutFn(() => {\n void poll(request, deadline);\n }, pollIntervalMs);\n }\n\n async function poll(request: AccessRequest, deadline: number): Promise<void> {\n if (!running) return;\n if (now() >= deadline) {\n running = false;\n setState({\n type: \"error\",\n error: new Error(\"Timed out waiting for approval\"),\n });\n return;\n }\n let status: AccessRequestStatus;\n try {\n status = await transports.getStatus(request.requestId);\n } catch (err) {\n if (!running) return;\n running = false;\n setState({ type: \"error\", error: toError(err) });\n return;\n }\n if (!running) return;\n\n if (isReadReadyStatus(status.status)) {\n clearPoll();\n await readAndFinish(request);\n return;\n }\n if (\n status.status === \"completed\" ||\n status.status === \"denied\" ||\n status.status === \"expired\"\n ) {\n running = false;\n setState({\n type: \"error\",\n error: new Error(`Access request ${status.status}`),\n });\n return;\n }\n scheduleNextPoll(request, deadline);\n }\n\n return {\n getState() {\n return state;\n },\n\n subscribe(listener: () => void) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n\n async start(): Promise<void> {\n if (running || isRunningPhase()) return;\n running = true;\n const runId = ++activeRunId;\n\n // Open the approval tab *synchronously*, while the click's transient\n // activation is still live. The approval URL isn't known yet (it comes\n // from createRequest below), so open a blank tab now and navigate it\n // once the URL arrives. Opening *after* the await — as this flow used to\n // — runs outside the gesture, so the browser suppresses it as an\n // unsolicited popup and the flow stalls forever (BUI-622).\n // Read the opener option *now* (not at construction) so a custom opener\n // swapped in after the flow was created is still used.\n const openApprovalWindow =\n options.openApprovalWindow ?? defaultOpenApprovalWindow;\n const approvalWindow = openApprovalWindow();\n openedWindow = approvalWindow;\n\n setState({ type: \"creating\" });\n\n let request: AccessRequest;\n try {\n request = await transports.createRequest();\n } catch (err) {\n // If we were superseded (reset, possibly + a newer start()) while this\n // request was in flight, only clean up our own tab — never the shared\n // state or the newer run's window.\n if (runId !== activeRunId) {\n approvalWindow?.close();\n return;\n }\n running = false;\n closeUnnavigatedWindow();\n setState({ type: \"error\", error: toError(err) });\n return;\n }\n if (runId !== activeRunId) {\n approvalWindow?.close();\n return;\n }\n\n if (approvalWindow) {\n approvalWindow.navigate(request.approvalUrl);\n // Hand the tab off to the user; we no longer own/close it.\n openedWindow = null;\n }\n // `approvalWindow === null` means the popup was blocked. Surface it so\n // the UI renders request.approvalUrl as a visible \"Open approval\" link\n // instead of hanging. We poll either way, so a manual open still\n // resolves the flow, and the timeout still bounds the wait.\n setState({\n type: \"awaiting_approval\",\n request,\n popupBlocked: approvalWindow === null,\n });\n\n const deadline = now() + timeoutMs;\n scheduleNextPoll(request, deadline);\n },\n\n reset(): void {\n running = false;\n // Invalidate any in-flight start() so a late createRequest can't clobber\n // a subsequent run.\n activeRunId++;\n clearPoll();\n closeUnnavigatedWindow();\n setState({ type: \"idle\" });\n },\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -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 { 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 { AccessNotApprovedError, DirectConfigError } 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 DirectServiceEndpoints,\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 /** Scopes to request (e.g. `[\"icloud_notes.notes\"]`). At least one required. */\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.\n * @returns `{ requestId, approvalUrl, appAddress }`.\n */\n createAccessRequest(input: { returnUrl: string }): 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? }`.\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 a {@link DirectPaymentReceipt} under `payment`\n * when the Personal Server returns one. After a successful read, the\n * controller acknowledges the DCR so Vana Web can close/redirect the approval\n * tab.\n *\n * @param input - The `dcr_*` request id to read.\n * @returns `{ scope, data, payment? }`.\n * @throws {@link AccessNotApprovedError} if the request is not approved.\n * @throws {@link PaymentRequiredError} if payment is required but unsettled.\n */\n readApprovedData<T = unknown>(input: {\n requestId: string;\n }): Promise<ApprovedDataResult<T>>;\n}\n\nfunction isHexPrivateKey(value: string): value is Hex {\n return /^0x[0-9a-fA-F]{64}$/.test(value);\n}\n\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 or scopes are invalid.\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.\n for (const scope of config.scopes) {\n parseScope(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 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 });\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 }): Promise<ApprovedDataResult<T>> {\n const status = await accessRequestClient.getAccessRequestStatus(\n input.requestId,\n );\n if (\n !isReadReadyStatus(status.status) ||\n !status.personalServerUrl ||\n !status.grantId ||\n !status.scope\n ) {\n throw new AccessNotApprovedError(\n \"Request is not approved or is missing grantId/scope/personalServerUrl\",\n {\n requestId: input.requestId,\n status: status.status,\n hasPersonalServerUrl: Boolean(status.personalServerUrl),\n hasGrantId: Boolean(status.grantId),\n hasScope: Boolean(status.scope),\n },\n );\n }\n\n const result = await readPersonalServerData({\n personalServerUrl: status.personalServerUrl,\n scope: status.scope,\n grantId: status.grantId,\n payerAddress: account.address,\n signMessage,\n escrow,\n fetchFn: config.personalServerFetch,\n transportRetry: config.personalServerTransportRetry,\n });\n try {\n await accessRequestClient.acknowledgeRead?.(input.requestId);\n } catch {\n // The read already succeeded; ack only drives Vana Web completion UX.\n }\n\n return {\n scope: status.scope,\n data: result.data as T,\n payment: result.payment,\n };\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 { 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 { AccessNotApprovedError, DirectConfigError } 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 DirectServiceEndpoints,\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 /** Scopes to request (e.g. `[\"icloud_notes.notes\"]`). At least one required. */\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.\n * @returns `{ requestId, approvalUrl, appAddress }`.\n */\n createAccessRequest(input: { returnUrl: string }): 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? }`.\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 a {@link DirectPaymentReceipt} under `payment`\n * when the Personal Server returns one. After a successful read, the\n * controller acknowledges the DCR so Vana Web can close/redirect the approval\n * tab.\n *\n * @param input - The `dcr_*` request id to read.\n * @returns `{ scope, data, payment? }`.\n * @throws {@link AccessNotApprovedError} if the request is not approved.\n * @throws {@link PaymentRequiredError} if payment is required but unsettled.\n */\n readApprovedData<T = unknown>(input: {\n requestId: string;\n }): Promise<ApprovedDataResult<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 or scopes are invalid.\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.\n for (const scope of config.scopes) {\n parseScope(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 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 });\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 }): Promise<ApprovedDataResult<T>> {\n const status = await accessRequestClient.getAccessRequestStatus(\n input.requestId,\n );\n if (\n !isReadReadyStatus(status.status) ||\n !status.personalServerUrl ||\n !status.grantId ||\n !status.scope\n ) {\n throw new AccessNotApprovedError(\n \"Request is not approved or is missing grantId/scope/personalServerUrl\",\n {\n requestId: input.requestId,\n status: status.status,\n hasPersonalServerUrl: Boolean(status.personalServerUrl),\n hasGrantId: Boolean(status.grantId),\n hasScope: Boolean(status.scope),\n },\n );\n }\n\n const result = await readPersonalServerData({\n personalServerUrl: status.personalServerUrl,\n scope: status.scope,\n grantId: status.grantId,\n payerAddress: account.address,\n signMessage,\n escrow,\n fetchFn: config.personalServerFetch,\n transportRetry: config.personalServerTransportRetry,\n });\n try {\n await accessRequestClient.acknowledgeRead?.(input.requestId);\n } catch {\n // The read already succeeded; ack only drives Vana Web completion UX.\n }\n\n return {\n scope: status.scope,\n data: result.data as T,\n payment: result.payment,\n };\n },\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -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 { 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 { AccessNotApprovedError, DirectConfigError } 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 DirectServiceEndpoints,\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 /** Scopes to request (e.g. `[\"icloud_notes.notes\"]`). At least one required. */\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.\n * @returns `{ requestId, approvalUrl, appAddress }`.\n */\n createAccessRequest(input: { returnUrl: string }): 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? }`.\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 a {@link DirectPaymentReceipt} under `payment`\n * when the Personal Server returns one. After a successful read, the\n * controller acknowledges the DCR so Vana Web can close/redirect the approval\n * tab.\n *\n * @param input - The `dcr_*` request id to read.\n * @returns `{ scope, data, payment? }`.\n * @throws {@link AccessNotApprovedError} if the request is not approved.\n * @throws {@link PaymentRequiredError} if payment is required but unsettled.\n */\n readApprovedData<T = unknown>(input: {\n requestId: string;\n }): Promise<ApprovedDataResult<T>>;\n}\n\nfunction isHexPrivateKey(value: string): value is Hex {\n return /^0x[0-9a-fA-F]{64}$/.test(value);\n}\n\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 or scopes are invalid.\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.\n for (const scope of config.scopes) {\n parseScope(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 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 });\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 }): Promise<ApprovedDataResult<T>> {\n const status = await accessRequestClient.getAccessRequestStatus(\n input.requestId,\n );\n if (\n !isReadReadyStatus(status.status) ||\n !status.personalServerUrl ||\n !status.grantId ||\n !status.scope\n ) {\n throw new AccessNotApprovedError(\n \"Request is not approved or is missing grantId/scope/personalServerUrl\",\n {\n requestId: input.requestId,\n status: status.status,\n hasPersonalServerUrl: Boolean(status.personalServerUrl),\n hasGrantId: Boolean(status.grantId),\n hasScope: Boolean(status.scope),\n },\n );\n }\n\n const result = await readPersonalServerData({\n personalServerUrl: status.personalServerUrl,\n scope: status.scope,\n grantId: status.grantId,\n payerAddress: account.address,\n signMessage,\n escrow,\n fetchFn: config.personalServerFetch,\n transportRetry: config.personalServerTransportRetry,\n });\n try {\n await accessRequestClient.acknowledgeRead?.(input.requestId);\n } catch {\n // The read already succeeded; ack only drives Vana Web completion UX.\n }\n\n return {\n scope: status.scope,\n data: result.data as T,\n payment: result.payment,\n };\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 { 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 { AccessNotApprovedError, DirectConfigError } 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 DirectServiceEndpoints,\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 /** Scopes to request (e.g. `[\"icloud_notes.notes\"]`). At least one required. */\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.\n * @returns `{ requestId, approvalUrl, appAddress }`.\n */\n createAccessRequest(input: { returnUrl: string }): 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? }`.\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 a {@link DirectPaymentReceipt} under `payment`\n * when the Personal Server returns one. After a successful read, the\n * controller acknowledges the DCR so Vana Web can close/redirect the approval\n * tab.\n *\n * @param input - The `dcr_*` request id to read.\n * @returns `{ scope, data, payment? }`.\n * @throws {@link AccessNotApprovedError} if the request is not approved.\n * @throws {@link PaymentRequiredError} if payment is required but unsettled.\n */\n readApprovedData<T = unknown>(input: {\n requestId: string;\n }): Promise<ApprovedDataResult<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 or scopes are invalid.\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.\n for (const scope of config.scopes) {\n parseScope(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 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 });\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 }): Promise<ApprovedDataResult<T>> {\n const status = await accessRequestClient.getAccessRequestStatus(\n input.requestId,\n );\n if (\n !isReadReadyStatus(status.status) ||\n !status.personalServerUrl ||\n !status.grantId ||\n !status.scope\n ) {\n throw new AccessNotApprovedError(\n \"Request is not approved or is missing grantId/scope/personalServerUrl\",\n {\n requestId: input.requestId,\n status: status.status,\n hasPersonalServerUrl: Boolean(status.personalServerUrl),\n hasGrantId: Boolean(status.grantId),\n hasScope: Boolean(status.scope),\n },\n );\n }\n\n const result = await readPersonalServerData({\n personalServerUrl: status.personalServerUrl,\n scope: status.scope,\n grantId: status.grantId,\n payerAddress: account.address,\n signMessage,\n escrow,\n fetchFn: config.personalServerFetch,\n transportRetry: config.personalServerTransportRetry,\n });\n try {\n await accessRequestClient.acknowledgeRead?.(input.requestId);\n } catch {\n // The read already succeeded; ack only drives Vana Web completion UX.\n }\n\n return {\n scope: status.scope,\n data: result.data as T,\n payment: result.payment,\n };\n },\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -36,7 +36,7 @@ const DEV_ENDPOINTS = {
|
|
|
36
36
|
chainId: 14800,
|
|
37
37
|
accessRequestBaseUrl: "https://app-dev.vana.org",
|
|
38
38
|
approvalAppBaseUrl: "https://app-dev.vana.org",
|
|
39
|
-
escrowGatewayUrl: "https://dp-rpc
|
|
39
|
+
escrowGatewayUrl: "https://dp-rpc.moksha.vana.org"
|
|
40
40
|
};
|
|
41
41
|
function getDirectEndpoints(env) {
|
|
42
42
|
if (env === "dev") {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/direct/endpoints.ts"],"sourcesContent":["/**\n * Per-environment service URLs for the Direct Data Controller.\n *\n * @remarks\n * The SDK ships production defaults; pass `env: \"dev\"` only when testing\n * against Vana's internal dev stack. Use the controller `network` option for\n * chain selection without changing deployment URLs.\n *\n * @category Direct\n * @module direct/endpoints\n */\n\nimport type { DirectEnv, DirectNetwork, DirectServiceEndpoints } from \"./types\";\nimport { getProtocolNetworkChainId } from \"../protocol/networks\";\n\n/** Production (mainnet) service URLs. */\nexport const PRODUCTION_ENDPOINTS: DirectServiceEndpoints = {\n chainId: 1480,\n accessRequestBaseUrl: \"https://app.vana.org\",\n approvalAppBaseUrl: \"https://app.vana.org\",\n escrowGatewayUrl: \"https://dp-rpc.vana.org\",\n} as const;\n\n/** Internal dev stack service URLs. */\nexport const DEV_ENDPOINTS: DirectServiceEndpoints = {\n chainId: 14800,\n accessRequestBaseUrl: \"https://app-dev.vana.org\",\n approvalAppBaseUrl: \"https://app-dev.vana.org\",\n escrowGatewayUrl: \"https://dp-rpc
|
|
1
|
+
{"version":3,"sources":["../../src/direct/endpoints.ts"],"sourcesContent":["/**\n * Per-environment service URLs for the Direct Data Controller.\n *\n * @remarks\n * The SDK ships production defaults; pass `env: \"dev\"` only when testing\n * against Vana's internal dev stack. Use the controller `network` option for\n * chain selection without changing deployment URLs.\n *\n * @category Direct\n * @module direct/endpoints\n */\n\nimport type { DirectEnv, DirectNetwork, DirectServiceEndpoints } from \"./types\";\nimport { getProtocolNetworkChainId } from \"../protocol/networks\";\n\n/** Production (mainnet) service URLs. */\nexport const PRODUCTION_ENDPOINTS: DirectServiceEndpoints = {\n chainId: 1480,\n accessRequestBaseUrl: \"https://app.vana.org\",\n approvalAppBaseUrl: \"https://app.vana.org\",\n escrowGatewayUrl: \"https://dp-rpc.vana.org\",\n} as const;\n\n/** Internal dev stack service URLs. */\nexport const DEV_ENDPOINTS: DirectServiceEndpoints = {\n chainId: 14800,\n accessRequestBaseUrl: \"https://app-dev.vana.org\",\n approvalAppBaseUrl: \"https://app-dev.vana.org\",\n escrowGatewayUrl: \"https://dp-rpc.moksha.vana.org\",\n} as const;\n\n/**\n * Resolve the default {@link DirectServiceEndpoints} for an environment.\n *\n * @param env - Target environment.\n * @returns The default endpoints for that environment.\n */\nexport function getDirectEndpoints(env: DirectEnv): DirectServiceEndpoints {\n if (env === \"dev\") {\n return DEV_ENDPOINTS;\n }\n\n return PRODUCTION_ENDPOINTS;\n}\n\n/**\n * Resolve the default network for a deployment environment.\n *\n * @param env - Target deployment environment.\n * @returns The network historically paired with that deployment.\n */\nexport function getDirectDefaultNetwork(env: DirectEnv): DirectNetwork {\n return env === \"dev\" ? \"moksha\" : \"mainnet\";\n}\n\n/**\n * Resolve the Vana chain id for a network.\n *\n * @param network - Target Vana network.\n * @returns The network chain id.\n */\nexport function getDirectNetworkChainId(network: DirectNetwork): number {\n return getProtocolNetworkChainId(network);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAaA,sBAA0C;AAGnC,MAAM,uBAA+C;AAAA,EAC1D,SAAS;AAAA,EACT,sBAAsB;AAAA,EACtB,oBAAoB;AAAA,EACpB,kBAAkB;AACpB;AAGO,MAAM,gBAAwC;AAAA,EACnD,SAAS;AAAA,EACT,sBAAsB;AAAA,EACtB,oBAAoB;AAAA,EACpB,kBAAkB;AACpB;AAQO,SAAS,mBAAmB,KAAwC;AACzE,MAAI,QAAQ,OAAO;AACjB,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAQO,SAAS,wBAAwB,KAA+B;AACrE,SAAO,QAAQ,QAAQ,WAAW;AACpC;AAQO,SAAS,wBAAwB,SAAgC;AACtE,aAAO,2CAA0B,OAAO;AAC1C;","names":[]}
|
package/dist/direct/endpoints.js
CHANGED
|
@@ -9,7 +9,7 @@ const DEV_ENDPOINTS = {
|
|
|
9
9
|
chainId: 14800,
|
|
10
10
|
accessRequestBaseUrl: "https://app-dev.vana.org",
|
|
11
11
|
approvalAppBaseUrl: "https://app-dev.vana.org",
|
|
12
|
-
escrowGatewayUrl: "https://dp-rpc
|
|
12
|
+
escrowGatewayUrl: "https://dp-rpc.moksha.vana.org"
|
|
13
13
|
};
|
|
14
14
|
function getDirectEndpoints(env) {
|
|
15
15
|
if (env === "dev") {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/direct/endpoints.ts"],"sourcesContent":["/**\n * Per-environment service URLs for the Direct Data Controller.\n *\n * @remarks\n * The SDK ships production defaults; pass `env: \"dev\"` only when testing\n * against Vana's internal dev stack. Use the controller `network` option for\n * chain selection without changing deployment URLs.\n *\n * @category Direct\n * @module direct/endpoints\n */\n\nimport type { DirectEnv, DirectNetwork, DirectServiceEndpoints } from \"./types\";\nimport { getProtocolNetworkChainId } from \"../protocol/networks\";\n\n/** Production (mainnet) service URLs. */\nexport const PRODUCTION_ENDPOINTS: DirectServiceEndpoints = {\n chainId: 1480,\n accessRequestBaseUrl: \"https://app.vana.org\",\n approvalAppBaseUrl: \"https://app.vana.org\",\n escrowGatewayUrl: \"https://dp-rpc.vana.org\",\n} as const;\n\n/** Internal dev stack service URLs. */\nexport const DEV_ENDPOINTS: DirectServiceEndpoints = {\n chainId: 14800,\n accessRequestBaseUrl: \"https://app-dev.vana.org\",\n approvalAppBaseUrl: \"https://app-dev.vana.org\",\n escrowGatewayUrl: \"https://dp-rpc
|
|
1
|
+
{"version":3,"sources":["../../src/direct/endpoints.ts"],"sourcesContent":["/**\n * Per-environment service URLs for the Direct Data Controller.\n *\n * @remarks\n * The SDK ships production defaults; pass `env: \"dev\"` only when testing\n * against Vana's internal dev stack. Use the controller `network` option for\n * chain selection without changing deployment URLs.\n *\n * @category Direct\n * @module direct/endpoints\n */\n\nimport type { DirectEnv, DirectNetwork, DirectServiceEndpoints } from \"./types\";\nimport { getProtocolNetworkChainId } from \"../protocol/networks\";\n\n/** Production (mainnet) service URLs. */\nexport const PRODUCTION_ENDPOINTS: DirectServiceEndpoints = {\n chainId: 1480,\n accessRequestBaseUrl: \"https://app.vana.org\",\n approvalAppBaseUrl: \"https://app.vana.org\",\n escrowGatewayUrl: \"https://dp-rpc.vana.org\",\n} as const;\n\n/** Internal dev stack service URLs. */\nexport const DEV_ENDPOINTS: DirectServiceEndpoints = {\n chainId: 14800,\n accessRequestBaseUrl: \"https://app-dev.vana.org\",\n approvalAppBaseUrl: \"https://app-dev.vana.org\",\n escrowGatewayUrl: \"https://dp-rpc.moksha.vana.org\",\n} as const;\n\n/**\n * Resolve the default {@link DirectServiceEndpoints} for an environment.\n *\n * @param env - Target environment.\n * @returns The default endpoints for that environment.\n */\nexport function getDirectEndpoints(env: DirectEnv): DirectServiceEndpoints {\n if (env === \"dev\") {\n return DEV_ENDPOINTS;\n }\n\n return PRODUCTION_ENDPOINTS;\n}\n\n/**\n * Resolve the default network for a deployment environment.\n *\n * @param env - Target deployment environment.\n * @returns The network historically paired with that deployment.\n */\nexport function getDirectDefaultNetwork(env: DirectEnv): DirectNetwork {\n return env === \"dev\" ? \"moksha\" : \"mainnet\";\n}\n\n/**\n * Resolve the Vana chain id for a network.\n *\n * @param network - Target Vana network.\n * @returns The network chain id.\n */\nexport function getDirectNetworkChainId(network: DirectNetwork): number {\n return getProtocolNetworkChainId(network);\n}\n"],"mappings":"AAaA,SAAS,iCAAiC;AAGnC,MAAM,uBAA+C;AAAA,EAC1D,SAAS;AAAA,EACT,sBAAsB;AAAA,EACtB,oBAAoB;AAAA,EACpB,kBAAkB;AACpB;AAGO,MAAM,gBAAwC;AAAA,EACnD,SAAS;AAAA,EACT,sBAAsB;AAAA,EACtB,oBAAoB;AAAA,EACpB,kBAAkB;AACpB;AAQO,SAAS,mBAAmB,KAAwC;AACzE,MAAI,QAAQ,OAAO;AACjB,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAQO,SAAS,wBAAwB,KAA+B;AACrE,SAAO,QAAQ,QAAQ,WAAW;AACpC;AAQO,SAAS,wBAAwB,SAAgC;AACtE,SAAO,0BAA0B,OAAO;AAC1C;","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/direct/types.ts"],"sourcesContent":["import type { EscrowAccessRecord } from \"../protocol/escrow\";\nimport type { ProtocolNetwork } from \"../protocol/networks\";\n\n/**\n * Shared types for the Direct Data Controller and the browser connect helper.\n *\n * @remarks\n * These types describe the \"two-tab\" Data Portability flow documented in the\n * builder guide: a backend controller creates an access request, the browser\n * opens Vana for user approval, and the backend reads the approved data from\n * the user's Personal Server (handling 402 Payment Required).\n *\n * @category Direct\n * @module direct/types\n */\n\n/**\n * Target environment for a {@link DirectDataController}.\n *\n * - `\"production\"` — Vana mainnet stack (default service URLs).\n * - `\"dev\"` — Vana internal dev stack. Use only when testing against\n * Vana's dev infrastructure.\n */\nexport type DirectEnv = \"dev\" | \"production\";\n\n/**\n * Vana network used for chain-aware Direct defaults.\n *\n * - `\"mainnet\"` — Vana mainnet (`chainId` 1480).\n * - `\"moksha\"` — Moksha testnet (`chainId` 14800).\n */\nexport type DirectNetwork = ProtocolNetwork;\n\n/**\n * App identity advertised to users during approval and attributed in Builder\n * League activity reports.\n */\nexport interface DirectAppConfig {\n /** Stable, human-readable app id (e.g. `\"notes-lens\"`). */\n id: string;\n /** Display name shown to the user in the Vana approval UI. */\n name: string;\n /** Public homepage URL for the app. */\n homepageUrl: string;\n}\n\n/**\n * Resolved app identity: the configured {@link DirectAppConfig} plus the app's\n * derived on-chain address (the address to fund and inspect).\n */\nexport interface AppIdentity extends DirectAppConfig {\n /** The app's `0x`-prefixed on-chain address (derived from `appPrivateKey`). */\n address: string;\n}\n\n/**\n * Resolved service URLs and chain id for a given {@link DirectEnv}.\n *\n * @remarks\n * Centralizes the per-environment base URLs the controller talks to. Each can\n * be overridden via {@link DirectDataControllerConfig.endpoints} when pointing\n * at a non-standard deployment.\n */\nexport interface DirectServiceEndpoints {\n /** Vana chain id for this environment (1480 mainnet, 14800 moksha). */\n chainId: number;\n /** Base URL of the Vana Account access-request API that issues `dcr_*` ids. */\n accessRequestBaseUrl: string;\n /** Base URL users are sent to for approval (the Vana app). */\n approvalAppBaseUrl: string;\n /** Base URL of the DP RPC escrow gateway used to settle `402 Payment Required`. */\n escrowGatewayUrl: string;\n}\n\n/** Result of {@link DirectDataController.createAccessRequest}. */\nexport interface AccessRequest {\n /** Opaque request id (e.g. `\"dcr_123\"`). */\n requestId: string;\n /** URL the browser opens so the user can approve the requested scopes. */\n approvalUrl: string;\n /** On-chain address of the (registered or reused) app. */\n appAddress: string;\n}\n\n/** Lifecycle status of an access request. */\nexport type AccessRequestStatusValue =\n | \"pending\"\n | \"approved\"\n | \"ready_for_read\"\n | \"denied\"\n | \"expired\";\n\n/** Result of {@link DirectDataController.getAccessRequestStatus}. */\nexport interface AccessRequestStatus {\n /** Current lifecycle status of the request. */\n status: AccessRequestStatusValue;\n /** Personal Server base URL — present once data is ready to read. */\n personalServerUrl?: string;\n /** Grant id covering the approved scope — present once data is ready to read. */\n grantId?: string;\n /** The approved scope — present once data is ready to read. */\n scope?: string;\n}\n\n/** Result of {@link DirectDataController.readApprovedData}. */\nexport interface ApprovedDataResult<T = unknown> {\n /** The scope the data was read for. */\n scope: string;\n /** The decoded payload returned by the Personal Server. */\n data: T;\n /**\n * Payment receipt — present only when this read required (and settled) a\n * payment. Lets builders inspect the amount, asset, and fee breakdown without\n * digging into the underlying 402/escrow exchange. Reads served from a paid-up\n * grant omit this field.\n */\n payment?: DirectPaymentReceipt;\n}\n\n/**\n * Client for the Vana Account access-request API — the service that turns a\n * registered app + scopes into a `dcr_*` id and approval URL.\n *\n * @remarks\n * The controller uses a default client against the Vana Account endpoints. You\n * can inject your own implementation to point at a custom deployment or to\n * supply a test double.\n */\nexport interface AccessRequestClient {\n /**\n * Create an access request for the given app + scopes.\n *\n * @param input - App identity, source, scopes, network, and the post-approval return URL.\n * @returns The created {@link AccessRequest}.\n */\n createAccessRequest(input: {\n appAddress: string;\n app: DirectAppConfig;\n source: string;\n scopes: string[];\n returnUrl: string;\n /** Vana protocol network for this request (`\"mainnet\"` or `\"moksha\"`). */\n network: DirectNetwork;\n }): Promise<AccessRequest>;\n\n /**\n * Fetch the current status of a previously created access request.\n *\n * @param requestId - The `dcr_*` id returned by {@link AccessRequestClient.createAccessRequest}.\n * @returns The current {@link AccessRequestStatus}.\n */\n getAccessRequestStatus(requestId: string): Promise<AccessRequestStatus>;\n\n /**\n * Acknowledge that the app successfully read the approved data.\n *\n * @remarks\n * Direct Vana Web DCRs remain in `ready_for_read` while the browser Personal\n * Server is serving the app. After a successful Personal Server read, the\n * controller calls this hook so Vana Web can mark the request completed and\n * close/redirect the approval tab.\n *\n * Optional so injected clients from older SDK integrations keep compiling;\n * the default HTTP client implements it.\n */\n acknowledgeRead?(requestId: string): Promise<void>;\n}\n\n/**\n * Op-type vocabulary used by the DPv2 escrow payment surface.\n *\n * @remarks\n * These are the operations the gateway prices and settles via\n * `POST /v1/escrow/pay` (`opType` field of the `GenericPayment` message). A\n * direct data read settles the {@link DirectOpType.DataAccess} op for the\n * approved grant; the other op types are listed here for completeness and to\n * give builders a typed vocabulary when inspecting fee breakdowns.\n *\n * Note: the escrow `GenericPayment` `opType` is currently `\"grant\"` on the wire\n * for grant lifecycle payments; this enum names the higher-level fee categories\n * the gateway reports in a {@link PaymentBreakdown}.\n */\nexport const DirectOpType = {\n GrantRegistration: \"grant_registration\",\n DataAccess: \"data_access\",\n DataRegistration: \"data_registration\",\n ServerRegistration: \"server_registration\",\n BuilderRegistration: \"builder_registration\",\n} as const;\n\n/** A direct-flow op type (see {@link DirectOpType}). */\nexport type DirectOpTypeValue =\n (typeof DirectOpType)[keyof typeof DirectOpType];\n\n/**\n * What a Personal Server `402 Payment Required` tells the controller is owed for\n * a data read.\n *\n * @remarks\n * The PS read 402 body identifies the grant to settle and the amount/asset. The\n * controller settles it via the DPv2 escrow gateway (`/v1/escrow/pay`). The full\n * unmodified body is preserved under {@link PersonalServerPaymentRequired.raw}.\n */\nexport interface PersonalServerPaymentRequired {\n /** Grant id to settle (the escrow `opId`). Defaults to the read's grantId. */\n grantId: string;\n /** X402 network advertised by the Personal Server challenge. */\n network?: string;\n /** Payment nonce requested by the 402 challenge. */\n paymentNonce?: string;\n /** Server-signed data access receipt requested by the 402 challenge. */\n accessRecord?: EscrowAccessRecord;\n /** Asset address owed (zero address = native VANA). */\n asset: string;\n /** Amount owed, as a decimal base-unit string (preserves uint256 precision). */\n amount: string;\n /** The full, unmodified 402 response body. */\n raw: unknown;\n}\n\n/**\n * Structured payment metadata attached to a successful paid read.\n *\n * @remarks\n * Derived from the gateway's {@link EscrowPayResult}. Lets builders debug the\n * amount, asset, and per-op fee breakdown without re-deriving anything from the\n * raw 402/payment exchange.\n */\nexport interface DirectPaymentReceipt {\n /** Op type settled (the gateway `opType`, e.g. `\"grant\"`). */\n opType: string;\n /** Op id settled (the grant id). */\n opId: string;\n /** Asset paid in (zero address = native VANA). */\n asset: string;\n /** Total amount paid, as a decimal base-unit string. */\n amount: string;\n /** Payment nonce used for this settlement. */\n paymentNonce: string;\n /** Fee breakdown reported by the gateway (registration vs data-access fee). */\n breakdown: DirectFeeBreakdown;\n /** ISO timestamp the gateway recorded the payment. */\n paidAt: string;\n}\n\n/**\n * Per-op fee breakdown reported by the gateway.\n *\n * @remarks\n * Mirrors the escrow {@link PaymentBreakdown}: a one-time registration fee plus\n * the per-read data-access fee, and whether this settlement covered the\n * registration fee.\n */\nexport interface DirectFeeBreakdown {\n /** One-time registration fee for the op, as a decimal base-unit string. */\n registrationFee: string;\n /** Per-read data-access fee, as a decimal base-unit string. */\n dataAccessFee: string;\n /** True when this settlement paid the registration fee. */\n registrationPaid: boolean;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAsLO,MAAM,eAAe;AAAA,EAC1B,mBAAmB;AAAA,EACnB,YAAY;AAAA,EACZ,kBAAkB;AAAA,EAClB,oBAAoB;AAAA,EACpB,qBAAqB;AACvB;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/direct/types.ts"],"sourcesContent":["import type { EscrowAccessRecord } from \"../protocol/escrow\";\nimport type { ProtocolNetwork } from \"../protocol/networks\";\n\n/**\n * Shared types for the Direct Data Controller and the browser connect helper.\n *\n * @remarks\n * These types describe the \"two-tab\" Data Portability flow documented in the\n * builder guide: a backend controller creates an access request, the browser\n * opens Vana for user approval, and the backend reads the approved data from\n * the user's Personal Server (handling 402 Payment Required).\n *\n * @category Direct\n * @module direct/types\n */\n\n/**\n * Target environment for a {@link DirectDataController}.\n *\n * - `\"production\"` — Vana mainnet stack (default service URLs).\n * - `\"dev\"` — Vana internal dev stack. Use only when testing against\n * Vana's dev infrastructure.\n */\nexport type DirectEnv = \"dev\" | \"production\";\n\n/**\n * Vana network used for chain-aware Direct defaults.\n *\n * - `\"mainnet\"` — Vana mainnet (`chainId` 1480).\n * - `\"moksha\"` — Moksha testnet (`chainId` 14800).\n */\nexport type DirectNetwork = ProtocolNetwork;\n\n/**\n * App identity advertised to users during approval and attributed in Builder\n * League activity reports.\n */\nexport interface DirectAppConfig {\n /** Stable, human-readable app id (e.g. `\"notes-lens\"`). */\n id: string;\n /** Display name shown to the user in the Vana approval UI. */\n name: string;\n /** Public homepage URL for the app. */\n homepageUrl: string;\n}\n\n/**\n * Resolved app identity: the configured {@link DirectAppConfig} plus the app's\n * derived on-chain address (the address to fund and inspect).\n */\nexport interface AppIdentity extends DirectAppConfig {\n /** The app's `0x`-prefixed on-chain address (derived from `appPrivateKey`). */\n address: string;\n}\n\n/**\n * Resolved service URLs and chain id for a given {@link DirectEnv}.\n *\n * @remarks\n * Centralizes the per-environment base URLs the controller talks to. Each can\n * be overridden via {@link DirectDataControllerConfig.endpoints} when pointing\n * at a non-standard deployment.\n */\nexport interface DirectServiceEndpoints {\n /** Vana chain id for this environment (1480 mainnet, 14800 moksha). */\n chainId: number;\n /** Base URL of the Vana Account access-request API that issues `dcr_*` ids. */\n accessRequestBaseUrl: string;\n /** Base URL users are sent to for approval (the Vana app). */\n approvalAppBaseUrl: string;\n /** Base URL of the DP RPC escrow gateway used to settle `402 Payment Required`. */\n escrowGatewayUrl: string;\n}\n\n/** Result of {@link DirectDataController.createAccessRequest}. */\nexport interface AccessRequest {\n /** Opaque request id (e.g. `\"dcr_123\"`). */\n requestId: string;\n /** URL the browser opens so the user can approve the requested scopes. */\n approvalUrl: string;\n /** On-chain address of the (registered or reused) app. */\n appAddress: string;\n}\n\n/**\n * Lifecycle status of an access request.\n *\n * @remarks\n * - `\"pending\"` — created, awaiting user approval.\n * - `\"approved\"` / `\"ready_for_read\"` — the grant exists and the Personal\n * Server is reachable; the data is read-ready (see {@link DirectDataController.readApprovedData}).\n * - `\"completed\"` — the app has already read the data and acknowledged it, so\n * the DCR is terminal. A `\"completed\"` request is **not** read-ready — the\n * browser Personal Server may no longer be serving it.\n * - `\"denied\"` / `\"expired\"` — terminal, no data was delivered.\n */\nexport type AccessRequestStatusValue =\n | \"pending\"\n | \"approved\"\n | \"ready_for_read\"\n | \"completed\"\n | \"denied\"\n | \"expired\";\n\n/** Result of {@link DirectDataController.getAccessRequestStatus}. */\nexport interface AccessRequestStatus {\n /** Current lifecycle status of the request. */\n status: AccessRequestStatusValue;\n /** Personal Server base URL — present once data is ready to read. */\n personalServerUrl?: string;\n /** Grant id covering the approved scope — present once data is ready to read. */\n grantId?: string;\n /** The approved scope — present once data is ready to read. */\n scope?: string;\n}\n\n/** Result of {@link DirectDataController.readApprovedData}. */\nexport interface ApprovedDataResult<T = unknown> {\n /** The scope the data was read for. */\n scope: string;\n /** The decoded payload returned by the Personal Server. */\n data: T;\n /**\n * Payment receipt — present only when this read required (and settled) a\n * payment. Lets builders inspect the amount, asset, and fee breakdown without\n * digging into the underlying 402/escrow exchange. Reads served from a paid-up\n * grant omit this field.\n */\n payment?: DirectPaymentReceipt;\n}\n\n/**\n * Client for the Vana Account access-request API — the service that turns a\n * registered app + scopes into a `dcr_*` id and approval URL.\n *\n * @remarks\n * The controller uses a default client against the Vana Account endpoints. You\n * can inject your own implementation to point at a custom deployment or to\n * supply a test double.\n */\nexport interface AccessRequestClient {\n /**\n * Create an access request for the given app + scopes.\n *\n * @param input - App identity, source, scopes, network, and the post-approval return URL.\n * @returns The created {@link AccessRequest}.\n */\n createAccessRequest(input: {\n appAddress: string;\n app: DirectAppConfig;\n source: string;\n scopes: string[];\n returnUrl: string;\n /** Vana protocol network for this request (`\"mainnet\"` or `\"moksha\"`). */\n network: DirectNetwork;\n }): Promise<AccessRequest>;\n\n /**\n * Fetch the current status of a previously created access request.\n *\n * @param requestId - The `dcr_*` id returned by {@link AccessRequestClient.createAccessRequest}.\n * @returns The current {@link AccessRequestStatus}.\n */\n getAccessRequestStatus(requestId: string): Promise<AccessRequestStatus>;\n\n /**\n * Acknowledge that the app successfully read the approved data.\n *\n * @remarks\n * Direct Vana Web DCRs remain in `ready_for_read` while the browser Personal\n * Server is serving the app. After a successful Personal Server read, the\n * controller calls this hook so Vana Web can mark the request completed and\n * close/redirect the approval tab.\n *\n * Optional so injected clients from older SDK integrations keep compiling;\n * the default HTTP client implements it.\n */\n acknowledgeRead?(requestId: string): Promise<void>;\n}\n\n/**\n * Op-type vocabulary used by the DPv2 escrow payment surface.\n *\n * @remarks\n * These are the operations the gateway prices and settles via\n * `POST /v1/escrow/pay` (`opType` field of the `GenericPayment` message). A\n * direct data read settles the {@link DirectOpType.DataAccess} op for the\n * approved grant; the other op types are listed here for completeness and to\n * give builders a typed vocabulary when inspecting fee breakdowns.\n *\n * Note: the escrow `GenericPayment` `opType` is currently `\"grant\"` on the wire\n * for grant lifecycle payments; this enum names the higher-level fee categories\n * the gateway reports in a {@link PaymentBreakdown}.\n */\nexport const DirectOpType = {\n GrantRegistration: \"grant_registration\",\n DataAccess: \"data_access\",\n DataRegistration: \"data_registration\",\n ServerRegistration: \"server_registration\",\n BuilderRegistration: \"builder_registration\",\n} as const;\n\n/** A direct-flow op type (see {@link DirectOpType}). */\nexport type DirectOpTypeValue =\n (typeof DirectOpType)[keyof typeof DirectOpType];\n\n/**\n * What a Personal Server `402 Payment Required` tells the controller is owed for\n * a data read.\n *\n * @remarks\n * The PS read 402 body identifies the grant to settle and the amount/asset. The\n * controller settles it via the DPv2 escrow gateway (`/v1/escrow/pay`). The full\n * unmodified body is preserved under {@link PersonalServerPaymentRequired.raw}.\n */\nexport interface PersonalServerPaymentRequired {\n /** Grant id to settle (the escrow `opId`). Defaults to the read's grantId. */\n grantId: string;\n /** X402 network advertised by the Personal Server challenge. */\n network?: string;\n /** Payment nonce requested by the 402 challenge. */\n paymentNonce?: string;\n /** Server-signed data access receipt requested by the 402 challenge. */\n accessRecord?: EscrowAccessRecord;\n /** Asset address owed (zero address = native VANA). */\n asset: string;\n /** Amount owed, as a decimal base-unit string (preserves uint256 precision). */\n amount: string;\n /** The full, unmodified 402 response body. */\n raw: unknown;\n}\n\n/**\n * Structured payment metadata attached to a successful paid read.\n *\n * @remarks\n * Derived from the gateway's {@link EscrowPayResult}. Lets builders debug the\n * amount, asset, and per-op fee breakdown without re-deriving anything from the\n * raw 402/payment exchange.\n */\nexport interface DirectPaymentReceipt {\n /** Op type settled (the gateway `opType`, e.g. `\"grant\"`). */\n opType: string;\n /** Op id settled (the grant id). */\n opId: string;\n /** Asset paid in (zero address = native VANA). */\n asset: string;\n /** Total amount paid, as a decimal base-unit string. */\n amount: string;\n /** Payment nonce used for this settlement. */\n paymentNonce: string;\n /** Fee breakdown reported by the gateway (registration vs data-access fee). */\n breakdown: DirectFeeBreakdown;\n /** ISO timestamp the gateway recorded the payment. */\n paidAt: string;\n}\n\n/**\n * Per-op fee breakdown reported by the gateway.\n *\n * @remarks\n * Mirrors the escrow {@link PaymentBreakdown}: a one-time registration fee plus\n * the per-read data-access fee, and whether this settlement covered the\n * registration fee.\n */\nexport interface DirectFeeBreakdown {\n /** One-time registration fee for the op, as a decimal base-unit string. */\n registrationFee: string;\n /** Per-read data-access fee, as a decimal base-unit string. */\n dataAccessFee: string;\n /** True when this settlement paid the registration fee. */\n registrationPaid: boolean;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAkMO,MAAM,eAAe;AAAA,EAC1B,mBAAmB;AAAA,EACnB,YAAY;AAAA,EACZ,kBAAkB;AAAA,EAClB,oBAAoB;AAAA,EACpB,qBAAqB;AACvB;","names":[]}
|
package/dist/direct/types.d.ts
CHANGED
|
@@ -74,8 +74,19 @@ export interface AccessRequest {
|
|
|
74
74
|
/** On-chain address of the (registered or reused) app. */
|
|
75
75
|
appAddress: string;
|
|
76
76
|
}
|
|
77
|
-
/**
|
|
78
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Lifecycle status of an access request.
|
|
79
|
+
*
|
|
80
|
+
* @remarks
|
|
81
|
+
* - `"pending"` — created, awaiting user approval.
|
|
82
|
+
* - `"approved"` / `"ready_for_read"` — the grant exists and the Personal
|
|
83
|
+
* Server is reachable; the data is read-ready (see {@link DirectDataController.readApprovedData}).
|
|
84
|
+
* - `"completed"` — the app has already read the data and acknowledged it, so
|
|
85
|
+
* the DCR is terminal. A `"completed"` request is **not** read-ready — the
|
|
86
|
+
* browser Personal Server may no longer be serving it.
|
|
87
|
+
* - `"denied"` / `"expired"` — terminal, no data was delivered.
|
|
88
|
+
*/
|
|
89
|
+
export type AccessRequestStatusValue = "pending" | "approved" | "ready_for_read" | "completed" | "denied" | "expired";
|
|
79
90
|
/** Result of {@link DirectDataController.getAccessRequestStatus}. */
|
|
80
91
|
export interface AccessRequestStatus {
|
|
81
92
|
/** Current lifecycle status of the request. */
|
package/dist/direct/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/direct/types.ts"],"sourcesContent":["import type { EscrowAccessRecord } from \"../protocol/escrow\";\nimport type { ProtocolNetwork } from \"../protocol/networks\";\n\n/**\n * Shared types for the Direct Data Controller and the browser connect helper.\n *\n * @remarks\n * These types describe the \"two-tab\" Data Portability flow documented in the\n * builder guide: a backend controller creates an access request, the browser\n * opens Vana for user approval, and the backend reads the approved data from\n * the user's Personal Server (handling 402 Payment Required).\n *\n * @category Direct\n * @module direct/types\n */\n\n/**\n * Target environment for a {@link DirectDataController}.\n *\n * - `\"production\"` — Vana mainnet stack (default service URLs).\n * - `\"dev\"` — Vana internal dev stack. Use only when testing against\n * Vana's dev infrastructure.\n */\nexport type DirectEnv = \"dev\" | \"production\";\n\n/**\n * Vana network used for chain-aware Direct defaults.\n *\n * - `\"mainnet\"` — Vana mainnet (`chainId` 1480).\n * - `\"moksha\"` — Moksha testnet (`chainId` 14800).\n */\nexport type DirectNetwork = ProtocolNetwork;\n\n/**\n * App identity advertised to users during approval and attributed in Builder\n * League activity reports.\n */\nexport interface DirectAppConfig {\n /** Stable, human-readable app id (e.g. `\"notes-lens\"`). */\n id: string;\n /** Display name shown to the user in the Vana approval UI. */\n name: string;\n /** Public homepage URL for the app. */\n homepageUrl: string;\n}\n\n/**\n * Resolved app identity: the configured {@link DirectAppConfig} plus the app's\n * derived on-chain address (the address to fund and inspect).\n */\nexport interface AppIdentity extends DirectAppConfig {\n /** The app's `0x`-prefixed on-chain address (derived from `appPrivateKey`). */\n address: string;\n}\n\n/**\n * Resolved service URLs and chain id for a given {@link DirectEnv}.\n *\n * @remarks\n * Centralizes the per-environment base URLs the controller talks to. Each can\n * be overridden via {@link DirectDataControllerConfig.endpoints} when pointing\n * at a non-standard deployment.\n */\nexport interface DirectServiceEndpoints {\n /** Vana chain id for this environment (1480 mainnet, 14800 moksha). */\n chainId: number;\n /** Base URL of the Vana Account access-request API that issues `dcr_*` ids. */\n accessRequestBaseUrl: string;\n /** Base URL users are sent to for approval (the Vana app). */\n approvalAppBaseUrl: string;\n /** Base URL of the DP RPC escrow gateway used to settle `402 Payment Required`. */\n escrowGatewayUrl: string;\n}\n\n/** Result of {@link DirectDataController.createAccessRequest}. */\nexport interface AccessRequest {\n /** Opaque request id (e.g. `\"dcr_123\"`). */\n requestId: string;\n /** URL the browser opens so the user can approve the requested scopes. */\n approvalUrl: string;\n /** On-chain address of the (registered or reused) app. */\n appAddress: string;\n}\n\n/** Lifecycle status of an access request. */\nexport type AccessRequestStatusValue =\n | \"pending\"\n | \"approved\"\n | \"ready_for_read\"\n | \"denied\"\n | \"expired\";\n\n/** Result of {@link DirectDataController.getAccessRequestStatus}. */\nexport interface AccessRequestStatus {\n /** Current lifecycle status of the request. */\n status: AccessRequestStatusValue;\n /** Personal Server base URL — present once data is ready to read. */\n personalServerUrl?: string;\n /** Grant id covering the approved scope — present once data is ready to read. */\n grantId?: string;\n /** The approved scope — present once data is ready to read. */\n scope?: string;\n}\n\n/** Result of {@link DirectDataController.readApprovedData}. */\nexport interface ApprovedDataResult<T = unknown> {\n /** The scope the data was read for. */\n scope: string;\n /** The decoded payload returned by the Personal Server. */\n data: T;\n /**\n * Payment receipt — present only when this read required (and settled) a\n * payment. Lets builders inspect the amount, asset, and fee breakdown without\n * digging into the underlying 402/escrow exchange. Reads served from a paid-up\n * grant omit this field.\n */\n payment?: DirectPaymentReceipt;\n}\n\n/**\n * Client for the Vana Account access-request API — the service that turns a\n * registered app + scopes into a `dcr_*` id and approval URL.\n *\n * @remarks\n * The controller uses a default client against the Vana Account endpoints. You\n * can inject your own implementation to point at a custom deployment or to\n * supply a test double.\n */\nexport interface AccessRequestClient {\n /**\n * Create an access request for the given app + scopes.\n *\n * @param input - App identity, source, scopes, network, and the post-approval return URL.\n * @returns The created {@link AccessRequest}.\n */\n createAccessRequest(input: {\n appAddress: string;\n app: DirectAppConfig;\n source: string;\n scopes: string[];\n returnUrl: string;\n /** Vana protocol network for this request (`\"mainnet\"` or `\"moksha\"`). */\n network: DirectNetwork;\n }): Promise<AccessRequest>;\n\n /**\n * Fetch the current status of a previously created access request.\n *\n * @param requestId - The `dcr_*` id returned by {@link AccessRequestClient.createAccessRequest}.\n * @returns The current {@link AccessRequestStatus}.\n */\n getAccessRequestStatus(requestId: string): Promise<AccessRequestStatus>;\n\n /**\n * Acknowledge that the app successfully read the approved data.\n *\n * @remarks\n * Direct Vana Web DCRs remain in `ready_for_read` while the browser Personal\n * Server is serving the app. After a successful Personal Server read, the\n * controller calls this hook so Vana Web can mark the request completed and\n * close/redirect the approval tab.\n *\n * Optional so injected clients from older SDK integrations keep compiling;\n * the default HTTP client implements it.\n */\n acknowledgeRead?(requestId: string): Promise<void>;\n}\n\n/**\n * Op-type vocabulary used by the DPv2 escrow payment surface.\n *\n * @remarks\n * These are the operations the gateway prices and settles via\n * `POST /v1/escrow/pay` (`opType` field of the `GenericPayment` message). A\n * direct data read settles the {@link DirectOpType.DataAccess} op for the\n * approved grant; the other op types are listed here for completeness and to\n * give builders a typed vocabulary when inspecting fee breakdowns.\n *\n * Note: the escrow `GenericPayment` `opType` is currently `\"grant\"` on the wire\n * for grant lifecycle payments; this enum names the higher-level fee categories\n * the gateway reports in a {@link PaymentBreakdown}.\n */\nexport const DirectOpType = {\n GrantRegistration: \"grant_registration\",\n DataAccess: \"data_access\",\n DataRegistration: \"data_registration\",\n ServerRegistration: \"server_registration\",\n BuilderRegistration: \"builder_registration\",\n} as const;\n\n/** A direct-flow op type (see {@link DirectOpType}). */\nexport type DirectOpTypeValue =\n (typeof DirectOpType)[keyof typeof DirectOpType];\n\n/**\n * What a Personal Server `402 Payment Required` tells the controller is owed for\n * a data read.\n *\n * @remarks\n * The PS read 402 body identifies the grant to settle and the amount/asset. The\n * controller settles it via the DPv2 escrow gateway (`/v1/escrow/pay`). The full\n * unmodified body is preserved under {@link PersonalServerPaymentRequired.raw}.\n */\nexport interface PersonalServerPaymentRequired {\n /** Grant id to settle (the escrow `opId`). Defaults to the read's grantId. */\n grantId: string;\n /** X402 network advertised by the Personal Server challenge. */\n network?: string;\n /** Payment nonce requested by the 402 challenge. */\n paymentNonce?: string;\n /** Server-signed data access receipt requested by the 402 challenge. */\n accessRecord?: EscrowAccessRecord;\n /** Asset address owed (zero address = native VANA). */\n asset: string;\n /** Amount owed, as a decimal base-unit string (preserves uint256 precision). */\n amount: string;\n /** The full, unmodified 402 response body. */\n raw: unknown;\n}\n\n/**\n * Structured payment metadata attached to a successful paid read.\n *\n * @remarks\n * Derived from the gateway's {@link EscrowPayResult}. Lets builders debug the\n * amount, asset, and per-op fee breakdown without re-deriving anything from the\n * raw 402/payment exchange.\n */\nexport interface DirectPaymentReceipt {\n /** Op type settled (the gateway `opType`, e.g. `\"grant\"`). */\n opType: string;\n /** Op id settled (the grant id). */\n opId: string;\n /** Asset paid in (zero address = native VANA). */\n asset: string;\n /** Total amount paid, as a decimal base-unit string. */\n amount: string;\n /** Payment nonce used for this settlement. */\n paymentNonce: string;\n /** Fee breakdown reported by the gateway (registration vs data-access fee). */\n breakdown: DirectFeeBreakdown;\n /** ISO timestamp the gateway recorded the payment. */\n paidAt: string;\n}\n\n/**\n * Per-op fee breakdown reported by the gateway.\n *\n * @remarks\n * Mirrors the escrow {@link PaymentBreakdown}: a one-time registration fee plus\n * the per-read data-access fee, and whether this settlement covered the\n * registration fee.\n */\nexport interface DirectFeeBreakdown {\n /** One-time registration fee for the op, as a decimal base-unit string. */\n registrationFee: string;\n /** Per-read data-access fee, as a decimal base-unit string. */\n dataAccessFee: string;\n /** True when this settlement paid the registration fee. */\n registrationPaid: boolean;\n}\n"],"mappings":"AAsLO,MAAM,eAAe;AAAA,EAC1B,mBAAmB;AAAA,EACnB,YAAY;AAAA,EACZ,kBAAkB;AAAA,EAClB,oBAAoB;AAAA,EACpB,qBAAqB;AACvB;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/direct/types.ts"],"sourcesContent":["import type { EscrowAccessRecord } from \"../protocol/escrow\";\nimport type { ProtocolNetwork } from \"../protocol/networks\";\n\n/**\n * Shared types for the Direct Data Controller and the browser connect helper.\n *\n * @remarks\n * These types describe the \"two-tab\" Data Portability flow documented in the\n * builder guide: a backend controller creates an access request, the browser\n * opens Vana for user approval, and the backend reads the approved data from\n * the user's Personal Server (handling 402 Payment Required).\n *\n * @category Direct\n * @module direct/types\n */\n\n/**\n * Target environment for a {@link DirectDataController}.\n *\n * - `\"production\"` — Vana mainnet stack (default service URLs).\n * - `\"dev\"` — Vana internal dev stack. Use only when testing against\n * Vana's dev infrastructure.\n */\nexport type DirectEnv = \"dev\" | \"production\";\n\n/**\n * Vana network used for chain-aware Direct defaults.\n *\n * - `\"mainnet\"` — Vana mainnet (`chainId` 1480).\n * - `\"moksha\"` — Moksha testnet (`chainId` 14800).\n */\nexport type DirectNetwork = ProtocolNetwork;\n\n/**\n * App identity advertised to users during approval and attributed in Builder\n * League activity reports.\n */\nexport interface DirectAppConfig {\n /** Stable, human-readable app id (e.g. `\"notes-lens\"`). */\n id: string;\n /** Display name shown to the user in the Vana approval UI. */\n name: string;\n /** Public homepage URL for the app. */\n homepageUrl: string;\n}\n\n/**\n * Resolved app identity: the configured {@link DirectAppConfig} plus the app's\n * derived on-chain address (the address to fund and inspect).\n */\nexport interface AppIdentity extends DirectAppConfig {\n /** The app's `0x`-prefixed on-chain address (derived from `appPrivateKey`). */\n address: string;\n}\n\n/**\n * Resolved service URLs and chain id for a given {@link DirectEnv}.\n *\n * @remarks\n * Centralizes the per-environment base URLs the controller talks to. Each can\n * be overridden via {@link DirectDataControllerConfig.endpoints} when pointing\n * at a non-standard deployment.\n */\nexport interface DirectServiceEndpoints {\n /** Vana chain id for this environment (1480 mainnet, 14800 moksha). */\n chainId: number;\n /** Base URL of the Vana Account access-request API that issues `dcr_*` ids. */\n accessRequestBaseUrl: string;\n /** Base URL users are sent to for approval (the Vana app). */\n approvalAppBaseUrl: string;\n /** Base URL of the DP RPC escrow gateway used to settle `402 Payment Required`. */\n escrowGatewayUrl: string;\n}\n\n/** Result of {@link DirectDataController.createAccessRequest}. */\nexport interface AccessRequest {\n /** Opaque request id (e.g. `\"dcr_123\"`). */\n requestId: string;\n /** URL the browser opens so the user can approve the requested scopes. */\n approvalUrl: string;\n /** On-chain address of the (registered or reused) app. */\n appAddress: string;\n}\n\n/**\n * Lifecycle status of an access request.\n *\n * @remarks\n * - `\"pending\"` — created, awaiting user approval.\n * - `\"approved\"` / `\"ready_for_read\"` — the grant exists and the Personal\n * Server is reachable; the data is read-ready (see {@link DirectDataController.readApprovedData}).\n * - `\"completed\"` — the app has already read the data and acknowledged it, so\n * the DCR is terminal. A `\"completed\"` request is **not** read-ready — the\n * browser Personal Server may no longer be serving it.\n * - `\"denied\"` / `\"expired\"` — terminal, no data was delivered.\n */\nexport type AccessRequestStatusValue =\n | \"pending\"\n | \"approved\"\n | \"ready_for_read\"\n | \"completed\"\n | \"denied\"\n | \"expired\";\n\n/** Result of {@link DirectDataController.getAccessRequestStatus}. */\nexport interface AccessRequestStatus {\n /** Current lifecycle status of the request. */\n status: AccessRequestStatusValue;\n /** Personal Server base URL — present once data is ready to read. */\n personalServerUrl?: string;\n /** Grant id covering the approved scope — present once data is ready to read. */\n grantId?: string;\n /** The approved scope — present once data is ready to read. */\n scope?: string;\n}\n\n/** Result of {@link DirectDataController.readApprovedData}. */\nexport interface ApprovedDataResult<T = unknown> {\n /** The scope the data was read for. */\n scope: string;\n /** The decoded payload returned by the Personal Server. */\n data: T;\n /**\n * Payment receipt — present only when this read required (and settled) a\n * payment. Lets builders inspect the amount, asset, and fee breakdown without\n * digging into the underlying 402/escrow exchange. Reads served from a paid-up\n * grant omit this field.\n */\n payment?: DirectPaymentReceipt;\n}\n\n/**\n * Client for the Vana Account access-request API — the service that turns a\n * registered app + scopes into a `dcr_*` id and approval URL.\n *\n * @remarks\n * The controller uses a default client against the Vana Account endpoints. You\n * can inject your own implementation to point at a custom deployment or to\n * supply a test double.\n */\nexport interface AccessRequestClient {\n /**\n * Create an access request for the given app + scopes.\n *\n * @param input - App identity, source, scopes, network, and the post-approval return URL.\n * @returns The created {@link AccessRequest}.\n */\n createAccessRequest(input: {\n appAddress: string;\n app: DirectAppConfig;\n source: string;\n scopes: string[];\n returnUrl: string;\n /** Vana protocol network for this request (`\"mainnet\"` or `\"moksha\"`). */\n network: DirectNetwork;\n }): Promise<AccessRequest>;\n\n /**\n * Fetch the current status of a previously created access request.\n *\n * @param requestId - The `dcr_*` id returned by {@link AccessRequestClient.createAccessRequest}.\n * @returns The current {@link AccessRequestStatus}.\n */\n getAccessRequestStatus(requestId: string): Promise<AccessRequestStatus>;\n\n /**\n * Acknowledge that the app successfully read the approved data.\n *\n * @remarks\n * Direct Vana Web DCRs remain in `ready_for_read` while the browser Personal\n * Server is serving the app. After a successful Personal Server read, the\n * controller calls this hook so Vana Web can mark the request completed and\n * close/redirect the approval tab.\n *\n * Optional so injected clients from older SDK integrations keep compiling;\n * the default HTTP client implements it.\n */\n acknowledgeRead?(requestId: string): Promise<void>;\n}\n\n/**\n * Op-type vocabulary used by the DPv2 escrow payment surface.\n *\n * @remarks\n * These are the operations the gateway prices and settles via\n * `POST /v1/escrow/pay` (`opType` field of the `GenericPayment` message). A\n * direct data read settles the {@link DirectOpType.DataAccess} op for the\n * approved grant; the other op types are listed here for completeness and to\n * give builders a typed vocabulary when inspecting fee breakdowns.\n *\n * Note: the escrow `GenericPayment` `opType` is currently `\"grant\"` on the wire\n * for grant lifecycle payments; this enum names the higher-level fee categories\n * the gateway reports in a {@link PaymentBreakdown}.\n */\nexport const DirectOpType = {\n GrantRegistration: \"grant_registration\",\n DataAccess: \"data_access\",\n DataRegistration: \"data_registration\",\n ServerRegistration: \"server_registration\",\n BuilderRegistration: \"builder_registration\",\n} as const;\n\n/** A direct-flow op type (see {@link DirectOpType}). */\nexport type DirectOpTypeValue =\n (typeof DirectOpType)[keyof typeof DirectOpType];\n\n/**\n * What a Personal Server `402 Payment Required` tells the controller is owed for\n * a data read.\n *\n * @remarks\n * The PS read 402 body identifies the grant to settle and the amount/asset. The\n * controller settles it via the DPv2 escrow gateway (`/v1/escrow/pay`). The full\n * unmodified body is preserved under {@link PersonalServerPaymentRequired.raw}.\n */\nexport interface PersonalServerPaymentRequired {\n /** Grant id to settle (the escrow `opId`). Defaults to the read's grantId. */\n grantId: string;\n /** X402 network advertised by the Personal Server challenge. */\n network?: string;\n /** Payment nonce requested by the 402 challenge. */\n paymentNonce?: string;\n /** Server-signed data access receipt requested by the 402 challenge. */\n accessRecord?: EscrowAccessRecord;\n /** Asset address owed (zero address = native VANA). */\n asset: string;\n /** Amount owed, as a decimal base-unit string (preserves uint256 precision). */\n amount: string;\n /** The full, unmodified 402 response body. */\n raw: unknown;\n}\n\n/**\n * Structured payment metadata attached to a successful paid read.\n *\n * @remarks\n * Derived from the gateway's {@link EscrowPayResult}. Lets builders debug the\n * amount, asset, and per-op fee breakdown without re-deriving anything from the\n * raw 402/payment exchange.\n */\nexport interface DirectPaymentReceipt {\n /** Op type settled (the gateway `opType`, e.g. `\"grant\"`). */\n opType: string;\n /** Op id settled (the grant id). */\n opId: string;\n /** Asset paid in (zero address = native VANA). */\n asset: string;\n /** Total amount paid, as a decimal base-unit string. */\n amount: string;\n /** Payment nonce used for this settlement. */\n paymentNonce: string;\n /** Fee breakdown reported by the gateway (registration vs data-access fee). */\n breakdown: DirectFeeBreakdown;\n /** ISO timestamp the gateway recorded the payment. */\n paidAt: string;\n}\n\n/**\n * Per-op fee breakdown reported by the gateway.\n *\n * @remarks\n * Mirrors the escrow {@link PaymentBreakdown}: a one-time registration fee plus\n * the per-read data-access fee, and whether this settlement covered the\n * registration fee.\n */\nexport interface DirectFeeBreakdown {\n /** One-time registration fee for the op, as a decimal base-unit string. */\n registrationFee: string;\n /** Per-read data-access fee, as a decimal base-unit string. */\n dataAccessFee: string;\n /** True when this settlement paid the registration fee. */\n registrationPaid: boolean;\n}\n"],"mappings":"AAkMO,MAAM,eAAe;AAAA,EAC1B,mBAAmB;AAAA,EACnB,YAAY;AAAA,EACZ,kBAAkB;AAAA,EAClB,oBAAoB;AAAA,EACpB,qBAAqB;AACvB;","names":[]}
|