@opendatalabs/vana-sdk 3.5.0 → 3.5.1-pr.159.12a6f39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/README.md +116 -0
  2. package/dist/direct/access-request-client.cjs +104 -0
  3. package/dist/direct/access-request-client.cjs.map +1 -0
  4. package/dist/direct/access-request-client.d.ts +51 -0
  5. package/dist/direct/access-request-client.js +79 -0
  6. package/dist/direct/access-request-client.js.map +1 -0
  7. package/dist/direct/access-request-client.test.d.ts +1 -0
  8. package/dist/direct/connect-flow.cjs +152 -0
  9. package/dist/direct/connect-flow.cjs.map +1 -0
  10. package/dist/direct/connect-flow.d.ts +85 -0
  11. package/dist/direct/connect-flow.js +128 -0
  12. package/dist/direct/connect-flow.js.map +1 -0
  13. package/dist/direct/connect-flow.test.d.ts +1 -0
  14. package/dist/direct/controller.cjs +129 -0
  15. package/dist/direct/controller.cjs.map +1 -0
  16. package/dist/direct/controller.d.ts +152 -0
  17. package/dist/direct/controller.js +109 -0
  18. package/dist/direct/controller.js.map +1 -0
  19. package/dist/direct/controller.test.d.ts +1 -0
  20. package/dist/direct/endpoints.cjs +45 -0
  21. package/dist/direct/endpoints.cjs.map +1 -0
  22. package/dist/direct/endpoints.d.ts +22 -0
  23. package/dist/direct/endpoints.js +19 -0
  24. package/dist/direct/endpoints.js.map +1 -0
  25. package/dist/direct/errors.cjs +65 -0
  26. package/dist/direct/errors.cjs.map +1 -0
  27. package/dist/direct/errors.d.ts +44 -0
  28. package/dist/direct/errors.js +38 -0
  29. package/dist/direct/errors.js.map +1 -0
  30. package/dist/direct/escrow-payment.cjs +96 -0
  31. package/dist/direct/escrow-payment.cjs.map +1 -0
  32. package/dist/direct/escrow-payment.d.ts +81 -0
  33. package/dist/direct/escrow-payment.js +72 -0
  34. package/dist/direct/escrow-payment.js.map +1 -0
  35. package/dist/direct/escrow-payment.test.d.ts +1 -0
  36. package/dist/direct/personal-server-read.cjs +149 -0
  37. package/dist/direct/personal-server-read.cjs.map +1 -0
  38. package/dist/direct/personal-server-read.d.ts +103 -0
  39. package/dist/direct/personal-server-read.js +124 -0
  40. package/dist/direct/personal-server-read.js.map +1 -0
  41. package/dist/direct/personal-server-read.test.d.ts +1 -0
  42. package/dist/direct/types.cjs +35 -0
  43. package/dist/direct/types.cjs.map +1 -0
  44. package/dist/direct/types.d.ts +205 -0
  45. package/dist/direct/types.js +11 -0
  46. package/dist/direct/types.js.map +1 -0
  47. package/dist/direct/use-direct-vana-connect.cjs +68 -0
  48. package/dist/direct/use-direct-vana-connect.cjs.map +1 -0
  49. package/dist/direct/use-direct-vana-connect.d.ts +45 -0
  50. package/dist/direct/use-direct-vana-connect.js +46 -0
  51. package/dist/direct/use-direct-vana-connect.js.map +1 -0
  52. package/dist/index.browser.d.ts +7 -3
  53. package/dist/index.browser.js +513 -174
  54. package/dist/index.browser.js.map +4 -4
  55. package/dist/index.node.cjs +536 -179
  56. package/dist/index.node.cjs.map +4 -4
  57. package/dist/index.node.d.ts +7 -3
  58. package/dist/index.node.js +513 -174
  59. package/dist/index.node.js.map +4 -4
  60. package/dist/protocol/data-point-status.cjs +80 -0
  61. package/dist/protocol/data-point-status.cjs.map +1 -0
  62. package/dist/protocol/data-point-status.d.ts +34 -0
  63. package/dist/protocol/data-point-status.js +51 -0
  64. package/dist/protocol/data-point-status.js.map +1 -0
  65. package/dist/protocol/data-point-status.test.d.ts +1 -0
  66. package/dist/protocol/eip712.cjs +53 -31
  67. package/dist/protocol/eip712.cjs.map +1 -1
  68. package/dist/protocol/eip712.d.ts +98 -43
  69. package/dist/protocol/eip712.js +47 -27
  70. package/dist/protocol/eip712.js.map +1 -1
  71. package/dist/protocol/escrow-deposit.cjs +89 -0
  72. package/dist/protocol/escrow-deposit.cjs.map +1 -0
  73. package/dist/protocol/escrow-deposit.d.ts +47 -0
  74. package/dist/protocol/escrow-deposit.js +60 -0
  75. package/dist/protocol/escrow-deposit.js.map +1 -0
  76. package/dist/protocol/escrow-deposit.test.d.ts +1 -0
  77. package/dist/protocol/escrow-flow.test.d.ts +21 -0
  78. package/dist/protocol/fee-registry.cjs +116 -0
  79. package/dist/protocol/fee-registry.cjs.map +1 -0
  80. package/dist/protocol/fee-registry.d.ts +151 -0
  81. package/dist/protocol/fee-registry.js +89 -0
  82. package/dist/protocol/fee-registry.js.map +1 -0
  83. package/dist/protocol/fee-registry.test.d.ts +1 -0
  84. package/dist/protocol/gateway.cjs +107 -37
  85. package/dist/protocol/gateway.cjs.map +1 -1
  86. package/dist/protocol/gateway.d.ts +223 -57
  87. package/dist/protocol/gateway.js +107 -37
  88. package/dist/protocol/gateway.js.map +1 -1
  89. package/dist/protocol/grants.cjs +27 -64
  90. package/dist/protocol/grants.cjs.map +1 -1
  91. package/dist/protocol/grants.d.ts +6 -13
  92. package/dist/protocol/grants.js +27 -63
  93. package/dist/protocol/grants.js.map +1 -1
  94. package/dist/protocol/personal-server-data.cjs +71 -0
  95. package/dist/protocol/personal-server-data.cjs.map +1 -0
  96. package/dist/protocol/personal-server-data.d.ts +16 -0
  97. package/dist/protocol/personal-server-data.js +47 -0
  98. package/dist/protocol/personal-server-data.js.map +1 -0
  99. package/dist/protocol/personal-server-data.test.d.ts +1 -0
  100. package/dist/protocol/personal-server-lite-owner-binding.cjs +93 -0
  101. package/dist/protocol/personal-server-lite-owner-binding.cjs.map +1 -0
  102. package/dist/protocol/personal-server-lite-owner-binding.d.ts +44 -0
  103. package/dist/protocol/personal-server-lite-owner-binding.js +65 -0
  104. package/dist/protocol/personal-server-lite-owner-binding.js.map +1 -0
  105. package/dist/protocol/personal-server-lite-owner-binding.test.d.ts +1 -0
  106. package/dist/react.cjs +32 -0
  107. package/dist/react.cjs.map +1 -0
  108. package/dist/react.d.ts +33 -0
  109. package/dist/react.js +11 -0
  110. package/dist/react.js.map +1 -0
  111. package/dist/server.cjs +73 -0
  112. package/dist/server.cjs.map +1 -0
  113. package/dist/server.d.ts +32 -0
  114. package/dist/server.js +55 -0
  115. package/dist/server.js.map +1 -0
  116. package/dist/storage/providers/vana-storage.cjs +75 -17
  117. package/dist/storage/providers/vana-storage.cjs.map +1 -1
  118. package/dist/storage/providers/vana-storage.js +75 -17
  119. package/dist/storage/providers/vana-storage.js.map +1 -1
  120. package/package.json +20 -1
package/README.md CHANGED
@@ -104,6 +104,122 @@ const result = await storage.upload(myBlob, "report.json");
104
104
  console.log(result.url);
105
105
  ```
106
106
 
107
+ ## Build a direct Vana app
108
+
109
+ Request user-approved data, read it from the user's Personal Server, and pay
110
+ for the read — without the browser ever seeing your app private key or choosing
111
+ scopes. Your **backend** owns the controller (`@opendatalabs/vana-sdk/server`);
112
+ your **frontend** drives a two-tab approval flow with a React hook
113
+ (`@opendatalabs/vana-sdk/react`).
114
+
115
+ > **How it fits together.** Access requests are created through the Vana Account
116
+ > access-request API; the Personal Server read uses Web3Signed auth; and payment
117
+ > settles on a `402` through the DPv2 escrow surface (`protocol/escrow`), where the
118
+ > controller signs a `GenericPayment` with your app key. You can inject your own
119
+ > `accessRequestClient` to target a custom deployment, and `escrow` config to wire
120
+ > the escrow gateway.
121
+
122
+ ### Backend controller
123
+
124
+ ```typescript
125
+ // lib/vana.ts
126
+ import { createDirectDataController } from "@opendatalabs/vana-sdk/server";
127
+
128
+ import { createEscrowGatewayClient } from "@opendatalabs/vana-sdk/node";
129
+
130
+ export const vana = createDirectDataController({
131
+ env: process.env.VANA_ENV === "dev" ? "dev" : "production",
132
+ appPrivateKey: process.env.VANA_APP_PRIVATE_KEY!,
133
+ app: {
134
+ id: "notes-lens",
135
+ name: "Notes Lens",
136
+ homepageUrl: process.env.VANA_APP_URL!,
137
+ },
138
+ source: "icloud_notes",
139
+ scopes: ["icloud_notes.notes"],
140
+ // Settle paid reads through the DPv2 escrow gateway. The controller signs the
141
+ // GenericPayment with your app key; you supply the gateway client + contract.
142
+ escrow: {
143
+ client: createEscrowGatewayClient(process.env.VANA_DP_RPC_URL!),
144
+ escrowContract: process.env.VANA_ESCROW_CONTRACT! as `0x${string}`,
145
+ },
146
+ });
147
+
148
+ // The app's on-chain address — fund and inspect this in the Builder activity
149
+ // report. (`vana.getAppIdentity()` also returns the configured id/name/homepage.)
150
+ console.log(vana.getAppAddress()); // 0x...
151
+ ```
152
+
153
+ Wire it to three routes — your backend chooses the source and scopes, owns the
154
+ private key, and handles `402 Payment Required`:
155
+
156
+ ```typescript
157
+ // POST /api/vana/request
158
+ const request = await vana.createAccessRequest({
159
+ returnUrl: `${process.env.VANA_APP_URL}/connect/return`,
160
+ });
161
+ // -> { requestId: "dcr_...", approvalUrl: "https://app.vana.org/...", appAddress: "0x..." }
162
+
163
+ // GET /api/vana/status?requestId=...
164
+ const status = await vana.getAccessRequestStatus(requestId);
165
+ // -> { status: "approved", personalServerUrl, grantId, scope }
166
+
167
+ // GET /api/vana/data?requestId=...
168
+ const result = await vana.readApprovedData({ requestId });
169
+ // -> {
170
+ // scope: "icloud_notes.notes",
171
+ // data: ...,
172
+ // payment?: { // present only when this read settled a payment
173
+ // amount, asset, paymentNonce, paidAt,
174
+ // breakdown: { registrationFee, dataAccessFee, registrationPaid },
175
+ // },
176
+ // }
177
+ ```
178
+
179
+ `readApprovedData` hides the payment flow for normal builders. If the Personal
180
+ Server returns `402 Payment Required`, the controller settles the grant through
181
+ the escrow gateway and retries, attaching a `payment` receipt so you can inspect
182
+ the amount, asset, and fee breakdown. If `escrow` is not configured (or the read
183
+ still requires payment afterward), it throws `PaymentRequiredError` carrying the
184
+ amount and asset owed.
185
+
186
+ ### Frontend hook
187
+
188
+ ```tsx
189
+ "use client";
190
+ import { useDirectVanaConnect } from "@opendatalabs/vana-sdk/react";
191
+
192
+ export function ConnectNotesButton() {
193
+ const connect = useDirectVanaConnect({
194
+ createRequest: () =>
195
+ fetch("/api/vana/request", { method: "POST" }).then((r) => r.json()),
196
+ getStatus: (requestId) =>
197
+ fetch(`/api/vana/status?requestId=${encodeURIComponent(requestId)}`).then(
198
+ (r) => r.json(),
199
+ ),
200
+ readResult: (requestId) =>
201
+ fetch(`/api/vana/data?requestId=${encodeURIComponent(requestId)}`).then(
202
+ (r) => r.json(),
203
+ ),
204
+ });
205
+
206
+ return (
207
+ <button
208
+ disabled={connect.state.type !== "idle"}
209
+ onClick={connect.start}
210
+ type="button"
211
+ >
212
+ {connect.state.type === "idle" ? "Connect Apple Notes" : "Connecting..."}
213
+ </button>
214
+ );
215
+ }
216
+ ```
217
+
218
+ The hook calls `createRequest`, opens the Vana approval URL, polls `getStatus`
219
+ until the request is approved, then calls `readResult`. `react` is an optional
220
+ peer dependency. The underlying `createDirectConnectFlow` store is also exported
221
+ for non-React frontends.
222
+
107
223
  ## Networks
108
224
 
109
225
  | Network | Chain ID | RPC URL |
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var access_request_client_exports = {};
20
+ __export(access_request_client_exports, {
21
+ buildApprovalUrl: () => buildApprovalUrl,
22
+ createDefaultAccessRequestClient: () => createDefaultAccessRequestClient
23
+ });
24
+ module.exports = __toCommonJS(access_request_client_exports);
25
+ const VALID_STATUSES = [
26
+ "pending",
27
+ "approved",
28
+ "denied",
29
+ "expired"
30
+ ];
31
+ function normalizeStatus(value) {
32
+ return VALID_STATUSES.includes(value) ? value : "pending";
33
+ }
34
+ function stripTrailingSlash(url) {
35
+ return url.replace(/\/+$/, "");
36
+ }
37
+ function buildApprovalUrl(approvalBaseUrl, requestId) {
38
+ return `${stripTrailingSlash(approvalBaseUrl)}/data-connection-requests/${encodeURIComponent(
39
+ requestId
40
+ )}?mode=page`;
41
+ }
42
+ function createDefaultAccessRequestClient(options) {
43
+ const fetchFn = options.fetchFn ?? globalThis.fetch;
44
+ if (!fetchFn) {
45
+ throw new Error(
46
+ "No fetch implementation available. Pass `fetchFn` to createDefaultAccessRequestClient."
47
+ );
48
+ }
49
+ const base = stripTrailingSlash(options.baseUrl);
50
+ return {
51
+ async createAccessRequest(input) {
52
+ const res = await fetchFn(`${base}/api/v1/data-connection-requests`, {
53
+ method: "POST",
54
+ headers: { "Content-Type": "application/json" },
55
+ body: JSON.stringify({
56
+ appAddress: input.appAddress,
57
+ app: input.app,
58
+ source: input.source,
59
+ scopes: input.scopes,
60
+ returnUrl: input.returnUrl
61
+ })
62
+ });
63
+ if (!res.ok) {
64
+ throw new Error(
65
+ `Access request service error: ${res.status} ${res.statusText}`
66
+ );
67
+ }
68
+ const body = await res.json();
69
+ const requestId = body.requestId ?? body.id;
70
+ if (!requestId) {
71
+ throw new Error("Access request service returned no requestId");
72
+ }
73
+ return {
74
+ requestId,
75
+ approvalUrl: body.approvalUrl ?? buildApprovalUrl(options.approvalBaseUrl, requestId),
76
+ appAddress: body.appAddress ?? input.appAddress
77
+ };
78
+ },
79
+ async getAccessRequestStatus(requestId) {
80
+ const res = await fetchFn(
81
+ `${base}/api/v1/data-connection-requests/${encodeURIComponent(requestId)}`,
82
+ { method: "GET" }
83
+ );
84
+ if (!res.ok) {
85
+ throw new Error(
86
+ `Access request service error: ${res.status} ${res.statusText}`
87
+ );
88
+ }
89
+ const body = await res.json();
90
+ return {
91
+ status: normalizeStatus(body.status),
92
+ personalServerUrl: body.personalServerUrl,
93
+ grantId: body.grantId,
94
+ scope: body.scope
95
+ };
96
+ }
97
+ };
98
+ }
99
+ // Annotate the CommonJS export names for ESM import in node:
100
+ 0 && (module.exports = {
101
+ buildApprovalUrl,
102
+ createDefaultAccessRequestClient
103
+ });
104
+ //# sourceMappingURL=access-request-client.cjs.map
@@ -0,0 +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\";\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}\n\nconst VALID_STATUSES: readonly AccessRequestStatusValue[] = [\n \"pending\",\n \"approved\",\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\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 res = await fetchFn(`${base}/api/v1/data-connection-requests`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n appAddress: input.appAddress,\n app: input.app,\n source: input.source,\n scopes: input.scopes,\n returnUrl: input.returnUrl,\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 requestId?: string;\n id?: string;\n approvalUrl?: string;\n appAddress?: string;\n };\n const requestId = body.requestId ?? body.id;\n if (!requestId) {\n throw new Error(\"Access request service returned no requestId\");\n }\n return {\n requestId,\n approvalUrl:\n body.approvalUrl ??\n buildApprovalUrl(options.approvalBaseUrl, requestId),\n appAddress: body.appAddress ?? input.appAddress,\n };\n },\n\n async getAccessRequestStatus(\n requestId: string,\n ): Promise<AccessRequestStatus> {\n const res = await fetchFn(\n `${base}/api/v1/data-connection-requests/${encodeURIComponent(requestId)}`,\n { method: \"GET\" },\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}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA8CA,MAAM,iBAAsD;AAAA,EAC1D;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,SAAS,gBAAgB,OAA0C;AACjE,SAAO,eAAe,SAAS,KAAiC,IAC3D,QACD;AACN;AAEA,SAAS,mBAAmB,KAAqB;AAC/C,SAAO,IAAI,QAAQ,QAAQ,EAAE;AAC/B;AAUO,SAAS,iBACd,iBACA,WACQ;AACR,SAAO,GAAG,mBAAmB,eAAe,CAAC,6BAA6B;AAAA,IACxE;AAAA,EACF,CAAC;AACH;AASO,SAAS,iCACd,SACqB;AACrB,QAAM,UAAU,QAAQ,WAAY,WAAW;AAC/C,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,QAAM,OAAO,mBAAmB,QAAQ,OAAO;AAE/C,SAAO;AAAA,IACL,MAAM,oBAAoB,OAA+B;AACvD,YAAM,MAAM,MAAM,QAAQ,GAAG,IAAI,oCAAoC;AAAA,QACnE,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C,MAAM,KAAK,UAAU;AAAA,UACnB,YAAY,MAAM;AAAA,UAClB,KAAK,MAAM;AAAA,UACX,QAAQ,MAAM;AAAA,UACd,QAAQ,MAAM;AAAA,UACd,WAAW,MAAM;AAAA,QACnB,CAAC;AAAA,MACH,CAAC;AACD,UAAI,CAAC,IAAI,IAAI;AACX,cAAM,IAAI;AAAA,UACR,iCAAiC,IAAI,MAAM,IAAI,IAAI,UAAU;AAAA,QAC/D;AAAA,MACF;AACA,YAAM,OAAQ,MAAM,IAAI,KAAK;AAM7B,YAAM,YAAY,KAAK,aAAa,KAAK;AACzC,UAAI,CAAC,WAAW;AACd,cAAM,IAAI,MAAM,8CAA8C;AAAA,MAChE;AACA,aAAO;AAAA,QACL;AAAA,QACA,aACE,KAAK,eACL,iBAAiB,QAAQ,iBAAiB,SAAS;AAAA,QACrD,YAAY,KAAK,cAAc,MAAM;AAAA,MACvC;AAAA,IACF;AAAA,IAEA,MAAM,uBACJ,WAC8B;AAC9B,YAAM,MAAM,MAAM;AAAA,QAChB,GAAG,IAAI,oCAAoC,mBAAmB,SAAS,CAAC;AAAA,QACxE,EAAE,QAAQ,MAAM;AAAA,MAClB;AACA,UAAI,CAAC,IAAI,IAAI;AACX,cAAM,IAAI;AAAA,UACR,iCAAiC,IAAI,MAAM,IAAI,IAAI,UAAU;AAAA,QAC/D;AAAA,MACF;AACA,YAAM,OAAQ,MAAM,IAAI,KAAK;AAM7B,aAAO;AAAA,QACL,QAAQ,gBAAgB,KAAK,MAAM;AAAA,QACnC,mBAAmB,KAAK;AAAA,QACxB,SAAS,KAAK;AAAA,QACd,OAAO,KAAK;AAAA,MACd;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Default client for the Vana Account access-request API.
3
+ *
4
+ * @remarks
5
+ * Calls the Vana Account endpoints that issue `dcr_*` ids and approval URLs and
6
+ * report request status. Inject a custom {@link AccessRequestClient} on the
7
+ * controller to point at a different deployment; pass `fetchFn` to supply a test
8
+ * double for the HTTP layer.
9
+ *
10
+ * @category Direct
11
+ * @module direct/access-request-client
12
+ */
13
+ import type { AccessRequestClient } from "./types";
14
+ /** Minimal `fetch` signature so the client is testable without a global fetch. */
15
+ export type FetchLike = (input: string, init?: {
16
+ method?: string;
17
+ headers?: Record<string, string>;
18
+ body?: string;
19
+ }) => Promise<{
20
+ ok: boolean;
21
+ status: number;
22
+ statusText: string;
23
+ json(): Promise<unknown>;
24
+ text(): Promise<string>;
25
+ }>;
26
+ /** Options for {@link createDefaultAccessRequestClient}. */
27
+ export interface DefaultAccessRequestClientOptions {
28
+ /** Base URL of the Vana Account access-request API. */
29
+ baseUrl: string;
30
+ /** Base URL the user is sent to for approval. */
31
+ approvalBaseUrl: string;
32
+ /** `fetch` implementation. Defaults to the global `fetch`. */
33
+ fetchFn?: FetchLike;
34
+ }
35
+ /**
36
+ * Build an approval URL for a request id, matching the documented format
37
+ * (`{app}/data-connection-requests/{requestId}?mode=page`).
38
+ *
39
+ * @param approvalBaseUrl - Base URL of the Vana approval app.
40
+ * @param requestId - The `dcr_*` request id.
41
+ * @returns The full approval URL.
42
+ */
43
+ export declare function buildApprovalUrl(approvalBaseUrl: string, requestId: string): string;
44
+ /**
45
+ * Create the default {@link AccessRequestClient} for the Vana Account
46
+ * access-request API.
47
+ *
48
+ * @param options - Base URLs and an optional `fetch` implementation.
49
+ * @returns An {@link AccessRequestClient} backed by HTTP calls.
50
+ */
51
+ export declare function createDefaultAccessRequestClient(options: DefaultAccessRequestClientOptions): AccessRequestClient;
@@ -0,0 +1,79 @@
1
+ const VALID_STATUSES = [
2
+ "pending",
3
+ "approved",
4
+ "denied",
5
+ "expired"
6
+ ];
7
+ function normalizeStatus(value) {
8
+ return VALID_STATUSES.includes(value) ? value : "pending";
9
+ }
10
+ function stripTrailingSlash(url) {
11
+ return url.replace(/\/+$/, "");
12
+ }
13
+ function buildApprovalUrl(approvalBaseUrl, requestId) {
14
+ return `${stripTrailingSlash(approvalBaseUrl)}/data-connection-requests/${encodeURIComponent(
15
+ requestId
16
+ )}?mode=page`;
17
+ }
18
+ function createDefaultAccessRequestClient(options) {
19
+ const fetchFn = options.fetchFn ?? globalThis.fetch;
20
+ if (!fetchFn) {
21
+ throw new Error(
22
+ "No fetch implementation available. Pass `fetchFn` to createDefaultAccessRequestClient."
23
+ );
24
+ }
25
+ const base = stripTrailingSlash(options.baseUrl);
26
+ return {
27
+ async createAccessRequest(input) {
28
+ const res = await fetchFn(`${base}/api/v1/data-connection-requests`, {
29
+ method: "POST",
30
+ headers: { "Content-Type": "application/json" },
31
+ body: JSON.stringify({
32
+ appAddress: input.appAddress,
33
+ app: input.app,
34
+ source: input.source,
35
+ scopes: input.scopes,
36
+ returnUrl: input.returnUrl
37
+ })
38
+ });
39
+ if (!res.ok) {
40
+ throw new Error(
41
+ `Access request service error: ${res.status} ${res.statusText}`
42
+ );
43
+ }
44
+ const body = await res.json();
45
+ const requestId = body.requestId ?? body.id;
46
+ if (!requestId) {
47
+ throw new Error("Access request service returned no requestId");
48
+ }
49
+ return {
50
+ requestId,
51
+ approvalUrl: body.approvalUrl ?? buildApprovalUrl(options.approvalBaseUrl, requestId),
52
+ appAddress: body.appAddress ?? input.appAddress
53
+ };
54
+ },
55
+ async getAccessRequestStatus(requestId) {
56
+ const res = await fetchFn(
57
+ `${base}/api/v1/data-connection-requests/${encodeURIComponent(requestId)}`,
58
+ { method: "GET" }
59
+ );
60
+ if (!res.ok) {
61
+ throw new Error(
62
+ `Access request service error: ${res.status} ${res.statusText}`
63
+ );
64
+ }
65
+ const body = await res.json();
66
+ return {
67
+ status: normalizeStatus(body.status),
68
+ personalServerUrl: body.personalServerUrl,
69
+ grantId: body.grantId,
70
+ scope: body.scope
71
+ };
72
+ }
73
+ };
74
+ }
75
+ export {
76
+ buildApprovalUrl,
77
+ createDefaultAccessRequestClient
78
+ };
79
+ //# sourceMappingURL=access-request-client.js.map
@@ -0,0 +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\";\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}\n\nconst VALID_STATUSES: readonly AccessRequestStatusValue[] = [\n \"pending\",\n \"approved\",\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\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 res = await fetchFn(`${base}/api/v1/data-connection-requests`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n appAddress: input.appAddress,\n app: input.app,\n source: input.source,\n scopes: input.scopes,\n returnUrl: input.returnUrl,\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 requestId?: string;\n id?: string;\n approvalUrl?: string;\n appAddress?: string;\n };\n const requestId = body.requestId ?? body.id;\n if (!requestId) {\n throw new Error(\"Access request service returned no requestId\");\n }\n return {\n requestId,\n approvalUrl:\n body.approvalUrl ??\n buildApprovalUrl(options.approvalBaseUrl, requestId),\n appAddress: body.appAddress ?? input.appAddress,\n };\n },\n\n async getAccessRequestStatus(\n requestId: string,\n ): Promise<AccessRequestStatus> {\n const res = await fetchFn(\n `${base}/api/v1/data-connection-requests/${encodeURIComponent(requestId)}`,\n { method: \"GET\" },\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}\n"],"mappings":"AA8CA,MAAM,iBAAsD;AAAA,EAC1D;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,SAAS,gBAAgB,OAA0C;AACjE,SAAO,eAAe,SAAS,KAAiC,IAC3D,QACD;AACN;AAEA,SAAS,mBAAmB,KAAqB;AAC/C,SAAO,IAAI,QAAQ,QAAQ,EAAE;AAC/B;AAUO,SAAS,iBACd,iBACA,WACQ;AACR,SAAO,GAAG,mBAAmB,eAAe,CAAC,6BAA6B;AAAA,IACxE;AAAA,EACF,CAAC;AACH;AASO,SAAS,iCACd,SACqB;AACrB,QAAM,UAAU,QAAQ,WAAY,WAAW;AAC/C,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,QAAM,OAAO,mBAAmB,QAAQ,OAAO;AAE/C,SAAO;AAAA,IACL,MAAM,oBAAoB,OAA+B;AACvD,YAAM,MAAM,MAAM,QAAQ,GAAG,IAAI,oCAAoC;AAAA,QACnE,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C,MAAM,KAAK,UAAU;AAAA,UACnB,YAAY,MAAM;AAAA,UAClB,KAAK,MAAM;AAAA,UACX,QAAQ,MAAM;AAAA,UACd,QAAQ,MAAM;AAAA,UACd,WAAW,MAAM;AAAA,QACnB,CAAC;AAAA,MACH,CAAC;AACD,UAAI,CAAC,IAAI,IAAI;AACX,cAAM,IAAI;AAAA,UACR,iCAAiC,IAAI,MAAM,IAAI,IAAI,UAAU;AAAA,QAC/D;AAAA,MACF;AACA,YAAM,OAAQ,MAAM,IAAI,KAAK;AAM7B,YAAM,YAAY,KAAK,aAAa,KAAK;AACzC,UAAI,CAAC,WAAW;AACd,cAAM,IAAI,MAAM,8CAA8C;AAAA,MAChE;AACA,aAAO;AAAA,QACL;AAAA,QACA,aACE,KAAK,eACL,iBAAiB,QAAQ,iBAAiB,SAAS;AAAA,QACrD,YAAY,KAAK,cAAc,MAAM;AAAA,MACvC;AAAA,IACF;AAAA,IAEA,MAAM,uBACJ,WAC8B;AAC9B,YAAM,MAAM,MAAM;AAAA,QAChB,GAAG,IAAI,oCAAoC,mBAAmB,SAAS,CAAC;AAAA,QACxE,EAAE,QAAQ,MAAM;AAAA,MAClB;AACA,UAAI,CAAC,IAAI,IAAI;AACX,cAAM,IAAI;AAAA,UACR,iCAAiC,IAAI,MAAM,IAAI,IAAI,UAAU;AAAA,QAC/D;AAAA,MACF;AACA,YAAM,OAAQ,MAAM,IAAI,KAAK;AAM7B,aAAO;AAAA,QACL,QAAQ,gBAAgB,KAAK,MAAM;AAAA,QACnC,mBAAmB,KAAK;AAAA,QACxB,SAAS,KAAK;AAAA,QACd,OAAO,KAAK;AAAA,MACd;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var connect_flow_exports = {};
20
+ __export(connect_flow_exports, {
21
+ createDirectConnectFlow: () => createDirectConnectFlow
22
+ });
23
+ module.exports = __toCommonJS(connect_flow_exports);
24
+ const DEFAULT_POLL_INTERVAL_MS = 1500;
25
+ const DEFAULT_TIMEOUT_MS = 3e5;
26
+ function toError(value) {
27
+ return value instanceof Error ? value : new Error(String(value));
28
+ }
29
+ function createDirectConnectFlow(transports, options = {}) {
30
+ const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
31
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
32
+ const openWindow = options.openWindow ?? ((url) => {
33
+ if (typeof window !== "undefined" && window.open) {
34
+ window.open(url, "_blank", "noopener,noreferrer");
35
+ }
36
+ });
37
+ const setTimeoutFn = options.setTimeoutFn ?? ((cb, ms) => globalThis.setTimeout(cb, ms));
38
+ const clearTimeoutFn = options.clearTimeoutFn ?? ((handle) => {
39
+ globalThis.clearTimeout(handle);
40
+ });
41
+ const now = options.now ?? (() => Date.now());
42
+ let state = { type: "idle" };
43
+ const listeners = /* @__PURE__ */ new Set();
44
+ let pollHandle = null;
45
+ let running = false;
46
+ function emit() {
47
+ for (const listener of listeners) listener();
48
+ }
49
+ function setState(next) {
50
+ state = next;
51
+ emit();
52
+ }
53
+ function clearPoll() {
54
+ if (pollHandle !== null) {
55
+ clearTimeoutFn(pollHandle);
56
+ pollHandle = null;
57
+ }
58
+ }
59
+ function isRunningPhase() {
60
+ return state.type === "creating" || state.type === "awaiting_approval" || state.type === "reading";
61
+ }
62
+ async function readAndFinish(request) {
63
+ setState({ type: "reading", request });
64
+ try {
65
+ const result = await transports.readResult(request.requestId);
66
+ if (!running) return;
67
+ setState({ type: "done", result });
68
+ } catch (err) {
69
+ if (!running) return;
70
+ setState({ type: "error", error: toError(err) });
71
+ } finally {
72
+ running = false;
73
+ }
74
+ }
75
+ function scheduleNextPoll(request, deadline) {
76
+ pollHandle = setTimeoutFn(() => {
77
+ void poll(request, deadline);
78
+ }, pollIntervalMs);
79
+ }
80
+ async function poll(request, deadline) {
81
+ if (!running) return;
82
+ if (now() >= deadline) {
83
+ running = false;
84
+ setState({
85
+ type: "error",
86
+ error: new Error("Timed out waiting for approval")
87
+ });
88
+ return;
89
+ }
90
+ let status;
91
+ try {
92
+ status = await transports.getStatus(request.requestId);
93
+ } catch (err) {
94
+ if (!running) return;
95
+ running = false;
96
+ setState({ type: "error", error: toError(err) });
97
+ return;
98
+ }
99
+ if (!running) return;
100
+ if (status.status === "approved") {
101
+ clearPoll();
102
+ await readAndFinish(request);
103
+ return;
104
+ }
105
+ if (status.status === "denied" || status.status === "expired") {
106
+ running = false;
107
+ setState({
108
+ type: "error",
109
+ error: new Error(`Access request ${status.status}`)
110
+ });
111
+ return;
112
+ }
113
+ scheduleNextPoll(request, deadline);
114
+ }
115
+ return {
116
+ getState() {
117
+ return state;
118
+ },
119
+ subscribe(listener) {
120
+ listeners.add(listener);
121
+ return () => listeners.delete(listener);
122
+ },
123
+ async start() {
124
+ if (running || isRunningPhase()) return;
125
+ running = true;
126
+ setState({ type: "creating" });
127
+ let request;
128
+ try {
129
+ request = await transports.createRequest();
130
+ } catch (err) {
131
+ running = false;
132
+ setState({ type: "error", error: toError(err) });
133
+ return;
134
+ }
135
+ if (!running) return;
136
+ setState({ type: "awaiting_approval", request });
137
+ openWindow(request.approvalUrl);
138
+ const deadline = now() + timeoutMs;
139
+ scheduleNextPoll(request, deadline);
140
+ },
141
+ reset() {
142
+ running = false;
143
+ clearPoll();
144
+ setState({ type: "idle" });
145
+ }
146
+ };
147
+ }
148
+ // Annotate the CommonJS export names for ESM import in node:
149
+ 0 && (module.exports = {
150
+ createDirectConnectFlow
151
+ });
152
+ //# sourceMappingURL=connect-flow.cjs.map
@@ -0,0 +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 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/** 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 /** Opens the approval URL. Defaults to `window.open`. Injectable for tests. */\n openWindow?: (url: string) => void;\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 | { type: \"awaiting_approval\"; request: AccessRequest }\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\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 const openWindow =\n options.openWindow ??\n ((url: string) => {\n if (typeof window !== \"undefined\" && window.open) {\n window.open(url, \"_blank\", \"noopener,noreferrer\");\n }\n });\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\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 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 (status.status === \"approved\") {\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 setState({ type: \"creating\" });\n\n let request: AccessRequest;\n try {\n request = await transports.createRequest();\n } catch (err) {\n running = false;\n setState({ type: \"error\", error: toError(err) });\n return;\n }\n if (!running) return;\n\n setState({ type: \"awaiting_approval\", request });\n openWindow(request.approvalUrl);\n\n const deadline = now() + timeoutMs;\n scheduleNextPoll(request, deadline);\n },\n\n reset(): void {\n running = false;\n clearPoll();\n setState({ type: \"idle\" });\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AA6EA,MAAM,2BAA2B;AACjC,MAAM,qBAAqB;AAE3B,SAAS,QAAQ,OAAuB;AACtC,SAAO,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC;AACjE;AASO,SAAS,wBACd,YACA,UAAgC,CAAC,GACX;AACtB,QAAM,iBAAiB,QAAQ,kBAAkB;AACjD,QAAM,YAAY,QAAQ,aAAa;AACvC,QAAM,aACJ,QAAQ,eACP,CAAC,QAAgB;AAChB,QAAI,OAAO,WAAW,eAAe,OAAO,MAAM;AAChD,aAAO,KAAK,KAAK,UAAU,qBAAqB;AAAA,IAClD;AAAA,EACF;AACF,QAAM,eACJ,QAAQ,iBACP,CAAC,IAAgB,OAAe,WAAW,WAAW,IAAI,EAAE;AAC/D,QAAM,iBACJ,QAAQ,mBACP,CAAC,WAAoB;AACpB,eAAW,aAAa,MAAe;AAAA,EACzC;AACF,QAAM,MAAM,QAAQ,QAAQ,MAAM,KAAK,IAAI;AAE3C,MAAI,QAA+B,EAAE,MAAM,OAAO;AAClD,QAAM,YAAY,oBAAI,IAAgB;AACtC,MAAI,aAAsB;AAC1B,MAAI,UAAU;AAEd,WAAS,OAAa;AACpB,eAAW,YAAY,UAAW,UAAS;AAAA,EAC7C;AAEA,WAAS,SAAS,MAAmC;AACnD,YAAQ;AACR,SAAK;AAAA,EACP;AAEA,WAAS,YAAkB;AACzB,QAAI,eAAe,MAAM;AACvB,qBAAe,UAAU;AACzB,mBAAa;AAAA,IACf;AAAA,EACF;AAEA,WAAS,iBAA0B;AACjC,WACE,MAAM,SAAS,cACf,MAAM,SAAS,uBACf,MAAM,SAAS;AAAA,EAEnB;AAEA,iBAAe,cAAc,SAAuC;AAClE,aAAS,EAAE,MAAM,WAAW,QAAQ,CAAC;AACrC,QAAI;AACF,YAAM,SAAS,MAAM,WAAW,WAAW,QAAQ,SAAS;AAC5D,UAAI,CAAC,QAAS;AACd,eAAS,EAAE,MAAM,QAAQ,OAAO,CAAC;AAAA,IACnC,SAAS,KAAK;AACZ,UAAI,CAAC,QAAS;AACd,eAAS,EAAE,MAAM,SAAS,OAAO,QAAQ,GAAG,EAAE,CAAC;AAAA,IACjD,UAAE;AACA,gBAAU;AAAA,IACZ;AAAA,EACF;AAEA,WAAS,iBAAiB,SAAwB,UAAwB;AACxE,iBAAa,aAAa,MAAM;AAC9B,WAAK,KAAK,SAAS,QAAQ;AAAA,IAC7B,GAAG,cAAc;AAAA,EACnB;AAEA,iBAAe,KAAK,SAAwB,UAAiC;AAC3E,QAAI,CAAC,QAAS;AACd,QAAI,IAAI,KAAK,UAAU;AACrB,gBAAU;AACV,eAAS;AAAA,QACP,MAAM;AAAA,QACN,OAAO,IAAI,MAAM,gCAAgC;AAAA,MACnD,CAAC;AACD;AAAA,IACF;AACA,QAAI;AACJ,QAAI;AACF,eAAS,MAAM,WAAW,UAAU,QAAQ,SAAS;AAAA,IACvD,SAAS,KAAK;AACZ,UAAI,CAAC,QAAS;AACd,gBAAU;AACV,eAAS,EAAE,MAAM,SAAS,OAAO,QAAQ,GAAG,EAAE,CAAC;AAC/C;AAAA,IACF;AACA,QAAI,CAAC,QAAS;AAEd,QAAI,OAAO,WAAW,YAAY;AAChC,gBAAU;AACV,YAAM,cAAc,OAAO;AAC3B;AAAA,IACF;AACA,QAAI,OAAO,WAAW,YAAY,OAAO,WAAW,WAAW;AAC7D,gBAAU;AACV,eAAS;AAAA,QACP,MAAM;AAAA,QACN,OAAO,IAAI,MAAM,kBAAkB,OAAO,MAAM,EAAE;AAAA,MACpD,CAAC;AACD;AAAA,IACF;AACA,qBAAiB,SAAS,QAAQ;AAAA,EACpC;AAEA,SAAO;AAAA,IACL,WAAW;AACT,aAAO;AAAA,IACT;AAAA,IAEA,UAAU,UAAsB;AAC9B,gBAAU,IAAI,QAAQ;AACtB,aAAO,MAAM,UAAU,OAAO,QAAQ;AAAA,IACxC;AAAA,IAEA,MAAM,QAAuB;AAC3B,UAAI,WAAW,eAAe,EAAG;AACjC,gBAAU;AACV,eAAS,EAAE,MAAM,WAAW,CAAC;AAE7B,UAAI;AACJ,UAAI;AACF,kBAAU,MAAM,WAAW,cAAc;AAAA,MAC3C,SAAS,KAAK;AACZ,kBAAU;AACV,iBAAS,EAAE,MAAM,SAAS,OAAO,QAAQ,GAAG,EAAE,CAAC;AAC/C;AAAA,MACF;AACA,UAAI,CAAC,QAAS;AAEd,eAAS,EAAE,MAAM,qBAAqB,QAAQ,CAAC;AAC/C,iBAAW,QAAQ,WAAW;AAE9B,YAAM,WAAW,IAAI,IAAI;AACzB,uBAAiB,SAAS,QAAQ;AAAA,IACpC;AAAA,IAEA,QAAc;AACZ,gBAAU;AACV,gBAAU;AACV,eAAS,EAAE,MAAM,OAAO,CAAC;AAAA,IAC3B;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Framework-agnostic connect-flow state machine for the browser two-tab helper.
3
+ *
4
+ * @remarks
5
+ * This is the testable core behind {@link useDirectVanaConnect}. It is pure
6
+ * TypeScript (no React, no DOM-only APIs beyond an injectable window opener and
7
+ * timers) so the full flow — create request, open Vana, poll status, read data —
8
+ * can be exercised in a Node test environment.
9
+ *
10
+ * The React hook is a thin `useSyncExternalStore` binding over this store.
11
+ *
12
+ * @category Direct
13
+ * @module direct/connect-flow
14
+ */
15
+ import type { AccessRequest, AccessRequestStatus, ApprovedDataResult } from "./types";
16
+ /**
17
+ * Caller-supplied transports. These typically `fetch` the app's own backend
18
+ * routes, which in turn delegate to a {@link DirectDataController}.
19
+ */
20
+ export interface DirectConnectTransports<T = unknown> {
21
+ /** Ask the backend to create an access request. */
22
+ createRequest: () => Promise<AccessRequest>;
23
+ /** Ask the backend for the current status of a request. */
24
+ getStatus: (requestId: string) => Promise<AccessRequestStatus>;
25
+ /** Ask the backend to read the approved data. */
26
+ readResult: (requestId: string) => Promise<ApprovedDataResult<T>>;
27
+ }
28
+ /** Tunables for the connect flow. */
29
+ export interface DirectConnectOptions {
30
+ /** Status poll interval in ms. Defaults to 1500. */
31
+ pollIntervalMs?: number;
32
+ /** Overall timeout in ms before giving up. Defaults to 300000 (5 min). */
33
+ timeoutMs?: number;
34
+ /** Opens the approval URL. Defaults to `window.open`. Injectable for tests. */
35
+ openWindow?: (url: string) => void;
36
+ /** `setTimeout`. Injectable for tests. Defaults to `globalThis.setTimeout`. */
37
+ setTimeoutFn?: (cb: () => void, ms: number) => unknown;
38
+ /** `clearTimeout`. Injectable for tests. Defaults to `globalThis.clearTimeout`. */
39
+ clearTimeoutFn?: (handle: unknown) => void;
40
+ /** Clock source in ms. Injectable for tests. Defaults to `Date.now`. */
41
+ now?: () => number;
42
+ }
43
+ /**
44
+ * Discriminated connect-flow state.
45
+ *
46
+ * @remarks
47
+ * `type` matches the builder guide: it starts at `"idle"` and is non-idle while
48
+ * connecting. The intermediate phases give richer UIs something to render.
49
+ */
50
+ export type DirectConnectState<T = unknown> = {
51
+ type: "idle";
52
+ } | {
53
+ type: "creating";
54
+ } | {
55
+ type: "awaiting_approval";
56
+ request: AccessRequest;
57
+ } | {
58
+ type: "reading";
59
+ request: AccessRequest;
60
+ } | {
61
+ type: "done";
62
+ result: ApprovedDataResult<T>;
63
+ } | {
64
+ type: "error";
65
+ error: Error;
66
+ };
67
+ /** The store returned by {@link createDirectConnectFlow}. */
68
+ export interface DirectConnectFlow<T = unknown> {
69
+ /** Current state. */
70
+ getState(): DirectConnectState<T>;
71
+ /** Subscribe to state changes; returns an unsubscribe function. */
72
+ subscribe(listener: () => void): () => void;
73
+ /** Begin the flow. No-op if already running. */
74
+ start(): Promise<void>;
75
+ /** Reset to `idle` and stop any in-flight polling. */
76
+ reset(): void;
77
+ }
78
+ /**
79
+ * Create a connect-flow store.
80
+ *
81
+ * @param transports - Backend transports (`createRequest`, `getStatus`, `readResult`).
82
+ * @param options - Polling/timeout tunables and injectable side effects.
83
+ * @returns A {@link DirectConnectFlow} store.
84
+ */
85
+ export declare function createDirectConnectFlow<T = unknown>(transports: DirectConnectTransports<T>, options?: DirectConnectOptions): DirectConnectFlow<T>;