@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.
- package/README.md +116 -0
- package/dist/direct/access-request-client.cjs +104 -0
- package/dist/direct/access-request-client.cjs.map +1 -0
- package/dist/direct/access-request-client.d.ts +51 -0
- package/dist/direct/access-request-client.js +79 -0
- package/dist/direct/access-request-client.js.map +1 -0
- package/dist/direct/access-request-client.test.d.ts +1 -0
- package/dist/direct/connect-flow.cjs +152 -0
- package/dist/direct/connect-flow.cjs.map +1 -0
- package/dist/direct/connect-flow.d.ts +85 -0
- package/dist/direct/connect-flow.js +128 -0
- package/dist/direct/connect-flow.js.map +1 -0
- package/dist/direct/connect-flow.test.d.ts +1 -0
- package/dist/direct/controller.cjs +129 -0
- package/dist/direct/controller.cjs.map +1 -0
- package/dist/direct/controller.d.ts +152 -0
- package/dist/direct/controller.js +109 -0
- package/dist/direct/controller.js.map +1 -0
- package/dist/direct/controller.test.d.ts +1 -0
- package/dist/direct/endpoints.cjs +45 -0
- package/dist/direct/endpoints.cjs.map +1 -0
- package/dist/direct/endpoints.d.ts +22 -0
- package/dist/direct/endpoints.js +19 -0
- package/dist/direct/endpoints.js.map +1 -0
- package/dist/direct/errors.cjs +65 -0
- package/dist/direct/errors.cjs.map +1 -0
- package/dist/direct/errors.d.ts +44 -0
- package/dist/direct/errors.js +38 -0
- package/dist/direct/errors.js.map +1 -0
- package/dist/direct/escrow-payment.cjs +96 -0
- package/dist/direct/escrow-payment.cjs.map +1 -0
- package/dist/direct/escrow-payment.d.ts +81 -0
- package/dist/direct/escrow-payment.js +72 -0
- package/dist/direct/escrow-payment.js.map +1 -0
- package/dist/direct/escrow-payment.test.d.ts +1 -0
- package/dist/direct/personal-server-read.cjs +149 -0
- package/dist/direct/personal-server-read.cjs.map +1 -0
- package/dist/direct/personal-server-read.d.ts +103 -0
- package/dist/direct/personal-server-read.js +124 -0
- package/dist/direct/personal-server-read.js.map +1 -0
- package/dist/direct/personal-server-read.test.d.ts +1 -0
- package/dist/direct/types.cjs +35 -0
- package/dist/direct/types.cjs.map +1 -0
- package/dist/direct/types.d.ts +205 -0
- package/dist/direct/types.js +11 -0
- package/dist/direct/types.js.map +1 -0
- package/dist/direct/use-direct-vana-connect.cjs +68 -0
- package/dist/direct/use-direct-vana-connect.cjs.map +1 -0
- package/dist/direct/use-direct-vana-connect.d.ts +45 -0
- package/dist/direct/use-direct-vana-connect.js +46 -0
- package/dist/direct/use-direct-vana-connect.js.map +1 -0
- package/dist/index.browser.d.ts +7 -3
- package/dist/index.browser.js +513 -174
- package/dist/index.browser.js.map +4 -4
- package/dist/index.node.cjs +536 -179
- package/dist/index.node.cjs.map +4 -4
- package/dist/index.node.d.ts +7 -3
- package/dist/index.node.js +513 -174
- package/dist/index.node.js.map +4 -4
- package/dist/protocol/data-point-status.cjs +80 -0
- package/dist/protocol/data-point-status.cjs.map +1 -0
- package/dist/protocol/data-point-status.d.ts +34 -0
- package/dist/protocol/data-point-status.js +51 -0
- package/dist/protocol/data-point-status.js.map +1 -0
- package/dist/protocol/data-point-status.test.d.ts +1 -0
- package/dist/protocol/eip712.cjs +53 -31
- package/dist/protocol/eip712.cjs.map +1 -1
- package/dist/protocol/eip712.d.ts +98 -43
- package/dist/protocol/eip712.js +47 -27
- package/dist/protocol/eip712.js.map +1 -1
- package/dist/protocol/escrow-deposit.cjs +89 -0
- package/dist/protocol/escrow-deposit.cjs.map +1 -0
- package/dist/protocol/escrow-deposit.d.ts +47 -0
- package/dist/protocol/escrow-deposit.js +60 -0
- package/dist/protocol/escrow-deposit.js.map +1 -0
- package/dist/protocol/escrow-deposit.test.d.ts +1 -0
- package/dist/protocol/escrow-flow.test.d.ts +21 -0
- package/dist/protocol/fee-registry.cjs +116 -0
- package/dist/protocol/fee-registry.cjs.map +1 -0
- package/dist/protocol/fee-registry.d.ts +151 -0
- package/dist/protocol/fee-registry.js +89 -0
- package/dist/protocol/fee-registry.js.map +1 -0
- package/dist/protocol/fee-registry.test.d.ts +1 -0
- package/dist/protocol/gateway.cjs +107 -37
- package/dist/protocol/gateway.cjs.map +1 -1
- package/dist/protocol/gateway.d.ts +223 -57
- package/dist/protocol/gateway.js +107 -37
- package/dist/protocol/gateway.js.map +1 -1
- package/dist/protocol/grants.cjs +27 -64
- package/dist/protocol/grants.cjs.map +1 -1
- package/dist/protocol/grants.d.ts +6 -13
- package/dist/protocol/grants.js +27 -63
- package/dist/protocol/grants.js.map +1 -1
- package/dist/protocol/personal-server-data.cjs +71 -0
- package/dist/protocol/personal-server-data.cjs.map +1 -0
- package/dist/protocol/personal-server-data.d.ts +16 -0
- package/dist/protocol/personal-server-data.js +47 -0
- package/dist/protocol/personal-server-data.js.map +1 -0
- package/dist/protocol/personal-server-data.test.d.ts +1 -0
- package/dist/protocol/personal-server-lite-owner-binding.cjs +93 -0
- package/dist/protocol/personal-server-lite-owner-binding.cjs.map +1 -0
- package/dist/protocol/personal-server-lite-owner-binding.d.ts +44 -0
- package/dist/protocol/personal-server-lite-owner-binding.js +65 -0
- package/dist/protocol/personal-server-lite-owner-binding.js.map +1 -0
- package/dist/protocol/personal-server-lite-owner-binding.test.d.ts +1 -0
- package/dist/react.cjs +32 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.ts +33 -0
- package/dist/react.js +11 -0
- package/dist/react.js.map +1 -0
- package/dist/server.cjs +73 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.ts +32 -0
- package/dist/server.js +55 -0
- package/dist/server.js.map +1 -0
- package/dist/storage/providers/vana-storage.cjs +75 -17
- package/dist/storage/providers/vana-storage.cjs.map +1 -1
- package/dist/storage/providers/vana-storage.js +75 -17
- package/dist/storage/providers/vana-storage.js.map +1 -1
- 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":"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","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>;
|