@opendatalabs/vana-sdk 3.19.0 → 3.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -15
- package/dist/auth/web3-signed-builder.cjs +3 -0
- package/dist/auth/web3-signed-builder.cjs.map +1 -1
- package/dist/auth/web3-signed-builder.d.ts +14 -0
- package/dist/auth/web3-signed-builder.js +3 -0
- package/dist/auth/web3-signed-builder.js.map +1 -1
- package/dist/direct/controller.cjs +3 -2
- package/dist/direct/controller.cjs.map +1 -1
- package/dist/direct/controller.d.ts +19 -2
- package/dist/direct/controller.js +3 -2
- package/dist/direct/controller.js.map +1 -1
- package/dist/errors.cjs +13 -0
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.d.ts +21 -2
- package/dist/errors.js +12 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.browser.js +69 -25
- package/dist/index.browser.js.map +2 -2
- package/dist/index.node.cjs +70 -25
- package/dist/index.node.cjs.map +2 -2
- package/dist/index.node.js +69 -25
- package/dist/index.node.js.map +2 -2
- package/dist/protocol/derivative-questions.cjs +42 -25
- package/dist/protocol/derivative-questions.cjs.map +1 -1
- package/dist/protocol/derivative-questions.d.ts +51 -10
- package/dist/protocol/derivative-questions.js +47 -26
- package/dist/protocol/derivative-questions.js.map +1 -1
- package/dist/protocol/write-request.cjs +14 -0
- package/dist/protocol/write-request.cjs.map +1 -1
- package/dist/protocol/write-request.d.ts +12 -0
- package/dist/protocol/write-request.js +13 -0
- package/dist/protocol/write-request.js.map +1 -1
- package/dist/server.cjs +28 -2
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.js +29 -1
- package/dist/server.js.map +1 -1
- package/dist/tests/mock-personal-server.d.ts +9 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -155,6 +155,55 @@ Use `network: "moksha"` to keep production app/API URLs while running escrow and
|
|
|
155
155
|
chain-aware defaults against Moksha. `env: "dev"` remains for Vana's internal dev
|
|
156
156
|
deployment and switches deployment URLs.
|
|
157
157
|
|
|
158
|
+
### Scope entries
|
|
159
|
+
|
|
160
|
+
A grant carries a list of **scope entries**, and each one is
|
|
161
|
+
`[operation:]scope`:
|
|
162
|
+
|
|
163
|
+
- no prefix means **read** — `spotify.savedTracks`, `chatgpt.*`, `*`;
|
|
164
|
+
- `write:` means **write** — `write:coach.weekly`, `write:chatgpt.*`;
|
|
165
|
+
- the operation is lowercase ASCII and matched exactly. `read:` is not an
|
|
166
|
+
alias for a bare entry, `delete:` is reserved but not implemented, and there
|
|
167
|
+
is no wildcard over operations: wildcards apply to the scope part only.
|
|
168
|
+
|
|
169
|
+
Read and write never cross. `write:coach.weekly` authorizes writing
|
|
170
|
+
`coach.weekly` and nothing else; reading it needs its own bare entry. A grant
|
|
171
|
+
that wants both carries both.
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
import {
|
|
175
|
+
parseScopeEntry, // "write:coach.weekly" -> { scope: "coach.weekly", action: "write" }
|
|
176
|
+
formatScopeEntry, // { scope, action } -> the wire entry
|
|
177
|
+
grantPermissions, // a grant's scopes -> [{ scope, actions: ["read", "write"] }]
|
|
178
|
+
hasAction, // does this grant authorize `action` over `scope`?
|
|
179
|
+
} from "@opendatalabs/vana-sdk/server";
|
|
180
|
+
|
|
181
|
+
// Request read + write on a derived scope, computed from two sources.
|
|
182
|
+
export const vana = createDirectDataController({
|
|
183
|
+
// ...
|
|
184
|
+
source: "oura",
|
|
185
|
+
scopes: [
|
|
186
|
+
"oura.sleep",
|
|
187
|
+
"chatgpt.conversations",
|
|
188
|
+
"coach.weekly",
|
|
189
|
+
"write:coach.weekly",
|
|
190
|
+
],
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
hasAction(grant.scopes, "coach.weekly", "write"); // true
|
|
194
|
+
hasAction(grant.scopes, "oura.sleep", "write"); // false — read entry only
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Entries are passed through and signed verbatim, so build and read them with
|
|
198
|
+
these helpers rather than slicing the strings by hand. An entry whose
|
|
199
|
+
operation the SDK does not recognise never parses as read: `parseScopeEntry`
|
|
200
|
+
throws `InvalidScopeEntryError`, `hasAction` skips it, and `grantPermissions`
|
|
201
|
+
refuses the whole list (use `tryGrantPermissions` to get `undefined` instead
|
|
202
|
+
when rendering a grant a newer release may have written). The controller
|
|
203
|
+
accepts write entries in `scopes`, but its scope part must be a concrete
|
|
204
|
+
scope: this flow reads approved scopes back one at a time, so wildcards are
|
|
205
|
+
rejected there for read and write alike.
|
|
206
|
+
|
|
158
207
|
### Backend controller
|
|
159
208
|
|
|
160
209
|
```typescript
|
|
@@ -520,14 +569,27 @@ What the SDK does for you:
|
|
|
520
569
|
(`POST /v1/write/session`) and reuses it for every question call, including
|
|
521
570
|
each poll of `waitForQuestion`.
|
|
522
571
|
- Signs a fresh, single-use `X-Vana-Write-Signature` proof for every request
|
|
523
|
-
(the grant id is a signed claim).
|
|
524
|
-
|
|
572
|
+
(the grant id is a signed claim).
|
|
573
|
+
- Puts a fresh `nonce` claim on every proof. **Polling needs it**: a proof
|
|
574
|
+
payload is otherwise fully determined by
|
|
575
|
+
`{aud, method, uri, bodyHash, grantId, iat, exp}`, so two identical
|
|
576
|
+
`GET /questions/:id` polls signed inside the same second are byte-identical
|
|
577
|
+
and the Personal Server refuses the second as a replay
|
|
578
|
+
(`WRITE_ATTRIBUTION_REPLAY`). With a nonce the replay key is
|
|
579
|
+
`(builder, nonce)` instead, and each poll is distinct. Every helper here
|
|
580
|
+
does it for you, `waitForQuestion` included; a hand-built question request
|
|
581
|
+
must pass `nonce` to `buildWeb3SignedHeader` itself.
|
|
582
|
+
- Signs the whole request **target**, query string included, because
|
|
583
|
+
`?derivedScope=` is what the list route authorizes against. The target is
|
|
584
|
+
built once and used for both the signed `uri` claim and the URL, so the
|
|
585
|
+
signature and the request can never name different scopes.
|
|
525
586
|
- Re-opens the session once and replays the call when the Personal Server
|
|
526
|
-
answers 401: it keeps sessions in
|
|
587
|
+
answers a 401 the **session** is responsible for: it keeps sessions in
|
|
588
|
+
memory and forgets them when it restarts. A 401 about the **proof** (it
|
|
589
|
+
does not cover this request, its nonce is spent, it recovers to another
|
|
590
|
+
key) is surfaced as it is, since a new session would not change it.
|
|
527
591
|
- Sends bodies as compact JSON, which the server requires
|
|
528
|
-
(`WRITE_BODY_NOT_CANONICAL` otherwise)
|
|
529
|
-
`?derivedScope=` of a list call is outside the proof, as the server verifies
|
|
530
|
-
it.
|
|
592
|
+
(`WRITE_BODY_NOT_CANONICAL` otherwise).
|
|
531
593
|
- Validates the registration (scope list, question length, model id, the
|
|
532
594
|
naming rule) before anything is signed.
|
|
533
595
|
|
|
@@ -544,15 +606,20 @@ Errors are typed and carry the server's `status`, `errorCode` and `details`:
|
|
|
544
606
|
sources), `DerivativeCycleError` (409, the question would make the derived
|
|
545
607
|
scope a transitive source of itself), `DerivativeQuestionNotFoundError` (404,
|
|
546
608
|
including another builder's question on the same scope),
|
|
547
|
-
`DerivativeQuestionInvalidError` (400), `
|
|
548
|
-
(
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
`
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
609
|
+
`DerivativeQuestionInvalidError` (400), `DerivativeDerivedScopeRequiredError`
|
|
610
|
+
(400 `DERIVATIVE_DERIVED_SCOPE_REQUIRED`, a builder list with no
|
|
611
|
+
`?derivedScope=`; the SDK refuses an empty one before signing),
|
|
612
|
+
`DerivativeComputeUnavailableError` (503, no compute layer on that server),
|
|
613
|
+
`DerivativeQuestionTimeoutError`, `DerivativeQuestionFailedError`, and
|
|
614
|
+
`DerivativeQuestionRejectedError` for anything else. Authentication failures
|
|
615
|
+
are the Write API's own `WriteUnauthorizedError`, `WriteForbiddenError`,
|
|
616
|
+
`WriteRequestError` (refused before sending) and `WriteTransportError`. An
|
|
617
|
+
**unknown** question id is a `DerivativeQuestionNotFoundError` (404), the
|
|
618
|
+
same as another builder's question.
|
|
619
|
+
|
|
620
|
+
These helpers require `personal-server-ts` main `d91124d` or later, which is
|
|
621
|
+
where the query-in-the-signed-uri rule, the `nonce` claim, the 404 for an
|
|
622
|
+
unknown id and the full-view `recompute` answer landed.
|
|
556
623
|
|
|
557
624
|
## Networks
|
|
558
625
|
|
|
@@ -52,6 +52,9 @@ async function buildWeb3SignedHeader(params) {
|
|
|
52
52
|
if (params.grantId !== void 0) {
|
|
53
53
|
payload["grantId"] = params.grantId;
|
|
54
54
|
}
|
|
55
|
+
if (params.nonce !== void 0) {
|
|
56
|
+
payload["nonce"] = params.nonce;
|
|
57
|
+
}
|
|
55
58
|
const sortedPayload = Object.keys(payload).sort().reduce((acc, key) => {
|
|
56
59
|
acc[key] = payload[key];
|
|
57
60
|
return acc;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/auth/web3-signed-builder.ts"],"sourcesContent":["/**\n * Builder for Web3Signed Authorization headers.\n *\n * @remarks\n * Ported from `personal-server-ts`\n * (`packages/core/src/signing/request-signer.ts`). The original was wired\n * to a Node-only `ServerAccount` and `node:crypto`. This isomorphic version\n * accepts any `signMessage` callback (viem accounts, wallet clients, etc.)\n * and uses `@noble/hashes` for SHA-256 so it runs in browsers and Workers.\n *\n * Wire format is identical to PS — payload is JSON with sorted keys,\n * base64url-encoded, signed via EIP-191.\n *\n * @category Auth\n */\n\nimport { sha256 } from \"@noble/hashes/sha2\";\nimport { bytesToHex } from \"viem\";\nimport { toBase64 } from \"../utils/encoding\";\n\n/**\n * Sign-message callback compatible with viem `LocalAccount`/`WalletClient`-style\n * signers. Must produce an EIP-191 (`personal_sign`) signature.\n */\nexport type Web3SignedSignFn = (message: string) => Promise<`0x${string}`>;\n\n/** SHA-256 of the empty string — bodyHash for empty bodies. */\nconst EMPTY_BODY_HASH =\n \"sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\";\n\n/** Default token lifetime (seconds). */\nconst DEFAULT_TTL_SECONDS = 300;\n\n/** Base64url encode bytes (no padding). */\nfunction base64urlEncode(input: Uint8Array): string {\n return toBase64(input)\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=+$/, \"\");\n}\n\n/** Compute the `sha256:<hex>` bodyHash claim for a request body. */\nexport function computeBodyHash(body: Uint8Array | undefined): string {\n if (!body || body.length === 0) {\n return EMPTY_BODY_HASH;\n }\n const digest = sha256(body);\n return `sha256:${bytesToHex(digest).slice(2)}`;\n}\n\n/**\n * Build a Web3Signed Authorization header value.\n *\n * @returns The full header value (`\"Web3Signed <base64url>.<sig>\"`).\n */\nexport async function buildWeb3SignedHeader(params: {\n /** EIP-191 signer (e.g. viem `account.signMessage`). */\n signMessage: Web3SignedSignFn;\n /** Expected origin (e.g. `\"https://ps.example.com\"`). */\n aud: string;\n /** HTTP method (e.g. `\"GET\"`). */\n method: string;\n /** Request URI/path (e.g. `\"/v1/data/instagram.profile\"`). */\n uri: string;\n /** Optional request body — when present, used to compute `bodyHash`. */\n body?: Uint8Array;\n /** Issued-at (unix seconds). Defaults to now. */\n iat?: number;\n /** Expiry (unix seconds). Defaults to `iat + 300`. */\n exp?: number;\n /** Optional grant id, attached as the `grantId` claim. */\n grantId?: string;\n /** Pre-computed `bodyHash` claim — overrides `body`. */\n bodyHash?: string;\n}): Promise<string> {\n const now = Math.floor(Date.now() / 1000);\n const iat = params.iat ?? now;\n const exp = params.exp ?? iat + DEFAULT_TTL_SECONDS;\n\n const payload: Record<string, unknown> = {\n aud: params.aud,\n bodyHash: params.bodyHash ?? computeBodyHash(params.body),\n exp,\n iat,\n method: params.method,\n uri: params.uri,\n };\n\n if (params.grantId !== undefined) {\n payload[\"grantId\"] = params.grantId;\n }\n\n // Sort keys for deterministic serialization.\n const sortedPayload = Object.keys(payload)\n .sort()\n .reduce<Record<string, unknown>>((acc, key) => {\n acc[key] = payload[key];\n return acc;\n }, {});\n\n const payloadJson = JSON.stringify(sortedPayload);\n const payloadBytes = new TextEncoder().encode(payloadJson);\n const payloadBase64 = base64urlEncode(payloadBytes);\n\n const signature = await params.signMessage(payloadBase64);\n\n return `Web3Signed ${payloadBase64}.${signature}`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgBA,kBAAuB;AACvB,kBAA2B;AAC3B,sBAAyB;AASzB,MAAM,kBACJ;AAGF,MAAM,sBAAsB;AAG5B,SAAS,gBAAgB,OAA2B;AAClD,aAAO,0BAAS,KAAK,EAClB,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,EAAE;AACtB;AAGO,SAAS,gBAAgB,MAAsC;AACpE,MAAI,CAAC,QAAQ,KAAK,WAAW,GAAG;AAC9B,WAAO;AAAA,EACT;AACA,QAAM,aAAS,oBAAO,IAAI;AAC1B,SAAO,cAAU,wBAAW,MAAM,EAAE,MAAM,CAAC,CAAC;AAC9C;AAOA,eAAsB,sBAAsB,
|
|
1
|
+
{"version":3,"sources":["../../src/auth/web3-signed-builder.ts"],"sourcesContent":["/**\n * Builder for Web3Signed Authorization headers.\n *\n * @remarks\n * Ported from `personal-server-ts`\n * (`packages/core/src/signing/request-signer.ts`). The original was wired\n * to a Node-only `ServerAccount` and `node:crypto`. This isomorphic version\n * accepts any `signMessage` callback (viem accounts, wallet clients, etc.)\n * and uses `@noble/hashes` for SHA-256 so it runs in browsers and Workers.\n *\n * Wire format is identical to PS — payload is JSON with sorted keys,\n * base64url-encoded, signed via EIP-191.\n *\n * @category Auth\n */\n\nimport { sha256 } from \"@noble/hashes/sha2\";\nimport { bytesToHex } from \"viem\";\nimport { toBase64 } from \"../utils/encoding\";\n\n/**\n * Sign-message callback compatible with viem `LocalAccount`/`WalletClient`-style\n * signers. Must produce an EIP-191 (`personal_sign`) signature.\n */\nexport type Web3SignedSignFn = (message: string) => Promise<`0x${string}`>;\n\n/** SHA-256 of the empty string — bodyHash for empty bodies. */\nconst EMPTY_BODY_HASH =\n \"sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\";\n\n/** Default token lifetime (seconds). */\nconst DEFAULT_TTL_SECONDS = 300;\n\n/** Base64url encode bytes (no padding). */\nfunction base64urlEncode(input: Uint8Array): string {\n return toBase64(input)\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=+$/, \"\");\n}\n\n/** Compute the `sha256:<hex>` bodyHash claim for a request body. */\nexport function computeBodyHash(body: Uint8Array | undefined): string {\n if (!body || body.length === 0) {\n return EMPTY_BODY_HASH;\n }\n const digest = sha256(body);\n return `sha256:${bytesToHex(digest).slice(2)}`;\n}\n\n/**\n * Build a Web3Signed Authorization header value.\n *\n * @returns The full header value (`\"Web3Signed <base64url>.<sig>\"`).\n */\nexport async function buildWeb3SignedHeader(params: {\n /** EIP-191 signer (e.g. viem `account.signMessage`). */\n signMessage: Web3SignedSignFn;\n /** Expected origin (e.g. `\"https://ps.example.com\"`). */\n aud: string;\n /** HTTP method (e.g. `\"GET\"`). */\n method: string;\n /** Request URI/path (e.g. `\"/v1/data/instagram.profile\"`). */\n uri: string;\n /** Optional request body — when present, used to compute `bodyHash`. */\n body?: Uint8Array;\n /** Issued-at (unix seconds). Defaults to now. */\n iat?: number;\n /** Expiry (unix seconds). Defaults to `iat + 300`. */\n exp?: number;\n /** Optional grant id, attached as the `grantId` claim. */\n grantId?: string;\n /**\n * Optional uniqueness claim, attached as the `nonce` claim (a uuid is the\n * intended shape; the Personal Server bounds it at 128 characters).\n *\n * @remarks\n * A payload is otherwise fully determined by\n * `{ aud, bodyHash, exp, grantId, iat, method, uri }`, so two identical\n * requests signed inside the same second produce the same bytes and the\n * Personal Server refuses the second as a replay. With a nonce the replay\n * key becomes `(signer, nonce)` instead, which is what lets a poll loop\n * send the same request twice. The nonce is then single use itself:\n * re-using one is a replay even when the rest of the payload changed.\n */\n nonce?: string;\n /** Pre-computed `bodyHash` claim — overrides `body`. */\n bodyHash?: string;\n}): Promise<string> {\n const now = Math.floor(Date.now() / 1000);\n const iat = params.iat ?? now;\n const exp = params.exp ?? iat + DEFAULT_TTL_SECONDS;\n\n const payload: Record<string, unknown> = {\n aud: params.aud,\n bodyHash: params.bodyHash ?? computeBodyHash(params.body),\n exp,\n iat,\n method: params.method,\n uri: params.uri,\n };\n\n if (params.grantId !== undefined) {\n payload[\"grantId\"] = params.grantId;\n }\n\n // Merged before the sort, so the nonce sits in its alphabetical place like\n // every other claim and the payload stays deterministic.\n if (params.nonce !== undefined) {\n payload[\"nonce\"] = params.nonce;\n }\n\n // Sort keys for deterministic serialization.\n const sortedPayload = Object.keys(payload)\n .sort()\n .reduce<Record<string, unknown>>((acc, key) => {\n acc[key] = payload[key];\n return acc;\n }, {});\n\n const payloadJson = JSON.stringify(sortedPayload);\n const payloadBytes = new TextEncoder().encode(payloadJson);\n const payloadBase64 = base64urlEncode(payloadBytes);\n\n const signature = await params.signMessage(payloadBase64);\n\n return `Web3Signed ${payloadBase64}.${signature}`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgBA,kBAAuB;AACvB,kBAA2B;AAC3B,sBAAyB;AASzB,MAAM,kBACJ;AAGF,MAAM,sBAAsB;AAG5B,SAAS,gBAAgB,OAA2B;AAClD,aAAO,0BAAS,KAAK,EAClB,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,EAAE;AACtB;AAGO,SAAS,gBAAgB,MAAsC;AACpE,MAAI,CAAC,QAAQ,KAAK,WAAW,GAAG;AAC9B,WAAO;AAAA,EACT;AACA,QAAM,aAAS,oBAAO,IAAI;AAC1B,SAAO,cAAU,wBAAW,MAAM,EAAE,MAAM,CAAC,CAAC;AAC9C;AAOA,eAAsB,sBAAsB,QAiCxB;AAClB,QAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACxC,QAAM,MAAM,OAAO,OAAO;AAC1B,QAAM,MAAM,OAAO,OAAO,MAAM;AAEhC,QAAM,UAAmC;AAAA,IACvC,KAAK,OAAO;AAAA,IACZ,UAAU,OAAO,YAAY,gBAAgB,OAAO,IAAI;AAAA,IACxD;AAAA,IACA;AAAA,IACA,QAAQ,OAAO;AAAA,IACf,KAAK,OAAO;AAAA,EACd;AAEA,MAAI,OAAO,YAAY,QAAW;AAChC,YAAQ,SAAS,IAAI,OAAO;AAAA,EAC9B;AAIA,MAAI,OAAO,UAAU,QAAW;AAC9B,YAAQ,OAAO,IAAI,OAAO;AAAA,EAC5B;AAGA,QAAM,gBAAgB,OAAO,KAAK,OAAO,EACtC,KAAK,EACL,OAAgC,CAAC,KAAK,QAAQ;AAC7C,QAAI,GAAG,IAAI,QAAQ,GAAG;AACtB,WAAO;AAAA,EACT,GAAG,CAAC,CAAC;AAEP,QAAM,cAAc,KAAK,UAAU,aAAa;AAChD,QAAM,eAAe,IAAI,YAAY,EAAE,OAAO,WAAW;AACzD,QAAM,gBAAgB,gBAAgB,YAAY;AAElD,QAAM,YAAY,MAAM,OAAO,YAAY,aAAa;AAExD,SAAO,cAAc,aAAa,IAAI,SAAS;AACjD;","names":[]}
|
|
@@ -42,6 +42,20 @@ export declare function buildWeb3SignedHeader(params: {
|
|
|
42
42
|
exp?: number;
|
|
43
43
|
/** Optional grant id, attached as the `grantId` claim. */
|
|
44
44
|
grantId?: string;
|
|
45
|
+
/**
|
|
46
|
+
* Optional uniqueness claim, attached as the `nonce` claim (a uuid is the
|
|
47
|
+
* intended shape; the Personal Server bounds it at 128 characters).
|
|
48
|
+
*
|
|
49
|
+
* @remarks
|
|
50
|
+
* A payload is otherwise fully determined by
|
|
51
|
+
* `{ aud, bodyHash, exp, grantId, iat, method, uri }`, so two identical
|
|
52
|
+
* requests signed inside the same second produce the same bytes and the
|
|
53
|
+
* Personal Server refuses the second as a replay. With a nonce the replay
|
|
54
|
+
* key becomes `(signer, nonce)` instead, which is what lets a poll loop
|
|
55
|
+
* send the same request twice. The nonce is then single use itself:
|
|
56
|
+
* re-using one is a replay even when the rest of the payload changed.
|
|
57
|
+
*/
|
|
58
|
+
nonce?: string;
|
|
45
59
|
/** Pre-computed `bodyHash` claim — overrides `body`. */
|
|
46
60
|
bodyHash?: string;
|
|
47
61
|
}): Promise<string>;
|
|
@@ -28,6 +28,9 @@ async function buildWeb3SignedHeader(params) {
|
|
|
28
28
|
if (params.grantId !== void 0) {
|
|
29
29
|
payload["grantId"] = params.grantId;
|
|
30
30
|
}
|
|
31
|
+
if (params.nonce !== void 0) {
|
|
32
|
+
payload["nonce"] = params.nonce;
|
|
33
|
+
}
|
|
31
34
|
const sortedPayload = Object.keys(payload).sort().reduce((acc, key) => {
|
|
32
35
|
acc[key] = payload[key];
|
|
33
36
|
return acc;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/auth/web3-signed-builder.ts"],"sourcesContent":["/**\n * Builder for Web3Signed Authorization headers.\n *\n * @remarks\n * Ported from `personal-server-ts`\n * (`packages/core/src/signing/request-signer.ts`). The original was wired\n * to a Node-only `ServerAccount` and `node:crypto`. This isomorphic version\n * accepts any `signMessage` callback (viem accounts, wallet clients, etc.)\n * and uses `@noble/hashes` for SHA-256 so it runs in browsers and Workers.\n *\n * Wire format is identical to PS — payload is JSON with sorted keys,\n * base64url-encoded, signed via EIP-191.\n *\n * @category Auth\n */\n\nimport { sha256 } from \"@noble/hashes/sha2\";\nimport { bytesToHex } from \"viem\";\nimport { toBase64 } from \"../utils/encoding\";\n\n/**\n * Sign-message callback compatible with viem `LocalAccount`/`WalletClient`-style\n * signers. Must produce an EIP-191 (`personal_sign`) signature.\n */\nexport type Web3SignedSignFn = (message: string) => Promise<`0x${string}`>;\n\n/** SHA-256 of the empty string — bodyHash for empty bodies. */\nconst EMPTY_BODY_HASH =\n \"sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\";\n\n/** Default token lifetime (seconds). */\nconst DEFAULT_TTL_SECONDS = 300;\n\n/** Base64url encode bytes (no padding). */\nfunction base64urlEncode(input: Uint8Array): string {\n return toBase64(input)\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=+$/, \"\");\n}\n\n/** Compute the `sha256:<hex>` bodyHash claim for a request body. */\nexport function computeBodyHash(body: Uint8Array | undefined): string {\n if (!body || body.length === 0) {\n return EMPTY_BODY_HASH;\n }\n const digest = sha256(body);\n return `sha256:${bytesToHex(digest).slice(2)}`;\n}\n\n/**\n * Build a Web3Signed Authorization header value.\n *\n * @returns The full header value (`\"Web3Signed <base64url>.<sig>\"`).\n */\nexport async function buildWeb3SignedHeader(params: {\n /** EIP-191 signer (e.g. viem `account.signMessage`). */\n signMessage: Web3SignedSignFn;\n /** Expected origin (e.g. `\"https://ps.example.com\"`). */\n aud: string;\n /** HTTP method (e.g. `\"GET\"`). */\n method: string;\n /** Request URI/path (e.g. `\"/v1/data/instagram.profile\"`). */\n uri: string;\n /** Optional request body — when present, used to compute `bodyHash`. */\n body?: Uint8Array;\n /** Issued-at (unix seconds). Defaults to now. */\n iat?: number;\n /** Expiry (unix seconds). Defaults to `iat + 300`. */\n exp?: number;\n /** Optional grant id, attached as the `grantId` claim. */\n grantId?: string;\n /** Pre-computed `bodyHash` claim — overrides `body`. */\n bodyHash?: string;\n}): Promise<string> {\n const now = Math.floor(Date.now() / 1000);\n const iat = params.iat ?? now;\n const exp = params.exp ?? iat + DEFAULT_TTL_SECONDS;\n\n const payload: Record<string, unknown> = {\n aud: params.aud,\n bodyHash: params.bodyHash ?? computeBodyHash(params.body),\n exp,\n iat,\n method: params.method,\n uri: params.uri,\n };\n\n if (params.grantId !== undefined) {\n payload[\"grantId\"] = params.grantId;\n }\n\n // Sort keys for deterministic serialization.\n const sortedPayload = Object.keys(payload)\n .sort()\n .reduce<Record<string, unknown>>((acc, key) => {\n acc[key] = payload[key];\n return acc;\n }, {});\n\n const payloadJson = JSON.stringify(sortedPayload);\n const payloadBytes = new TextEncoder().encode(payloadJson);\n const payloadBase64 = base64urlEncode(payloadBytes);\n\n const signature = await params.signMessage(payloadBase64);\n\n return `Web3Signed ${payloadBase64}.${signature}`;\n}\n"],"mappings":"AAgBA,SAAS,cAAc;AACvB,SAAS,kBAAkB;AAC3B,SAAS,gBAAgB;AASzB,MAAM,kBACJ;AAGF,MAAM,sBAAsB;AAG5B,SAAS,gBAAgB,OAA2B;AAClD,SAAO,SAAS,KAAK,EAClB,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,EAAE;AACtB;AAGO,SAAS,gBAAgB,MAAsC;AACpE,MAAI,CAAC,QAAQ,KAAK,WAAW,GAAG;AAC9B,WAAO;AAAA,EACT;AACA,QAAM,SAAS,OAAO,IAAI;AAC1B,SAAO,UAAU,WAAW,MAAM,EAAE,MAAM,CAAC,CAAC;AAC9C;AAOA,eAAsB,sBAAsB,
|
|
1
|
+
{"version":3,"sources":["../../src/auth/web3-signed-builder.ts"],"sourcesContent":["/**\n * Builder for Web3Signed Authorization headers.\n *\n * @remarks\n * Ported from `personal-server-ts`\n * (`packages/core/src/signing/request-signer.ts`). The original was wired\n * to a Node-only `ServerAccount` and `node:crypto`. This isomorphic version\n * accepts any `signMessage` callback (viem accounts, wallet clients, etc.)\n * and uses `@noble/hashes` for SHA-256 so it runs in browsers and Workers.\n *\n * Wire format is identical to PS — payload is JSON with sorted keys,\n * base64url-encoded, signed via EIP-191.\n *\n * @category Auth\n */\n\nimport { sha256 } from \"@noble/hashes/sha2\";\nimport { bytesToHex } from \"viem\";\nimport { toBase64 } from \"../utils/encoding\";\n\n/**\n * Sign-message callback compatible with viem `LocalAccount`/`WalletClient`-style\n * signers. Must produce an EIP-191 (`personal_sign`) signature.\n */\nexport type Web3SignedSignFn = (message: string) => Promise<`0x${string}`>;\n\n/** SHA-256 of the empty string — bodyHash for empty bodies. */\nconst EMPTY_BODY_HASH =\n \"sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\";\n\n/** Default token lifetime (seconds). */\nconst DEFAULT_TTL_SECONDS = 300;\n\n/** Base64url encode bytes (no padding). */\nfunction base64urlEncode(input: Uint8Array): string {\n return toBase64(input)\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=+$/, \"\");\n}\n\n/** Compute the `sha256:<hex>` bodyHash claim for a request body. */\nexport function computeBodyHash(body: Uint8Array | undefined): string {\n if (!body || body.length === 0) {\n return EMPTY_BODY_HASH;\n }\n const digest = sha256(body);\n return `sha256:${bytesToHex(digest).slice(2)}`;\n}\n\n/**\n * Build a Web3Signed Authorization header value.\n *\n * @returns The full header value (`\"Web3Signed <base64url>.<sig>\"`).\n */\nexport async function buildWeb3SignedHeader(params: {\n /** EIP-191 signer (e.g. viem `account.signMessage`). */\n signMessage: Web3SignedSignFn;\n /** Expected origin (e.g. `\"https://ps.example.com\"`). */\n aud: string;\n /** HTTP method (e.g. `\"GET\"`). */\n method: string;\n /** Request URI/path (e.g. `\"/v1/data/instagram.profile\"`). */\n uri: string;\n /** Optional request body — when present, used to compute `bodyHash`. */\n body?: Uint8Array;\n /** Issued-at (unix seconds). Defaults to now. */\n iat?: number;\n /** Expiry (unix seconds). Defaults to `iat + 300`. */\n exp?: number;\n /** Optional grant id, attached as the `grantId` claim. */\n grantId?: string;\n /**\n * Optional uniqueness claim, attached as the `nonce` claim (a uuid is the\n * intended shape; the Personal Server bounds it at 128 characters).\n *\n * @remarks\n * A payload is otherwise fully determined by\n * `{ aud, bodyHash, exp, grantId, iat, method, uri }`, so two identical\n * requests signed inside the same second produce the same bytes and the\n * Personal Server refuses the second as a replay. With a nonce the replay\n * key becomes `(signer, nonce)` instead, which is what lets a poll loop\n * send the same request twice. The nonce is then single use itself:\n * re-using one is a replay even when the rest of the payload changed.\n */\n nonce?: string;\n /** Pre-computed `bodyHash` claim — overrides `body`. */\n bodyHash?: string;\n}): Promise<string> {\n const now = Math.floor(Date.now() / 1000);\n const iat = params.iat ?? now;\n const exp = params.exp ?? iat + DEFAULT_TTL_SECONDS;\n\n const payload: Record<string, unknown> = {\n aud: params.aud,\n bodyHash: params.bodyHash ?? computeBodyHash(params.body),\n exp,\n iat,\n method: params.method,\n uri: params.uri,\n };\n\n if (params.grantId !== undefined) {\n payload[\"grantId\"] = params.grantId;\n }\n\n // Merged before the sort, so the nonce sits in its alphabetical place like\n // every other claim and the payload stays deterministic.\n if (params.nonce !== undefined) {\n payload[\"nonce\"] = params.nonce;\n }\n\n // Sort keys for deterministic serialization.\n const sortedPayload = Object.keys(payload)\n .sort()\n .reduce<Record<string, unknown>>((acc, key) => {\n acc[key] = payload[key];\n return acc;\n }, {});\n\n const payloadJson = JSON.stringify(sortedPayload);\n const payloadBytes = new TextEncoder().encode(payloadJson);\n const payloadBase64 = base64urlEncode(payloadBytes);\n\n const signature = await params.signMessage(payloadBase64);\n\n return `Web3Signed ${payloadBase64}.${signature}`;\n}\n"],"mappings":"AAgBA,SAAS,cAAc;AACvB,SAAS,kBAAkB;AAC3B,SAAS,gBAAgB;AASzB,MAAM,kBACJ;AAGF,MAAM,sBAAsB;AAG5B,SAAS,gBAAgB,OAA2B;AAClD,SAAO,SAAS,KAAK,EAClB,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,EAAE;AACtB;AAGO,SAAS,gBAAgB,MAAsC;AACpE,MAAI,CAAC,QAAQ,KAAK,WAAW,GAAG;AAC9B,WAAO;AAAA,EACT;AACA,QAAM,SAAS,OAAO,IAAI;AAC1B,SAAO,UAAU,WAAW,MAAM,EAAE,MAAM,CAAC,CAAC;AAC9C;AAOA,eAAsB,sBAAsB,QAiCxB;AAClB,QAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACxC,QAAM,MAAM,OAAO,OAAO;AAC1B,QAAM,MAAM,OAAO,OAAO,MAAM;AAEhC,QAAM,UAAmC;AAAA,IACvC,KAAK,OAAO;AAAA,IACZ,UAAU,OAAO,YAAY,gBAAgB,OAAO,IAAI;AAAA,IACxD;AAAA,IACA;AAAA,IACA,QAAQ,OAAO;AAAA,IACf,KAAK,OAAO;AAAA,EACd;AAEA,MAAI,OAAO,YAAY,QAAW;AAChC,YAAQ,SAAS,IAAI,OAAO;AAAA,EAC9B;AAIA,MAAI,OAAO,UAAU,QAAW;AAC9B,YAAQ,OAAO,IAAI,OAAO;AAAA,EAC5B;AAGA,QAAM,gBAAgB,OAAO,KAAK,OAAO,EACtC,KAAK,EACL,OAAgC,CAAC,KAAK,QAAQ;AAC7C,QAAI,GAAG,IAAI,QAAQ,GAAG;AACtB,WAAO;AAAA,EACT,GAAG,CAAC,CAAC;AAEP,QAAM,cAAc,KAAK,UAAU,aAAa;AAChD,QAAM,eAAe,IAAI,YAAY,EAAE,OAAO,WAAW;AACzD,QAAM,gBAAgB,gBAAgB,YAAY;AAElD,QAAM,YAAY,MAAM,OAAO,YAAY,aAAa;AAExD,SAAO,cAAc,aAAa,IAAI,SAAS;AACjD;","names":[]}
|
|
@@ -23,6 +23,7 @@ __export(controller_exports, {
|
|
|
23
23
|
module.exports = __toCommonJS(controller_exports);
|
|
24
24
|
var import_accounts = require("viem/accounts");
|
|
25
25
|
var import_scopes = require("../protocol/scopes");
|
|
26
|
+
var import_scope_actions = require("../protocol/scope-actions");
|
|
26
27
|
var import_escrow = require("../protocol/escrow");
|
|
27
28
|
var import_addresses = require("../generated/addresses");
|
|
28
29
|
var import_access_request_client = require("./access-request-client");
|
|
@@ -45,8 +46,8 @@ function createDirectDataController(config) {
|
|
|
45
46
|
if (!config.scopes || config.scopes.length === 0) {
|
|
46
47
|
throw new import_errors.DirectConfigError("At least one scope is required");
|
|
47
48
|
}
|
|
48
|
-
for (const
|
|
49
|
-
(0, import_scopes.parseScope)(scope);
|
|
49
|
+
for (const entry of config.scopes) {
|
|
50
|
+
(0, import_scopes.parseScope)((0, import_scope_actions.parseScopeEntry)(entry).scope);
|
|
50
51
|
}
|
|
51
52
|
const env = config.env ?? "production";
|
|
52
53
|
const network = config.network ?? (0, import_endpoints.getDirectDefaultNetwork)(env);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/direct/controller.ts"],"sourcesContent":["/**\n * Direct Data Controller — the server-side facade for the two-tab Data\n * Portability flow.\n *\n * @remarks\n * One controller owns an app's private key, source, scopes, app identity, and\n * payment flow. It exposes the three methods the builder guide documents:\n *\n * - {@link DirectDataController.createAccessRequest} — start an approval request.\n * - {@link DirectDataController.getAccessRequestStatus} — poll while the Vana tab is open.\n * - {@link DirectDataController.readApprovedData} — read from the Personal Server,\n * handling 402 Payment Required.\n *\n * Access requests are created through the Vana Account access-request API; the\n * Personal Server read uses Web3Signed auth; and payment uses the DPv2 escrow\n * surface (`protocol/escrow`) — when a read returns `402`, the controller signs\n * a `GenericPayment` with the app key, settles it through the escrow gateway,\n * and retries.\n *\n * @category Direct\n * @module direct/controller\n */\n\nimport { privateKeyToAccount } from \"viem/accounts\";\nimport type { Hex } from \"viem\";\nimport type { Web3SignedSignFn } from \"../auth/web3-signed-builder\";\nimport { parseScope } from \"../protocol/scopes\";\nimport { createEscrowGatewayClient } from \"../protocol/escrow\";\nimport { CONTRACTS } from \"../generated/addresses\";\nimport {\n createDefaultAccessRequestClient,\n type FetchLike,\n} from \"./access-request-client\";\nimport {\n getDirectDefaultNetwork,\n getDirectEndpoints,\n getDirectNetworkChainId,\n} from \"./endpoints\";\nimport {\n AccessNotApprovedError,\n DirectConfigError,\n ScopeNotApprovedError,\n} from \"./errors\";\nimport {\n type EscrowPaymentConfig,\n type SignTypedDataFn,\n} from \"./escrow-payment\";\nimport {\n readPersonalServerData,\n type PersonalServerFetch,\n type PersonalServerTransportRetryOptions,\n} from \"./personal-server-read\";\nimport type {\n AccessRequest,\n AccessRequestClient,\n AccessRequestStatus,\n AccessRequestStatusValue,\n ApprovedDataResult,\n AppIdentity,\n DirectAppConfig,\n DirectEnv,\n DirectNetwork,\n DirectPaymentResponseMetadata,\n DirectServiceEndpoints,\n ForegroundDelivery,\n MultiScopeDataResult,\n} from \"./types\";\n\n/** Configuration for {@link createDirectDataController}. */\nexport interface DirectDataControllerConfig {\n /** Target environment. Defaults to `\"production\"`. */\n env?: DirectEnv;\n /**\n * Target Vana network for chain-aware defaults. Defaults to the selected\n * environment's historical network (`mainnet` for production, `moksha` for\n * dev). Use `network: \"moksha\"` with the default production env for\n * production app/API URLs on testnet.\n */\n network?: DirectNetwork;\n /**\n * The app private key (`0x`-prefixed, 32 bytes). Server-side only — this key\n * is the app's on-chain identity and is never exposed to the browser.\n */\n appPrivateKey?: string;\n /**\n * @deprecated Use {@link DirectDataControllerConfig.appPrivateKey}. Accepted as\n * a backwards-compatible alias; if both are set, `appPrivateKey` wins.\n */\n builderPrivateKey?: string;\n /** App identity advertised during approval. */\n app: DirectAppConfig;\n /** Data source key (e.g. `\"icloud_notes\"`). */\n source: string;\n /** Scopes to request (e.g. `[\"icloud_notes.notes\"]`). At least one required. */\n scopes: string[];\n /**\n * Override the resolved service endpoints (partial). Useful for pointing at a\n * non-standard deployment.\n */\n endpoints?: Partial<DirectServiceEndpoints>;\n /**\n * Client for the Vana Account access-request API. Defaults to a client against\n * the resolved Vana Account endpoints; inject your own to point at a custom\n * deployment or to supply a test double.\n */\n accessRequestClient?: AccessRequestClient;\n /**\n * Escrow settlement config used when a Personal Server read returns `402`.\n *\n * @remarks\n * Wires the DPv2 escrow gateway (`protocol/escrow`). The controller supplies\n * the EIP-712 `signTypedData` from the app key automatically.\n *\n * When omitted (or partially omitted), the SDK derives defaults from the\n * per-network endpoints table and the contract registry:\n * - `client` defaults to a gateway client at `endpoints.escrowGatewayUrl`\n * - `escrowContract` defaults to `CONTRACTS.DataPortabilityEscrow.addresses[chainId]`\n * - `chainId` defaults to the controller's resolved chain id\n *\n * Provide this field only to override a specific default.\n */\n escrow?: Partial<DirectEscrowConfig>;\n /** `fetch` used by the default access-request client. Defaults to `globalThis.fetch`. */\n fetchFn?: FetchLike;\n /** `fetch` used for the Personal Server read. Defaults to `globalThis.fetch`. */\n personalServerFetch?: PersonalServerFetch;\n /**\n * Transport-retry knobs for the Personal Server read\n * ({@link PersonalServerTransportRetryOptions}). Defaults to 3 attempts with\n * exponential backoff. Retries fire only when fetch throws (the browser-PS\n * relay reconnect window), never on a received HTTP status, and never\n * re-sign a payment.\n */\n personalServerTransportRetry?: PersonalServerTransportRetryOptions;\n}\n\n/**\n * Controller-level escrow config — the {@link EscrowPaymentConfig} minus the\n * `signTypedData` and `chainId` the controller injects itself.\n */\nexport interface DirectEscrowConfig extends Omit<\n EscrowPaymentConfig,\n \"signTypedData\" | \"chainId\"\n> {\n /**\n * Chain id for the EIP-712 domain. Defaults to the controller's environment\n * (1480 for mainnet, 14800 for moksha).\n */\n chainId?: number;\n}\n\n/**\n * Server-side controller for the direct Data Portability flow.\n *\n * @typeParam T - Shape of the data returned by {@link DirectDataController.readApprovedData}.\n */\nexport interface DirectDataController {\n /** The on-chain address of the app, derived from `appPrivateKey`. */\n readonly appAddress: string;\n\n /**\n * The app's on-chain address — the address to fund and inspect in the Builder\n * activity report. Equivalent to {@link DirectDataController.appAddress}.\n *\n * @returns The app's `0x`-prefixed address.\n */\n getAppAddress(): string;\n\n /**\n * The app's full identity: its configured id/name/homepage plus the derived\n * on-chain address. Useful for telling builders which app address to fund or\n * look up.\n *\n * @returns `{ id, name, homepageUrl, address }`.\n */\n getAppIdentity(): AppIdentity;\n\n /**\n * Create an access request the user can approve.\n *\n * @param input - The post-approval return URL and optional create retry key.\n * @returns The request id, HTTPS approval URL, and — for a pending deep Direct\n * request on mobile — an optional HTTPS `mobileContinuationUrl`.\n */\n createAccessRequest(input: {\n returnUrl: string;\n /** Optional foreground mobile delivery callback. */\n foregroundDelivery?: ForegroundDelivery;\n /**\n * Stable retry key when the caller retries after an uncertain response.\n * Each create without one gets its own generated key.\n */\n idempotencyKey?: string;\n }): Promise<AccessRequest>;\n\n /**\n * Fetch the current status of an access request.\n *\n * @param requestId - The `dcr_*` id from {@link DirectDataController.createAccessRequest}.\n * @returns `{ status, personalServerUrl?, grantId?, scope?, scopes? }`.\n */\n getAccessRequestStatus(requestId: string): Promise<AccessRequestStatus>;\n\n /**\n * Read the approved data from the user's Personal Server.\n *\n * @remarks\n * Resolves the request to its grant + Personal Server and performs a Web3Signed\n * read. Hides the `402 Payment Required` flow by default: if a read needs\n * payment, it signs the Personal Server's payment challenge, retries with\n * `X-PAYMENT`, and attaches shape-validated but unauthenticated\n * {@link DirectPaymentResponseMetadata} under `payment` when the Personal\n * Server returns it. After a successful read, the controller acknowledges\n * the DCR so Vana Web can close/redirect the approval tab.\n *\n * A request can approve several scopes. This reads **one** of them — `scope`\n * when given, otherwise the first approved scope. Use\n * {@link DirectDataController.readAllApprovedData} to read them all.\n *\n * Acknowledging moves the DCR to `completed`, which is terminal and no longer\n * read-ready. To read several scopes with your own loop, pass\n * `acknowledge: false` on every call but the last.\n *\n * @param input - The `dcr_*` request id, the optional `scope` to read, and an\n * optional `acknowledge` flag (default `true`).\n * @returns `{ scope, data, payment? }`.\n * @throws {@link AccessNotApprovedError} if the request is not approved.\n * @throws {@link ScopeNotApprovedError} if `scope` is not an approved scope.\n * @throws {@link PaymentRequiredError} if payment is required but unsettled.\n */\n readApprovedData<T = unknown>(input: {\n requestId: string;\n scope?: string;\n acknowledge?: boolean;\n }): Promise<ApprovedDataResult<T>>;\n\n /**\n * Read every scope the user approved on a request.\n *\n * @remarks\n * Reads the scopes in approval order, then acknowledges the DCR **once**,\n * after the last read — acknowledging earlier would move the request to\n * `completed` and make the remaining scopes unreadable.\n *\n * Each scope is a separate Personal Server read that settles its own\n * `data_access` fee from escrow, so reading N scopes costs N times a\n * single-scope read. The one-off registration fee is charged per grant, not\n * per scope.\n *\n * A scope that fails does not abort the rest: successes land in `results` and\n * failures in `errors`, because the fees for earlier scopes are already spent.\n * If any scope fails the request is left unacknowledged, so the scopes that\n * failed stay retryable — read them with `readApprovedData({ scope })` and\n * acknowledge on the last one.\n *\n * @param input - The `dcr_*` request id to read.\n * @returns `{ results, errors }`, both keyed by scope.\n * @throws {@link AccessNotApprovedError} if the request is not approved.\n */\n readAllApprovedData<T = unknown>(input: {\n requestId: string;\n }): Promise<MultiScopeDataResult<T>>;\n}\n\nfunction isHexPrivateKey(value: string): value is Hex {\n return /^0x[0-9a-fA-F]{64}$/.test(value);\n}\n\n// A DCR is read-ready only while the grant exists and the Personal Server is\n// still serving it: `approved` (durable PS) or `ready_for_read` (browser PS).\n// `completed` is terminal — the app already read and acknowledged, and the\n// browser PS may be gone — so it is deliberately excluded here.\nfunction isReadReadyStatus(status: AccessRequestStatusValue): boolean {\n return status === \"approved\" || status === \"ready_for_read\";\n}\n\n/**\n * Create a {@link DirectDataController}.\n *\n * @param config - Controller configuration (env, key, app identity, source, scopes).\n * @returns A ready-to-use controller.\n * @throws {@link DirectConfigError} when the key or scopes are invalid.\n */\nexport function createDirectDataController(\n config: DirectDataControllerConfig,\n): DirectDataController {\n // `appPrivateKey` is the documented field; `builderPrivateKey` is a\n // deprecated alias kept for backwards compatibility.\n const privateKey = config.appPrivateKey ?? config.builderPrivateKey;\n if (!privateKey || !isHexPrivateKey(privateKey)) {\n throw new DirectConfigError(\n \"appPrivateKey must be a 0x-prefixed 32-byte hex string\",\n );\n }\n if (!config.scopes || config.scopes.length === 0) {\n throw new DirectConfigError(\"At least one scope is required\");\n }\n // Validate scopes eagerly so misconfiguration fails at construction.\n for (const scope of config.scopes) {\n parseScope(scope);\n }\n\n const env: DirectEnv = config.env ?? \"production\";\n const network: DirectNetwork = config.network ?? getDirectDefaultNetwork(env);\n const defaultEndpoints = getDirectEndpoints(env);\n const chainId = config.endpoints?.chainId ?? getDirectNetworkChainId(network);\n const endpoints: DirectServiceEndpoints = {\n ...defaultEndpoints,\n ...config.endpoints,\n chainId,\n };\n\n const account = privateKeyToAccount(privateKey as Hex);\n const signMessage: Web3SignedSignFn = (message: string) =>\n account.signMessage({ message });\n // viem's account.signTypedData satisfies the structural SignTypedDataFn used\n // by the escrow GenericPayment signer.\n const signTypedData = account.signTypedData as unknown as SignTypedDataFn;\n const accessRequestClient: AccessRequestClient =\n config.accessRequestClient ??\n createDefaultAccessRequestClient({\n baseUrl: endpoints.accessRequestBaseUrl,\n approvalBaseUrl: endpoints.approvalAppBaseUrl,\n env,\n fetchFn: config.fetchFn,\n appAddress: account.address,\n signMessage,\n });\n\n // Build the escrow payment config, defaulting from the per-network endpoints\n // table and the contract registry when `config.escrow` is omitted or partial.\n const escrowChainId = config.escrow?.chainId ?? chainId;\n const defaultEscrowContract =\n CONTRACTS.DataPortabilityEscrow.addresses[\n escrowChainId as keyof typeof CONTRACTS.DataPortabilityEscrow.addresses\n ] ?? undefined;\n if (!config.escrow?.escrowContract && !defaultEscrowContract) {\n throw new DirectConfigError(\n `No DataPortabilityEscrow address found in the registry for chainId ${escrowChainId}. ` +\n `Provide an explicit escrow.escrowContract in the controller config.`,\n );\n }\n const escrow: EscrowPaymentConfig = {\n client:\n config.escrow?.client ??\n createEscrowGatewayClient(endpoints.escrowGatewayUrl),\n escrowContract:\n config.escrow?.escrowContract ?? (defaultEscrowContract as `0x${string}`),\n chainId: escrowChainId,\n nonceSource: config.escrow?.nonceSource,\n signTypedData,\n };\n\n return {\n appAddress: account.address,\n\n getAppAddress(): string {\n return account.address;\n },\n\n getAppIdentity(): AppIdentity {\n return {\n id: config.app.id,\n name: config.app.name,\n homepageUrl: config.app.homepageUrl,\n address: account.address,\n };\n },\n\n async createAccessRequest(input): Promise<AccessRequest> {\n return accessRequestClient.createAccessRequest({\n appAddress: account.address,\n app: config.app,\n source: config.source,\n scopes: config.scopes,\n returnUrl: input.returnUrl,\n network,\n ...(input.foregroundDelivery !== undefined\n ? { foregroundDelivery: input.foregroundDelivery }\n : {}),\n ...(input.idempotencyKey !== undefined\n ? { idempotencyKey: input.idempotencyKey }\n : {}),\n });\n },\n\n async getAccessRequestStatus(\n requestId: string,\n ): Promise<AccessRequestStatus> {\n return accessRequestClient.getAccessRequestStatus(requestId);\n },\n\n async readApprovedData<T = unknown>(input: {\n requestId: string;\n scope?: string;\n acknowledge?: boolean;\n }): Promise<ApprovedDataResult<T>> {\n const status = await requireReadReady(input.requestId);\n const scope = resolveRequestedScope(status, input.scope);\n\n const result = await readScope<T>(status, scope);\n if (input.acknowledge !== false) {\n await acknowledgeQuietly(input.requestId);\n }\n return result;\n },\n\n async readAllApprovedData<T = unknown>(input: {\n requestId: string;\n }): Promise<MultiScopeDataResult<T>> {\n const status = await requireReadReady(input.requestId);\n const scopes = approvedScopes(status);\n\n const results: Record<string, ApprovedDataResult<T>> = {};\n const errors: Record<string, Error> = {};\n // Sequential, not parallel: each read settles its own escrow payment and\n // the default nonce source is process-local, so concurrent reads would\n // race on the payment nonce.\n for (const scope of scopes) {\n try {\n results[scope] = await readScope<T>(status, scope);\n } catch (error) {\n errors[scope] =\n error instanceof Error ? error : new Error(String(error));\n }\n }\n\n // Acknowledge only after the last read, and only if every scope read —\n // acking moves the DCR to `completed`, which is terminal and no longer\n // read-ready, so acking on a partial failure would make the scope that\n // failed impossible to retry.\n if (Object.keys(errors).length === 0) {\n await acknowledgeQuietly(input.requestId);\n }\n\n return { results, errors };\n },\n };\n\n async function requireReadReady(\n requestId: string,\n ): Promise<AccessRequestStatus> {\n const status = await accessRequestClient.getAccessRequestStatus(requestId);\n // `scope` and `scopes` are both optional on the public status type, and a\n // client may return either one — require at least one approved scope rather\n // than the singular field specifically.\n if (\n !isReadReadyStatus(status.status) ||\n !status.personalServerUrl ||\n !status.grantId ||\n approvedScopes(status).length === 0\n ) {\n throw new AccessNotApprovedError(\n \"Request is not approved or is missing grantId/scope/personalServerUrl\",\n {\n requestId,\n status: status.status,\n hasPersonalServerUrl: Boolean(status.personalServerUrl),\n hasGrantId: Boolean(status.grantId),\n hasScope: approvedScopes(status).length > 0,\n },\n );\n }\n return status;\n }\n\n /** Approved scopes in approval order, falling back to the single `scope`. */\n function approvedScopes(status: AccessRequestStatus): string[] {\n if (status.scopes && status.scopes.length > 0) return status.scopes;\n return status.scope ? [status.scope] : [];\n }\n\n /**\n * Resolve which scope to read. Rejects an unapproved scope up front so it\n * never reaches the Personal Server and never settles a fee.\n */\n function resolveRequestedScope(\n status: AccessRequestStatus,\n requested?: string,\n ): string {\n const scopes = approvedScopes(status);\n if (requested === undefined) return scopes[0];\n if (!scopes.includes(requested)) {\n throw new ScopeNotApprovedError(\n `Scope \"${requested}\" is not approved on this request`,\n { requestedScope: requested, approvedScopes: scopes },\n );\n }\n return requested;\n }\n\n async function readScope<T>(\n status: AccessRequestStatus,\n scope: string,\n ): Promise<ApprovedDataResult<T>> {\n const result = await readPersonalServerData({\n personalServerUrl: status.personalServerUrl as string,\n scope,\n grantId: status.grantId as string,\n payerAddress: account.address,\n signMessage,\n escrow,\n fetchFn: config.personalServerFetch,\n transportRetry: config.personalServerTransportRetry,\n });\n return { scope, data: result.data as T, payment: result.payment };\n }\n\n async function acknowledgeQuietly(requestId: string): Promise<void> {\n try {\n await accessRequestClient.acknowledgeRead?.(requestId);\n } catch {\n // The read already succeeded; ack only drives Vana Web completion UX.\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/direct/controller.ts"],"sourcesContent":["/**\n * Direct Data Controller — the server-side facade for the two-tab Data\n * Portability flow.\n *\n * @remarks\n * One controller owns an app's private key, source, scopes, app identity, and\n * payment flow. It exposes the three methods the builder guide documents:\n *\n * - {@link DirectDataController.createAccessRequest} — start an approval request.\n * - {@link DirectDataController.getAccessRequestStatus} — poll while the Vana tab is open.\n * - {@link DirectDataController.readApprovedData} — read from the Personal Server,\n * handling 402 Payment Required.\n *\n * Access requests are created through the Vana Account access-request API; the\n * Personal Server read uses Web3Signed auth; and payment uses the DPv2 escrow\n * surface (`protocol/escrow`) — when a read returns `402`, the controller signs\n * a `GenericPayment` with the app key, settles it through the escrow gateway,\n * and retries.\n *\n * @category Direct\n * @module direct/controller\n */\n\nimport { privateKeyToAccount } from \"viem/accounts\";\nimport type { Hex } from \"viem\";\nimport type { Web3SignedSignFn } from \"../auth/web3-signed-builder\";\nimport { parseScope } from \"../protocol/scopes\";\nimport { parseScopeEntry } from \"../protocol/scope-actions\";\nimport { createEscrowGatewayClient } from \"../protocol/escrow\";\nimport { CONTRACTS } from \"../generated/addresses\";\nimport {\n createDefaultAccessRequestClient,\n type FetchLike,\n} from \"./access-request-client\";\nimport {\n getDirectDefaultNetwork,\n getDirectEndpoints,\n getDirectNetworkChainId,\n} from \"./endpoints\";\nimport {\n AccessNotApprovedError,\n DirectConfigError,\n ScopeNotApprovedError,\n} from \"./errors\";\nimport {\n type EscrowPaymentConfig,\n type SignTypedDataFn,\n} from \"./escrow-payment\";\nimport {\n readPersonalServerData,\n type PersonalServerFetch,\n type PersonalServerTransportRetryOptions,\n} from \"./personal-server-read\";\nimport type {\n AccessRequest,\n AccessRequestClient,\n AccessRequestStatus,\n AccessRequestStatusValue,\n ApprovedDataResult,\n AppIdentity,\n DirectAppConfig,\n DirectEnv,\n DirectNetwork,\n DirectPaymentResponseMetadata,\n DirectServiceEndpoints,\n ForegroundDelivery,\n MultiScopeDataResult,\n} from \"./types\";\n\n/** Configuration for {@link createDirectDataController}. */\nexport interface DirectDataControllerConfig {\n /** Target environment. Defaults to `\"production\"`. */\n env?: DirectEnv;\n /**\n * Target Vana network for chain-aware defaults. Defaults to the selected\n * environment's historical network (`mainnet` for production, `moksha` for\n * dev). Use `network: \"moksha\"` with the default production env for\n * production app/API URLs on testnet.\n */\n network?: DirectNetwork;\n /**\n * The app private key (`0x`-prefixed, 32 bytes). Server-side only — this key\n * is the app's on-chain identity and is never exposed to the browser.\n */\n appPrivateKey?: string;\n /**\n * @deprecated Use {@link DirectDataControllerConfig.appPrivateKey}. Accepted as\n * a backwards-compatible alias; if both are set, `appPrivateKey` wins.\n */\n builderPrivateKey?: string;\n /** App identity advertised during approval. */\n app: DirectAppConfig;\n /** Data source key (e.g. `\"icloud_notes\"`). */\n source: string;\n /**\n * Grant scope entries to request. At least one required.\n *\n * Each entry is `[operation:]scope` (see `parseScopeEntry`): a bare entry\n * such as `\"icloud_notes.notes\"` requests read, and `\"write:coach.weekly\"`\n * requests write. The entries are carried through to the access request\n * verbatim and become the grant's `scopes`, so a request can mix both\n * (`[\"oura.sleep\", \"coach.weekly\", \"write:coach.weekly\"]`).\n *\n * The scope part must be a concrete `{source}.{category}[.{subcategory}]`\n * scope: this flow reads approved scopes back one by one, so wildcard\n * patterns (`chatgpt.*`, `write:chatgpt.*`) are not accepted here for\n * either operation.\n */\n scopes: string[];\n /**\n * Override the resolved service endpoints (partial). Useful for pointing at a\n * non-standard deployment.\n */\n endpoints?: Partial<DirectServiceEndpoints>;\n /**\n * Client for the Vana Account access-request API. Defaults to a client against\n * the resolved Vana Account endpoints; inject your own to point at a custom\n * deployment or to supply a test double.\n */\n accessRequestClient?: AccessRequestClient;\n /**\n * Escrow settlement config used when a Personal Server read returns `402`.\n *\n * @remarks\n * Wires the DPv2 escrow gateway (`protocol/escrow`). The controller supplies\n * the EIP-712 `signTypedData` from the app key automatically.\n *\n * When omitted (or partially omitted), the SDK derives defaults from the\n * per-network endpoints table and the contract registry:\n * - `client` defaults to a gateway client at `endpoints.escrowGatewayUrl`\n * - `escrowContract` defaults to `CONTRACTS.DataPortabilityEscrow.addresses[chainId]`\n * - `chainId` defaults to the controller's resolved chain id\n *\n * Provide this field only to override a specific default.\n */\n escrow?: Partial<DirectEscrowConfig>;\n /** `fetch` used by the default access-request client. Defaults to `globalThis.fetch`. */\n fetchFn?: FetchLike;\n /** `fetch` used for the Personal Server read. Defaults to `globalThis.fetch`. */\n personalServerFetch?: PersonalServerFetch;\n /**\n * Transport-retry knobs for the Personal Server read\n * ({@link PersonalServerTransportRetryOptions}). Defaults to 3 attempts with\n * exponential backoff. Retries fire only when fetch throws (the browser-PS\n * relay reconnect window), never on a received HTTP status, and never\n * re-sign a payment.\n */\n personalServerTransportRetry?: PersonalServerTransportRetryOptions;\n}\n\n/**\n * Controller-level escrow config — the {@link EscrowPaymentConfig} minus the\n * `signTypedData` and `chainId` the controller injects itself.\n */\nexport interface DirectEscrowConfig extends Omit<\n EscrowPaymentConfig,\n \"signTypedData\" | \"chainId\"\n> {\n /**\n * Chain id for the EIP-712 domain. Defaults to the controller's environment\n * (1480 for mainnet, 14800 for moksha).\n */\n chainId?: number;\n}\n\n/**\n * Server-side controller for the direct Data Portability flow.\n *\n * @typeParam T - Shape of the data returned by {@link DirectDataController.readApprovedData}.\n */\nexport interface DirectDataController {\n /** The on-chain address of the app, derived from `appPrivateKey`. */\n readonly appAddress: string;\n\n /**\n * The app's on-chain address — the address to fund and inspect in the Builder\n * activity report. Equivalent to {@link DirectDataController.appAddress}.\n *\n * @returns The app's `0x`-prefixed address.\n */\n getAppAddress(): string;\n\n /**\n * The app's full identity: its configured id/name/homepage plus the derived\n * on-chain address. Useful for telling builders which app address to fund or\n * look up.\n *\n * @returns `{ id, name, homepageUrl, address }`.\n */\n getAppIdentity(): AppIdentity;\n\n /**\n * Create an access request the user can approve.\n *\n * @param input - The post-approval return URL and optional create retry key.\n * @returns The request id, HTTPS approval URL, and — for a pending deep Direct\n * request on mobile — an optional HTTPS `mobileContinuationUrl`.\n */\n createAccessRequest(input: {\n returnUrl: string;\n /** Optional foreground mobile delivery callback. */\n foregroundDelivery?: ForegroundDelivery;\n /**\n * Stable retry key when the caller retries after an uncertain response.\n * Each create without one gets its own generated key.\n */\n idempotencyKey?: string;\n }): Promise<AccessRequest>;\n\n /**\n * Fetch the current status of an access request.\n *\n * @param requestId - The `dcr_*` id from {@link DirectDataController.createAccessRequest}.\n * @returns `{ status, personalServerUrl?, grantId?, scope?, scopes? }`.\n */\n getAccessRequestStatus(requestId: string): Promise<AccessRequestStatus>;\n\n /**\n * Read the approved data from the user's Personal Server.\n *\n * @remarks\n * Resolves the request to its grant + Personal Server and performs a Web3Signed\n * read. Hides the `402 Payment Required` flow by default: if a read needs\n * payment, it signs the Personal Server's payment challenge, retries with\n * `X-PAYMENT`, and attaches shape-validated but unauthenticated\n * {@link DirectPaymentResponseMetadata} under `payment` when the Personal\n * Server returns it. After a successful read, the controller acknowledges\n * the DCR so Vana Web can close/redirect the approval tab.\n *\n * A request can approve several scopes. This reads **one** of them — `scope`\n * when given, otherwise the first approved scope. Use\n * {@link DirectDataController.readAllApprovedData} to read them all.\n *\n * Acknowledging moves the DCR to `completed`, which is terminal and no longer\n * read-ready. To read several scopes with your own loop, pass\n * `acknowledge: false` on every call but the last.\n *\n * @param input - The `dcr_*` request id, the optional `scope` to read, and an\n * optional `acknowledge` flag (default `true`).\n * @returns `{ scope, data, payment? }`.\n * @throws {@link AccessNotApprovedError} if the request is not approved.\n * @throws {@link ScopeNotApprovedError} if `scope` is not an approved scope.\n * @throws {@link PaymentRequiredError} if payment is required but unsettled.\n */\n readApprovedData<T = unknown>(input: {\n requestId: string;\n scope?: string;\n acknowledge?: boolean;\n }): Promise<ApprovedDataResult<T>>;\n\n /**\n * Read every scope the user approved on a request.\n *\n * @remarks\n * Reads the scopes in approval order, then acknowledges the DCR **once**,\n * after the last read — acknowledging earlier would move the request to\n * `completed` and make the remaining scopes unreadable.\n *\n * Each scope is a separate Personal Server read that settles its own\n * `data_access` fee from escrow, so reading N scopes costs N times a\n * single-scope read. The one-off registration fee is charged per grant, not\n * per scope.\n *\n * A scope that fails does not abort the rest: successes land in `results` and\n * failures in `errors`, because the fees for earlier scopes are already spent.\n * If any scope fails the request is left unacknowledged, so the scopes that\n * failed stay retryable — read them with `readApprovedData({ scope })` and\n * acknowledge on the last one.\n *\n * @param input - The `dcr_*` request id to read.\n * @returns `{ results, errors }`, both keyed by scope.\n * @throws {@link AccessNotApprovedError} if the request is not approved.\n */\n readAllApprovedData<T = unknown>(input: {\n requestId: string;\n }): Promise<MultiScopeDataResult<T>>;\n}\n\nfunction isHexPrivateKey(value: string): value is Hex {\n return /^0x[0-9a-fA-F]{64}$/.test(value);\n}\n\n// A DCR is read-ready only while the grant exists and the Personal Server is\n// still serving it: `approved` (durable PS) or `ready_for_read` (browser PS).\n// `completed` is terminal — the app already read and acknowledged, and the\n// browser PS may be gone — so it is deliberately excluded here.\nfunction isReadReadyStatus(status: AccessRequestStatusValue): boolean {\n return status === \"approved\" || status === \"ready_for_read\";\n}\n\n/**\n * Create a {@link DirectDataController}.\n *\n * @param config - Controller configuration (env, key, app identity, source, scopes).\n * @returns A ready-to-use controller.\n * @throws {@link DirectConfigError} when the key is missing or malformed, when\n * `scopes` is empty, or when no escrow contract can be resolved.\n * @throws InvalidScopeEntryError when a `scopes` entry does not fit the\n * `[operation:]scope` grammar (an unknown operation prefix such as `delete:`).\n * @throws ZodError when the scope part of an entry is not a valid scope.\n */\nexport function createDirectDataController(\n config: DirectDataControllerConfig,\n): DirectDataController {\n // `appPrivateKey` is the documented field; `builderPrivateKey` is a\n // deprecated alias kept for backwards compatibility.\n const privateKey = config.appPrivateKey ?? config.builderPrivateKey;\n if (!privateKey || !isHexPrivateKey(privateKey)) {\n throw new DirectConfigError(\n \"appPrivateKey must be a 0x-prefixed 32-byte hex string\",\n );\n }\n if (!config.scopes || config.scopes.length === 0) {\n throw new DirectConfigError(\"At least one scope is required\");\n }\n // Validate scopes eagerly so misconfiguration fails at construction. Each\n // element is a grant scope entry (`[operation:]scope`), so the operation\n // prefix is stripped first and only the scope part is checked against the\n // scope grammar — `write:coach.weekly` is a valid write-grant request, and\n // an unknown operation (`delete:x`) throws rather than being taken as read.\n // The entries themselves are passed through to the access request verbatim,\n // prefix included.\n for (const entry of config.scopes) {\n parseScope(parseScopeEntry(entry).scope);\n }\n\n const env: DirectEnv = config.env ?? \"production\";\n const network: DirectNetwork = config.network ?? getDirectDefaultNetwork(env);\n const defaultEndpoints = getDirectEndpoints(env);\n const chainId = config.endpoints?.chainId ?? getDirectNetworkChainId(network);\n const endpoints: DirectServiceEndpoints = {\n ...defaultEndpoints,\n ...config.endpoints,\n chainId,\n };\n\n const account = privateKeyToAccount(privateKey as Hex);\n const signMessage: Web3SignedSignFn = (message: string) =>\n account.signMessage({ message });\n // viem's account.signTypedData satisfies the structural SignTypedDataFn used\n // by the escrow GenericPayment signer.\n const signTypedData = account.signTypedData as unknown as SignTypedDataFn;\n const accessRequestClient: AccessRequestClient =\n config.accessRequestClient ??\n createDefaultAccessRequestClient({\n baseUrl: endpoints.accessRequestBaseUrl,\n approvalBaseUrl: endpoints.approvalAppBaseUrl,\n env,\n fetchFn: config.fetchFn,\n appAddress: account.address,\n signMessage,\n });\n\n // Build the escrow payment config, defaulting from the per-network endpoints\n // table and the contract registry when `config.escrow` is omitted or partial.\n const escrowChainId = config.escrow?.chainId ?? chainId;\n const defaultEscrowContract =\n CONTRACTS.DataPortabilityEscrow.addresses[\n escrowChainId as keyof typeof CONTRACTS.DataPortabilityEscrow.addresses\n ] ?? undefined;\n if (!config.escrow?.escrowContract && !defaultEscrowContract) {\n throw new DirectConfigError(\n `No DataPortabilityEscrow address found in the registry for chainId ${escrowChainId}. ` +\n `Provide an explicit escrow.escrowContract in the controller config.`,\n );\n }\n const escrow: EscrowPaymentConfig = {\n client:\n config.escrow?.client ??\n createEscrowGatewayClient(endpoints.escrowGatewayUrl),\n escrowContract:\n config.escrow?.escrowContract ?? (defaultEscrowContract as `0x${string}`),\n chainId: escrowChainId,\n nonceSource: config.escrow?.nonceSource,\n signTypedData,\n };\n\n return {\n appAddress: account.address,\n\n getAppAddress(): string {\n return account.address;\n },\n\n getAppIdentity(): AppIdentity {\n return {\n id: config.app.id,\n name: config.app.name,\n homepageUrl: config.app.homepageUrl,\n address: account.address,\n };\n },\n\n async createAccessRequest(input): Promise<AccessRequest> {\n return accessRequestClient.createAccessRequest({\n appAddress: account.address,\n app: config.app,\n source: config.source,\n scopes: config.scopes,\n returnUrl: input.returnUrl,\n network,\n ...(input.foregroundDelivery !== undefined\n ? { foregroundDelivery: input.foregroundDelivery }\n : {}),\n ...(input.idempotencyKey !== undefined\n ? { idempotencyKey: input.idempotencyKey }\n : {}),\n });\n },\n\n async getAccessRequestStatus(\n requestId: string,\n ): Promise<AccessRequestStatus> {\n return accessRequestClient.getAccessRequestStatus(requestId);\n },\n\n async readApprovedData<T = unknown>(input: {\n requestId: string;\n scope?: string;\n acknowledge?: boolean;\n }): Promise<ApprovedDataResult<T>> {\n const status = await requireReadReady(input.requestId);\n const scope = resolveRequestedScope(status, input.scope);\n\n const result = await readScope<T>(status, scope);\n if (input.acknowledge !== false) {\n await acknowledgeQuietly(input.requestId);\n }\n return result;\n },\n\n async readAllApprovedData<T = unknown>(input: {\n requestId: string;\n }): Promise<MultiScopeDataResult<T>> {\n const status = await requireReadReady(input.requestId);\n const scopes = approvedScopes(status);\n\n const results: Record<string, ApprovedDataResult<T>> = {};\n const errors: Record<string, Error> = {};\n // Sequential, not parallel: each read settles its own escrow payment and\n // the default nonce source is process-local, so concurrent reads would\n // race on the payment nonce.\n for (const scope of scopes) {\n try {\n results[scope] = await readScope<T>(status, scope);\n } catch (error) {\n errors[scope] =\n error instanceof Error ? error : new Error(String(error));\n }\n }\n\n // Acknowledge only after the last read, and only if every scope read —\n // acking moves the DCR to `completed`, which is terminal and no longer\n // read-ready, so acking on a partial failure would make the scope that\n // failed impossible to retry.\n if (Object.keys(errors).length === 0) {\n await acknowledgeQuietly(input.requestId);\n }\n\n return { results, errors };\n },\n };\n\n async function requireReadReady(\n requestId: string,\n ): Promise<AccessRequestStatus> {\n const status = await accessRequestClient.getAccessRequestStatus(requestId);\n // `scope` and `scopes` are both optional on the public status type, and a\n // client may return either one — require at least one approved scope rather\n // than the singular field specifically.\n if (\n !isReadReadyStatus(status.status) ||\n !status.personalServerUrl ||\n !status.grantId ||\n approvedScopes(status).length === 0\n ) {\n throw new AccessNotApprovedError(\n \"Request is not approved or is missing grantId/scope/personalServerUrl\",\n {\n requestId,\n status: status.status,\n hasPersonalServerUrl: Boolean(status.personalServerUrl),\n hasGrantId: Boolean(status.grantId),\n hasScope: approvedScopes(status).length > 0,\n },\n );\n }\n return status;\n }\n\n /** Approved scopes in approval order, falling back to the single `scope`. */\n function approvedScopes(status: AccessRequestStatus): string[] {\n if (status.scopes && status.scopes.length > 0) return status.scopes;\n return status.scope ? [status.scope] : [];\n }\n\n /**\n * Resolve which scope to read. Rejects an unapproved scope up front so it\n * never reaches the Personal Server and never settles a fee.\n */\n function resolveRequestedScope(\n status: AccessRequestStatus,\n requested?: string,\n ): string {\n const scopes = approvedScopes(status);\n if (requested === undefined) return scopes[0];\n if (!scopes.includes(requested)) {\n throw new ScopeNotApprovedError(\n `Scope \"${requested}\" is not approved on this request`,\n { requestedScope: requested, approvedScopes: scopes },\n );\n }\n return requested;\n }\n\n async function readScope<T>(\n status: AccessRequestStatus,\n scope: string,\n ): Promise<ApprovedDataResult<T>> {\n const result = await readPersonalServerData({\n personalServerUrl: status.personalServerUrl as string,\n scope,\n grantId: status.grantId as string,\n payerAddress: account.address,\n signMessage,\n escrow,\n fetchFn: config.personalServerFetch,\n transportRetry: config.personalServerTransportRetry,\n });\n return { scope, data: result.data as T, payment: result.payment };\n }\n\n async function acknowledgeQuietly(requestId: string): Promise<void> {\n try {\n await accessRequestClient.acknowledgeRead?.(requestId);\n } catch {\n // The read already succeeded; ack only drives Vana Web completion UX.\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -49,7 +49,20 @@ export interface DirectDataControllerConfig {
|
|
|
49
49
|
app: DirectAppConfig;
|
|
50
50
|
/** Data source key (e.g. `"icloud_notes"`). */
|
|
51
51
|
source: string;
|
|
52
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* Grant scope entries to request. At least one required.
|
|
54
|
+
*
|
|
55
|
+
* Each entry is `[operation:]scope` (see `parseScopeEntry`): a bare entry
|
|
56
|
+
* such as `"icloud_notes.notes"` requests read, and `"write:coach.weekly"`
|
|
57
|
+
* requests write. The entries are carried through to the access request
|
|
58
|
+
* verbatim and become the grant's `scopes`, so a request can mix both
|
|
59
|
+
* (`["oura.sleep", "coach.weekly", "write:coach.weekly"]`).
|
|
60
|
+
*
|
|
61
|
+
* The scope part must be a concrete `{source}.{category}[.{subcategory}]`
|
|
62
|
+
* scope: this flow reads approved scopes back one by one, so wildcard
|
|
63
|
+
* patterns (`chatgpt.*`, `write:chatgpt.*`) are not accepted here for
|
|
64
|
+
* either operation.
|
|
65
|
+
*/
|
|
53
66
|
scopes: string[];
|
|
54
67
|
/**
|
|
55
68
|
* Override the resolved service endpoints (partial). Useful for pointing at a
|
|
@@ -213,6 +226,10 @@ export interface DirectDataController {
|
|
|
213
226
|
*
|
|
214
227
|
* @param config - Controller configuration (env, key, app identity, source, scopes).
|
|
215
228
|
* @returns A ready-to-use controller.
|
|
216
|
-
* @throws {@link DirectConfigError} when the key or
|
|
229
|
+
* @throws {@link DirectConfigError} when the key is missing or malformed, when
|
|
230
|
+
* `scopes` is empty, or when no escrow contract can be resolved.
|
|
231
|
+
* @throws InvalidScopeEntryError when a `scopes` entry does not fit the
|
|
232
|
+
* `[operation:]scope` grammar (an unknown operation prefix such as `delete:`).
|
|
233
|
+
* @throws ZodError when the scope part of an entry is not a valid scope.
|
|
217
234
|
*/
|
|
218
235
|
export declare function createDirectDataController(config: DirectDataControllerConfig): DirectDataController;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { privateKeyToAccount } from "viem/accounts";
|
|
2
2
|
import { parseScope } from "../protocol/scopes.js";
|
|
3
|
+
import { parseScopeEntry } from "../protocol/scope-actions.js";
|
|
3
4
|
import { createEscrowGatewayClient } from "../protocol/escrow.js";
|
|
4
5
|
import { CONTRACTS } from "../generated/addresses.js";
|
|
5
6
|
import {
|
|
@@ -34,8 +35,8 @@ function createDirectDataController(config) {
|
|
|
34
35
|
if (!config.scopes || config.scopes.length === 0) {
|
|
35
36
|
throw new DirectConfigError("At least one scope is required");
|
|
36
37
|
}
|
|
37
|
-
for (const
|
|
38
|
-
parseScope(scope);
|
|
38
|
+
for (const entry of config.scopes) {
|
|
39
|
+
parseScope(parseScopeEntry(entry).scope);
|
|
39
40
|
}
|
|
40
41
|
const env = config.env ?? "production";
|
|
41
42
|
const network = config.network ?? getDirectDefaultNetwork(env);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/direct/controller.ts"],"sourcesContent":["/**\n * Direct Data Controller — the server-side facade for the two-tab Data\n * Portability flow.\n *\n * @remarks\n * One controller owns an app's private key, source, scopes, app identity, and\n * payment flow. It exposes the three methods the builder guide documents:\n *\n * - {@link DirectDataController.createAccessRequest} — start an approval request.\n * - {@link DirectDataController.getAccessRequestStatus} — poll while the Vana tab is open.\n * - {@link DirectDataController.readApprovedData} — read from the Personal Server,\n * handling 402 Payment Required.\n *\n * Access requests are created through the Vana Account access-request API; the\n * Personal Server read uses Web3Signed auth; and payment uses the DPv2 escrow\n * surface (`protocol/escrow`) — when a read returns `402`, the controller signs\n * a `GenericPayment` with the app key, settles it through the escrow gateway,\n * and retries.\n *\n * @category Direct\n * @module direct/controller\n */\n\nimport { privateKeyToAccount } from \"viem/accounts\";\nimport type { Hex } from \"viem\";\nimport type { Web3SignedSignFn } from \"../auth/web3-signed-builder\";\nimport { parseScope } from \"../protocol/scopes\";\nimport { createEscrowGatewayClient } from \"../protocol/escrow\";\nimport { CONTRACTS } from \"../generated/addresses\";\nimport {\n createDefaultAccessRequestClient,\n type FetchLike,\n} from \"./access-request-client\";\nimport {\n getDirectDefaultNetwork,\n getDirectEndpoints,\n getDirectNetworkChainId,\n} from \"./endpoints\";\nimport {\n AccessNotApprovedError,\n DirectConfigError,\n ScopeNotApprovedError,\n} from \"./errors\";\nimport {\n type EscrowPaymentConfig,\n type SignTypedDataFn,\n} from \"./escrow-payment\";\nimport {\n readPersonalServerData,\n type PersonalServerFetch,\n type PersonalServerTransportRetryOptions,\n} from \"./personal-server-read\";\nimport type {\n AccessRequest,\n AccessRequestClient,\n AccessRequestStatus,\n AccessRequestStatusValue,\n ApprovedDataResult,\n AppIdentity,\n DirectAppConfig,\n DirectEnv,\n DirectNetwork,\n DirectPaymentResponseMetadata,\n DirectServiceEndpoints,\n ForegroundDelivery,\n MultiScopeDataResult,\n} from \"./types\";\n\n/** Configuration for {@link createDirectDataController}. */\nexport interface DirectDataControllerConfig {\n /** Target environment. Defaults to `\"production\"`. */\n env?: DirectEnv;\n /**\n * Target Vana network for chain-aware defaults. Defaults to the selected\n * environment's historical network (`mainnet` for production, `moksha` for\n * dev). Use `network: \"moksha\"` with the default production env for\n * production app/API URLs on testnet.\n */\n network?: DirectNetwork;\n /**\n * The app private key (`0x`-prefixed, 32 bytes). Server-side only — this key\n * is the app's on-chain identity and is never exposed to the browser.\n */\n appPrivateKey?: string;\n /**\n * @deprecated Use {@link DirectDataControllerConfig.appPrivateKey}. Accepted as\n * a backwards-compatible alias; if both are set, `appPrivateKey` wins.\n */\n builderPrivateKey?: string;\n /** App identity advertised during approval. */\n app: DirectAppConfig;\n /** Data source key (e.g. `\"icloud_notes\"`). */\n source: string;\n /** Scopes to request (e.g. `[\"icloud_notes.notes\"]`). At least one required. */\n scopes: string[];\n /**\n * Override the resolved service endpoints (partial). Useful for pointing at a\n * non-standard deployment.\n */\n endpoints?: Partial<DirectServiceEndpoints>;\n /**\n * Client for the Vana Account access-request API. Defaults to a client against\n * the resolved Vana Account endpoints; inject your own to point at a custom\n * deployment or to supply a test double.\n */\n accessRequestClient?: AccessRequestClient;\n /**\n * Escrow settlement config used when a Personal Server read returns `402`.\n *\n * @remarks\n * Wires the DPv2 escrow gateway (`protocol/escrow`). The controller supplies\n * the EIP-712 `signTypedData` from the app key automatically.\n *\n * When omitted (or partially omitted), the SDK derives defaults from the\n * per-network endpoints table and the contract registry:\n * - `client` defaults to a gateway client at `endpoints.escrowGatewayUrl`\n * - `escrowContract` defaults to `CONTRACTS.DataPortabilityEscrow.addresses[chainId]`\n * - `chainId` defaults to the controller's resolved chain id\n *\n * Provide this field only to override a specific default.\n */\n escrow?: Partial<DirectEscrowConfig>;\n /** `fetch` used by the default access-request client. Defaults to `globalThis.fetch`. */\n fetchFn?: FetchLike;\n /** `fetch` used for the Personal Server read. Defaults to `globalThis.fetch`. */\n personalServerFetch?: PersonalServerFetch;\n /**\n * Transport-retry knobs for the Personal Server read\n * ({@link PersonalServerTransportRetryOptions}). Defaults to 3 attempts with\n * exponential backoff. Retries fire only when fetch throws (the browser-PS\n * relay reconnect window), never on a received HTTP status, and never\n * re-sign a payment.\n */\n personalServerTransportRetry?: PersonalServerTransportRetryOptions;\n}\n\n/**\n * Controller-level escrow config — the {@link EscrowPaymentConfig} minus the\n * `signTypedData` and `chainId` the controller injects itself.\n */\nexport interface DirectEscrowConfig extends Omit<\n EscrowPaymentConfig,\n \"signTypedData\" | \"chainId\"\n> {\n /**\n * Chain id for the EIP-712 domain. Defaults to the controller's environment\n * (1480 for mainnet, 14800 for moksha).\n */\n chainId?: number;\n}\n\n/**\n * Server-side controller for the direct Data Portability flow.\n *\n * @typeParam T - Shape of the data returned by {@link DirectDataController.readApprovedData}.\n */\nexport interface DirectDataController {\n /** The on-chain address of the app, derived from `appPrivateKey`. */\n readonly appAddress: string;\n\n /**\n * The app's on-chain address — the address to fund and inspect in the Builder\n * activity report. Equivalent to {@link DirectDataController.appAddress}.\n *\n * @returns The app's `0x`-prefixed address.\n */\n getAppAddress(): string;\n\n /**\n * The app's full identity: its configured id/name/homepage plus the derived\n * on-chain address. Useful for telling builders which app address to fund or\n * look up.\n *\n * @returns `{ id, name, homepageUrl, address }`.\n */\n getAppIdentity(): AppIdentity;\n\n /**\n * Create an access request the user can approve.\n *\n * @param input - The post-approval return URL and optional create retry key.\n * @returns The request id, HTTPS approval URL, and — for a pending deep Direct\n * request on mobile — an optional HTTPS `mobileContinuationUrl`.\n */\n createAccessRequest(input: {\n returnUrl: string;\n /** Optional foreground mobile delivery callback. */\n foregroundDelivery?: ForegroundDelivery;\n /**\n * Stable retry key when the caller retries after an uncertain response.\n * Each create without one gets its own generated key.\n */\n idempotencyKey?: string;\n }): Promise<AccessRequest>;\n\n /**\n * Fetch the current status of an access request.\n *\n * @param requestId - The `dcr_*` id from {@link DirectDataController.createAccessRequest}.\n * @returns `{ status, personalServerUrl?, grantId?, scope?, scopes? }`.\n */\n getAccessRequestStatus(requestId: string): Promise<AccessRequestStatus>;\n\n /**\n * Read the approved data from the user's Personal Server.\n *\n * @remarks\n * Resolves the request to its grant + Personal Server and performs a Web3Signed\n * read. Hides the `402 Payment Required` flow by default: if a read needs\n * payment, it signs the Personal Server's payment challenge, retries with\n * `X-PAYMENT`, and attaches shape-validated but unauthenticated\n * {@link DirectPaymentResponseMetadata} under `payment` when the Personal\n * Server returns it. After a successful read, the controller acknowledges\n * the DCR so Vana Web can close/redirect the approval tab.\n *\n * A request can approve several scopes. This reads **one** of them — `scope`\n * when given, otherwise the first approved scope. Use\n * {@link DirectDataController.readAllApprovedData} to read them all.\n *\n * Acknowledging moves the DCR to `completed`, which is terminal and no longer\n * read-ready. To read several scopes with your own loop, pass\n * `acknowledge: false` on every call but the last.\n *\n * @param input - The `dcr_*` request id, the optional `scope` to read, and an\n * optional `acknowledge` flag (default `true`).\n * @returns `{ scope, data, payment? }`.\n * @throws {@link AccessNotApprovedError} if the request is not approved.\n * @throws {@link ScopeNotApprovedError} if `scope` is not an approved scope.\n * @throws {@link PaymentRequiredError} if payment is required but unsettled.\n */\n readApprovedData<T = unknown>(input: {\n requestId: string;\n scope?: string;\n acknowledge?: boolean;\n }): Promise<ApprovedDataResult<T>>;\n\n /**\n * Read every scope the user approved on a request.\n *\n * @remarks\n * Reads the scopes in approval order, then acknowledges the DCR **once**,\n * after the last read — acknowledging earlier would move the request to\n * `completed` and make the remaining scopes unreadable.\n *\n * Each scope is a separate Personal Server read that settles its own\n * `data_access` fee from escrow, so reading N scopes costs N times a\n * single-scope read. The one-off registration fee is charged per grant, not\n * per scope.\n *\n * A scope that fails does not abort the rest: successes land in `results` and\n * failures in `errors`, because the fees for earlier scopes are already spent.\n * If any scope fails the request is left unacknowledged, so the scopes that\n * failed stay retryable — read them with `readApprovedData({ scope })` and\n * acknowledge on the last one.\n *\n * @param input - The `dcr_*` request id to read.\n * @returns `{ results, errors }`, both keyed by scope.\n * @throws {@link AccessNotApprovedError} if the request is not approved.\n */\n readAllApprovedData<T = unknown>(input: {\n requestId: string;\n }): Promise<MultiScopeDataResult<T>>;\n}\n\nfunction isHexPrivateKey(value: string): value is Hex {\n return /^0x[0-9a-fA-F]{64}$/.test(value);\n}\n\n// A DCR is read-ready only while the grant exists and the Personal Server is\n// still serving it: `approved` (durable PS) or `ready_for_read` (browser PS).\n// `completed` is terminal — the app already read and acknowledged, and the\n// browser PS may be gone — so it is deliberately excluded here.\nfunction isReadReadyStatus(status: AccessRequestStatusValue): boolean {\n return status === \"approved\" || status === \"ready_for_read\";\n}\n\n/**\n * Create a {@link DirectDataController}.\n *\n * @param config - Controller configuration (env, key, app identity, source, scopes).\n * @returns A ready-to-use controller.\n * @throws {@link DirectConfigError} when the key or scopes are invalid.\n */\nexport function createDirectDataController(\n config: DirectDataControllerConfig,\n): DirectDataController {\n // `appPrivateKey` is the documented field; `builderPrivateKey` is a\n // deprecated alias kept for backwards compatibility.\n const privateKey = config.appPrivateKey ?? config.builderPrivateKey;\n if (!privateKey || !isHexPrivateKey(privateKey)) {\n throw new DirectConfigError(\n \"appPrivateKey must be a 0x-prefixed 32-byte hex string\",\n );\n }\n if (!config.scopes || config.scopes.length === 0) {\n throw new DirectConfigError(\"At least one scope is required\");\n }\n // Validate scopes eagerly so misconfiguration fails at construction.\n for (const scope of config.scopes) {\n parseScope(scope);\n }\n\n const env: DirectEnv = config.env ?? \"production\";\n const network: DirectNetwork = config.network ?? getDirectDefaultNetwork(env);\n const defaultEndpoints = getDirectEndpoints(env);\n const chainId = config.endpoints?.chainId ?? getDirectNetworkChainId(network);\n const endpoints: DirectServiceEndpoints = {\n ...defaultEndpoints,\n ...config.endpoints,\n chainId,\n };\n\n const account = privateKeyToAccount(privateKey as Hex);\n const signMessage: Web3SignedSignFn = (message: string) =>\n account.signMessage({ message });\n // viem's account.signTypedData satisfies the structural SignTypedDataFn used\n // by the escrow GenericPayment signer.\n const signTypedData = account.signTypedData as unknown as SignTypedDataFn;\n const accessRequestClient: AccessRequestClient =\n config.accessRequestClient ??\n createDefaultAccessRequestClient({\n baseUrl: endpoints.accessRequestBaseUrl,\n approvalBaseUrl: endpoints.approvalAppBaseUrl,\n env,\n fetchFn: config.fetchFn,\n appAddress: account.address,\n signMessage,\n });\n\n // Build the escrow payment config, defaulting from the per-network endpoints\n // table and the contract registry when `config.escrow` is omitted or partial.\n const escrowChainId = config.escrow?.chainId ?? chainId;\n const defaultEscrowContract =\n CONTRACTS.DataPortabilityEscrow.addresses[\n escrowChainId as keyof typeof CONTRACTS.DataPortabilityEscrow.addresses\n ] ?? undefined;\n if (!config.escrow?.escrowContract && !defaultEscrowContract) {\n throw new DirectConfigError(\n `No DataPortabilityEscrow address found in the registry for chainId ${escrowChainId}. ` +\n `Provide an explicit escrow.escrowContract in the controller config.`,\n );\n }\n const escrow: EscrowPaymentConfig = {\n client:\n config.escrow?.client ??\n createEscrowGatewayClient(endpoints.escrowGatewayUrl),\n escrowContract:\n config.escrow?.escrowContract ?? (defaultEscrowContract as `0x${string}`),\n chainId: escrowChainId,\n nonceSource: config.escrow?.nonceSource,\n signTypedData,\n };\n\n return {\n appAddress: account.address,\n\n getAppAddress(): string {\n return account.address;\n },\n\n getAppIdentity(): AppIdentity {\n return {\n id: config.app.id,\n name: config.app.name,\n homepageUrl: config.app.homepageUrl,\n address: account.address,\n };\n },\n\n async createAccessRequest(input): Promise<AccessRequest> {\n return accessRequestClient.createAccessRequest({\n appAddress: account.address,\n app: config.app,\n source: config.source,\n scopes: config.scopes,\n returnUrl: input.returnUrl,\n network,\n ...(input.foregroundDelivery !== undefined\n ? { foregroundDelivery: input.foregroundDelivery }\n : {}),\n ...(input.idempotencyKey !== undefined\n ? { idempotencyKey: input.idempotencyKey }\n : {}),\n });\n },\n\n async getAccessRequestStatus(\n requestId: string,\n ): Promise<AccessRequestStatus> {\n return accessRequestClient.getAccessRequestStatus(requestId);\n },\n\n async readApprovedData<T = unknown>(input: {\n requestId: string;\n scope?: string;\n acknowledge?: boolean;\n }): Promise<ApprovedDataResult<T>> {\n const status = await requireReadReady(input.requestId);\n const scope = resolveRequestedScope(status, input.scope);\n\n const result = await readScope<T>(status, scope);\n if (input.acknowledge !== false) {\n await acknowledgeQuietly(input.requestId);\n }\n return result;\n },\n\n async readAllApprovedData<T = unknown>(input: {\n requestId: string;\n }): Promise<MultiScopeDataResult<T>> {\n const status = await requireReadReady(input.requestId);\n const scopes = approvedScopes(status);\n\n const results: Record<string, ApprovedDataResult<T>> = {};\n const errors: Record<string, Error> = {};\n // Sequential, not parallel: each read settles its own escrow payment and\n // the default nonce source is process-local, so concurrent reads would\n // race on the payment nonce.\n for (const scope of scopes) {\n try {\n results[scope] = await readScope<T>(status, scope);\n } catch (error) {\n errors[scope] =\n error instanceof Error ? error : new Error(String(error));\n }\n }\n\n // Acknowledge only after the last read, and only if every scope read —\n // acking moves the DCR to `completed`, which is terminal and no longer\n // read-ready, so acking on a partial failure would make the scope that\n // failed impossible to retry.\n if (Object.keys(errors).length === 0) {\n await acknowledgeQuietly(input.requestId);\n }\n\n return { results, errors };\n },\n };\n\n async function requireReadReady(\n requestId: string,\n ): Promise<AccessRequestStatus> {\n const status = await accessRequestClient.getAccessRequestStatus(requestId);\n // `scope` and `scopes` are both optional on the public status type, and a\n // client may return either one — require at least one approved scope rather\n // than the singular field specifically.\n if (\n !isReadReadyStatus(status.status) ||\n !status.personalServerUrl ||\n !status.grantId ||\n approvedScopes(status).length === 0\n ) {\n throw new AccessNotApprovedError(\n \"Request is not approved or is missing grantId/scope/personalServerUrl\",\n {\n requestId,\n status: status.status,\n hasPersonalServerUrl: Boolean(status.personalServerUrl),\n hasGrantId: Boolean(status.grantId),\n hasScope: approvedScopes(status).length > 0,\n },\n );\n }\n return status;\n }\n\n /** Approved scopes in approval order, falling back to the single `scope`. */\n function approvedScopes(status: AccessRequestStatus): string[] {\n if (status.scopes && status.scopes.length > 0) return status.scopes;\n return status.scope ? [status.scope] : [];\n }\n\n /**\n * Resolve which scope to read. Rejects an unapproved scope up front so it\n * never reaches the Personal Server and never settles a fee.\n */\n function resolveRequestedScope(\n status: AccessRequestStatus,\n requested?: string,\n ): string {\n const scopes = approvedScopes(status);\n if (requested === undefined) return scopes[0];\n if (!scopes.includes(requested)) {\n throw new ScopeNotApprovedError(\n `Scope \"${requested}\" is not approved on this request`,\n { requestedScope: requested, approvedScopes: scopes },\n );\n }\n return requested;\n }\n\n async function readScope<T>(\n status: AccessRequestStatus,\n scope: string,\n ): Promise<ApprovedDataResult<T>> {\n const result = await readPersonalServerData({\n personalServerUrl: status.personalServerUrl as string,\n scope,\n grantId: status.grantId as string,\n payerAddress: account.address,\n signMessage,\n escrow,\n fetchFn: config.personalServerFetch,\n transportRetry: config.personalServerTransportRetry,\n });\n return { scope, data: result.data as T, payment: result.payment };\n }\n\n async function acknowledgeQuietly(requestId: string): Promise<void> {\n try {\n await accessRequestClient.acknowledgeRead?.(requestId);\n } catch {\n // The read already succeeded; ack only drives Vana Web completion UX.\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/direct/controller.ts"],"sourcesContent":["/**\n * Direct Data Controller — the server-side facade for the two-tab Data\n * Portability flow.\n *\n * @remarks\n * One controller owns an app's private key, source, scopes, app identity, and\n * payment flow. It exposes the three methods the builder guide documents:\n *\n * - {@link DirectDataController.createAccessRequest} — start an approval request.\n * - {@link DirectDataController.getAccessRequestStatus} — poll while the Vana tab is open.\n * - {@link DirectDataController.readApprovedData} — read from the Personal Server,\n * handling 402 Payment Required.\n *\n * Access requests are created through the Vana Account access-request API; the\n * Personal Server read uses Web3Signed auth; and payment uses the DPv2 escrow\n * surface (`protocol/escrow`) — when a read returns `402`, the controller signs\n * a `GenericPayment` with the app key, settles it through the escrow gateway,\n * and retries.\n *\n * @category Direct\n * @module direct/controller\n */\n\nimport { privateKeyToAccount } from \"viem/accounts\";\nimport type { Hex } from \"viem\";\nimport type { Web3SignedSignFn } from \"../auth/web3-signed-builder\";\nimport { parseScope } from \"../protocol/scopes\";\nimport { parseScopeEntry } from \"../protocol/scope-actions\";\nimport { createEscrowGatewayClient } from \"../protocol/escrow\";\nimport { CONTRACTS } from \"../generated/addresses\";\nimport {\n createDefaultAccessRequestClient,\n type FetchLike,\n} from \"./access-request-client\";\nimport {\n getDirectDefaultNetwork,\n getDirectEndpoints,\n getDirectNetworkChainId,\n} from \"./endpoints\";\nimport {\n AccessNotApprovedError,\n DirectConfigError,\n ScopeNotApprovedError,\n} from \"./errors\";\nimport {\n type EscrowPaymentConfig,\n type SignTypedDataFn,\n} from \"./escrow-payment\";\nimport {\n readPersonalServerData,\n type PersonalServerFetch,\n type PersonalServerTransportRetryOptions,\n} from \"./personal-server-read\";\nimport type {\n AccessRequest,\n AccessRequestClient,\n AccessRequestStatus,\n AccessRequestStatusValue,\n ApprovedDataResult,\n AppIdentity,\n DirectAppConfig,\n DirectEnv,\n DirectNetwork,\n DirectPaymentResponseMetadata,\n DirectServiceEndpoints,\n ForegroundDelivery,\n MultiScopeDataResult,\n} from \"./types\";\n\n/** Configuration for {@link createDirectDataController}. */\nexport interface DirectDataControllerConfig {\n /** Target environment. Defaults to `\"production\"`. */\n env?: DirectEnv;\n /**\n * Target Vana network for chain-aware defaults. Defaults to the selected\n * environment's historical network (`mainnet` for production, `moksha` for\n * dev). Use `network: \"moksha\"` with the default production env for\n * production app/API URLs on testnet.\n */\n network?: DirectNetwork;\n /**\n * The app private key (`0x`-prefixed, 32 bytes). Server-side only — this key\n * is the app's on-chain identity and is never exposed to the browser.\n */\n appPrivateKey?: string;\n /**\n * @deprecated Use {@link DirectDataControllerConfig.appPrivateKey}. Accepted as\n * a backwards-compatible alias; if both are set, `appPrivateKey` wins.\n */\n builderPrivateKey?: string;\n /** App identity advertised during approval. */\n app: DirectAppConfig;\n /** Data source key (e.g. `\"icloud_notes\"`). */\n source: string;\n /**\n * Grant scope entries to request. At least one required.\n *\n * Each entry is `[operation:]scope` (see `parseScopeEntry`): a bare entry\n * such as `\"icloud_notes.notes\"` requests read, and `\"write:coach.weekly\"`\n * requests write. The entries are carried through to the access request\n * verbatim and become the grant's `scopes`, so a request can mix both\n * (`[\"oura.sleep\", \"coach.weekly\", \"write:coach.weekly\"]`).\n *\n * The scope part must be a concrete `{source}.{category}[.{subcategory}]`\n * scope: this flow reads approved scopes back one by one, so wildcard\n * patterns (`chatgpt.*`, `write:chatgpt.*`) are not accepted here for\n * either operation.\n */\n scopes: string[];\n /**\n * Override the resolved service endpoints (partial). Useful for pointing at a\n * non-standard deployment.\n */\n endpoints?: Partial<DirectServiceEndpoints>;\n /**\n * Client for the Vana Account access-request API. Defaults to a client against\n * the resolved Vana Account endpoints; inject your own to point at a custom\n * deployment or to supply a test double.\n */\n accessRequestClient?: AccessRequestClient;\n /**\n * Escrow settlement config used when a Personal Server read returns `402`.\n *\n * @remarks\n * Wires the DPv2 escrow gateway (`protocol/escrow`). The controller supplies\n * the EIP-712 `signTypedData` from the app key automatically.\n *\n * When omitted (or partially omitted), the SDK derives defaults from the\n * per-network endpoints table and the contract registry:\n * - `client` defaults to a gateway client at `endpoints.escrowGatewayUrl`\n * - `escrowContract` defaults to `CONTRACTS.DataPortabilityEscrow.addresses[chainId]`\n * - `chainId` defaults to the controller's resolved chain id\n *\n * Provide this field only to override a specific default.\n */\n escrow?: Partial<DirectEscrowConfig>;\n /** `fetch` used by the default access-request client. Defaults to `globalThis.fetch`. */\n fetchFn?: FetchLike;\n /** `fetch` used for the Personal Server read. Defaults to `globalThis.fetch`. */\n personalServerFetch?: PersonalServerFetch;\n /**\n * Transport-retry knobs for the Personal Server read\n * ({@link PersonalServerTransportRetryOptions}). Defaults to 3 attempts with\n * exponential backoff. Retries fire only when fetch throws (the browser-PS\n * relay reconnect window), never on a received HTTP status, and never\n * re-sign a payment.\n */\n personalServerTransportRetry?: PersonalServerTransportRetryOptions;\n}\n\n/**\n * Controller-level escrow config — the {@link EscrowPaymentConfig} minus the\n * `signTypedData` and `chainId` the controller injects itself.\n */\nexport interface DirectEscrowConfig extends Omit<\n EscrowPaymentConfig,\n \"signTypedData\" | \"chainId\"\n> {\n /**\n * Chain id for the EIP-712 domain. Defaults to the controller's environment\n * (1480 for mainnet, 14800 for moksha).\n */\n chainId?: number;\n}\n\n/**\n * Server-side controller for the direct Data Portability flow.\n *\n * @typeParam T - Shape of the data returned by {@link DirectDataController.readApprovedData}.\n */\nexport interface DirectDataController {\n /** The on-chain address of the app, derived from `appPrivateKey`. */\n readonly appAddress: string;\n\n /**\n * The app's on-chain address — the address to fund and inspect in the Builder\n * activity report. Equivalent to {@link DirectDataController.appAddress}.\n *\n * @returns The app's `0x`-prefixed address.\n */\n getAppAddress(): string;\n\n /**\n * The app's full identity: its configured id/name/homepage plus the derived\n * on-chain address. Useful for telling builders which app address to fund or\n * look up.\n *\n * @returns `{ id, name, homepageUrl, address }`.\n */\n getAppIdentity(): AppIdentity;\n\n /**\n * Create an access request the user can approve.\n *\n * @param input - The post-approval return URL and optional create retry key.\n * @returns The request id, HTTPS approval URL, and — for a pending deep Direct\n * request on mobile — an optional HTTPS `mobileContinuationUrl`.\n */\n createAccessRequest(input: {\n returnUrl: string;\n /** Optional foreground mobile delivery callback. */\n foregroundDelivery?: ForegroundDelivery;\n /**\n * Stable retry key when the caller retries after an uncertain response.\n * Each create without one gets its own generated key.\n */\n idempotencyKey?: string;\n }): Promise<AccessRequest>;\n\n /**\n * Fetch the current status of an access request.\n *\n * @param requestId - The `dcr_*` id from {@link DirectDataController.createAccessRequest}.\n * @returns `{ status, personalServerUrl?, grantId?, scope?, scopes? }`.\n */\n getAccessRequestStatus(requestId: string): Promise<AccessRequestStatus>;\n\n /**\n * Read the approved data from the user's Personal Server.\n *\n * @remarks\n * Resolves the request to its grant + Personal Server and performs a Web3Signed\n * read. Hides the `402 Payment Required` flow by default: if a read needs\n * payment, it signs the Personal Server's payment challenge, retries with\n * `X-PAYMENT`, and attaches shape-validated but unauthenticated\n * {@link DirectPaymentResponseMetadata} under `payment` when the Personal\n * Server returns it. After a successful read, the controller acknowledges\n * the DCR so Vana Web can close/redirect the approval tab.\n *\n * A request can approve several scopes. This reads **one** of them — `scope`\n * when given, otherwise the first approved scope. Use\n * {@link DirectDataController.readAllApprovedData} to read them all.\n *\n * Acknowledging moves the DCR to `completed`, which is terminal and no longer\n * read-ready. To read several scopes with your own loop, pass\n * `acknowledge: false` on every call but the last.\n *\n * @param input - The `dcr_*` request id, the optional `scope` to read, and an\n * optional `acknowledge` flag (default `true`).\n * @returns `{ scope, data, payment? }`.\n * @throws {@link AccessNotApprovedError} if the request is not approved.\n * @throws {@link ScopeNotApprovedError} if `scope` is not an approved scope.\n * @throws {@link PaymentRequiredError} if payment is required but unsettled.\n */\n readApprovedData<T = unknown>(input: {\n requestId: string;\n scope?: string;\n acknowledge?: boolean;\n }): Promise<ApprovedDataResult<T>>;\n\n /**\n * Read every scope the user approved on a request.\n *\n * @remarks\n * Reads the scopes in approval order, then acknowledges the DCR **once**,\n * after the last read — acknowledging earlier would move the request to\n * `completed` and make the remaining scopes unreadable.\n *\n * Each scope is a separate Personal Server read that settles its own\n * `data_access` fee from escrow, so reading N scopes costs N times a\n * single-scope read. The one-off registration fee is charged per grant, not\n * per scope.\n *\n * A scope that fails does not abort the rest: successes land in `results` and\n * failures in `errors`, because the fees for earlier scopes are already spent.\n * If any scope fails the request is left unacknowledged, so the scopes that\n * failed stay retryable — read them with `readApprovedData({ scope })` and\n * acknowledge on the last one.\n *\n * @param input - The `dcr_*` request id to read.\n * @returns `{ results, errors }`, both keyed by scope.\n * @throws {@link AccessNotApprovedError} if the request is not approved.\n */\n readAllApprovedData<T = unknown>(input: {\n requestId: string;\n }): Promise<MultiScopeDataResult<T>>;\n}\n\nfunction isHexPrivateKey(value: string): value is Hex {\n return /^0x[0-9a-fA-F]{64}$/.test(value);\n}\n\n// A DCR is read-ready only while the grant exists and the Personal Server is\n// still serving it: `approved` (durable PS) or `ready_for_read` (browser PS).\n// `completed` is terminal — the app already read and acknowledged, and the\n// browser PS may be gone — so it is deliberately excluded here.\nfunction isReadReadyStatus(status: AccessRequestStatusValue): boolean {\n return status === \"approved\" || status === \"ready_for_read\";\n}\n\n/**\n * Create a {@link DirectDataController}.\n *\n * @param config - Controller configuration (env, key, app identity, source, scopes).\n * @returns A ready-to-use controller.\n * @throws {@link DirectConfigError} when the key is missing or malformed, when\n * `scopes` is empty, or when no escrow contract can be resolved.\n * @throws InvalidScopeEntryError when a `scopes` entry does not fit the\n * `[operation:]scope` grammar (an unknown operation prefix such as `delete:`).\n * @throws ZodError when the scope part of an entry is not a valid scope.\n */\nexport function createDirectDataController(\n config: DirectDataControllerConfig,\n): DirectDataController {\n // `appPrivateKey` is the documented field; `builderPrivateKey` is a\n // deprecated alias kept for backwards compatibility.\n const privateKey = config.appPrivateKey ?? config.builderPrivateKey;\n if (!privateKey || !isHexPrivateKey(privateKey)) {\n throw new DirectConfigError(\n \"appPrivateKey must be a 0x-prefixed 32-byte hex string\",\n );\n }\n if (!config.scopes || config.scopes.length === 0) {\n throw new DirectConfigError(\"At least one scope is required\");\n }\n // Validate scopes eagerly so misconfiguration fails at construction. Each\n // element is a grant scope entry (`[operation:]scope`), so the operation\n // prefix is stripped first and only the scope part is checked against the\n // scope grammar — `write:coach.weekly` is a valid write-grant request, and\n // an unknown operation (`delete:x`) throws rather than being taken as read.\n // The entries themselves are passed through to the access request verbatim,\n // prefix included.\n for (const entry of config.scopes) {\n parseScope(parseScopeEntry(entry).scope);\n }\n\n const env: DirectEnv = config.env ?? \"production\";\n const network: DirectNetwork = config.network ?? getDirectDefaultNetwork(env);\n const defaultEndpoints = getDirectEndpoints(env);\n const chainId = config.endpoints?.chainId ?? getDirectNetworkChainId(network);\n const endpoints: DirectServiceEndpoints = {\n ...defaultEndpoints,\n ...config.endpoints,\n chainId,\n };\n\n const account = privateKeyToAccount(privateKey as Hex);\n const signMessage: Web3SignedSignFn = (message: string) =>\n account.signMessage({ message });\n // viem's account.signTypedData satisfies the structural SignTypedDataFn used\n // by the escrow GenericPayment signer.\n const signTypedData = account.signTypedData as unknown as SignTypedDataFn;\n const accessRequestClient: AccessRequestClient =\n config.accessRequestClient ??\n createDefaultAccessRequestClient({\n baseUrl: endpoints.accessRequestBaseUrl,\n approvalBaseUrl: endpoints.approvalAppBaseUrl,\n env,\n fetchFn: config.fetchFn,\n appAddress: account.address,\n signMessage,\n });\n\n // Build the escrow payment config, defaulting from the per-network endpoints\n // table and the contract registry when `config.escrow` is omitted or partial.\n const escrowChainId = config.escrow?.chainId ?? chainId;\n const defaultEscrowContract =\n CONTRACTS.DataPortabilityEscrow.addresses[\n escrowChainId as keyof typeof CONTRACTS.DataPortabilityEscrow.addresses\n ] ?? undefined;\n if (!config.escrow?.escrowContract && !defaultEscrowContract) {\n throw new DirectConfigError(\n `No DataPortabilityEscrow address found in the registry for chainId ${escrowChainId}. ` +\n `Provide an explicit escrow.escrowContract in the controller config.`,\n );\n }\n const escrow: EscrowPaymentConfig = {\n client:\n config.escrow?.client ??\n createEscrowGatewayClient(endpoints.escrowGatewayUrl),\n escrowContract:\n config.escrow?.escrowContract ?? (defaultEscrowContract as `0x${string}`),\n chainId: escrowChainId,\n nonceSource: config.escrow?.nonceSource,\n signTypedData,\n };\n\n return {\n appAddress: account.address,\n\n getAppAddress(): string {\n return account.address;\n },\n\n getAppIdentity(): AppIdentity {\n return {\n id: config.app.id,\n name: config.app.name,\n homepageUrl: config.app.homepageUrl,\n address: account.address,\n };\n },\n\n async createAccessRequest(input): Promise<AccessRequest> {\n return accessRequestClient.createAccessRequest({\n appAddress: account.address,\n app: config.app,\n source: config.source,\n scopes: config.scopes,\n returnUrl: input.returnUrl,\n network,\n ...(input.foregroundDelivery !== undefined\n ? { foregroundDelivery: input.foregroundDelivery }\n : {}),\n ...(input.idempotencyKey !== undefined\n ? { idempotencyKey: input.idempotencyKey }\n : {}),\n });\n },\n\n async getAccessRequestStatus(\n requestId: string,\n ): Promise<AccessRequestStatus> {\n return accessRequestClient.getAccessRequestStatus(requestId);\n },\n\n async readApprovedData<T = unknown>(input: {\n requestId: string;\n scope?: string;\n acknowledge?: boolean;\n }): Promise<ApprovedDataResult<T>> {\n const status = await requireReadReady(input.requestId);\n const scope = resolveRequestedScope(status, input.scope);\n\n const result = await readScope<T>(status, scope);\n if (input.acknowledge !== false) {\n await acknowledgeQuietly(input.requestId);\n }\n return result;\n },\n\n async readAllApprovedData<T = unknown>(input: {\n requestId: string;\n }): Promise<MultiScopeDataResult<T>> {\n const status = await requireReadReady(input.requestId);\n const scopes = approvedScopes(status);\n\n const results: Record<string, ApprovedDataResult<T>> = {};\n const errors: Record<string, Error> = {};\n // Sequential, not parallel: each read settles its own escrow payment and\n // the default nonce source is process-local, so concurrent reads would\n // race on the payment nonce.\n for (const scope of scopes) {\n try {\n results[scope] = await readScope<T>(status, scope);\n } catch (error) {\n errors[scope] =\n error instanceof Error ? error : new Error(String(error));\n }\n }\n\n // Acknowledge only after the last read, and only if every scope read —\n // acking moves the DCR to `completed`, which is terminal and no longer\n // read-ready, so acking on a partial failure would make the scope that\n // failed impossible to retry.\n if (Object.keys(errors).length === 0) {\n await acknowledgeQuietly(input.requestId);\n }\n\n return { results, errors };\n },\n };\n\n async function requireReadReady(\n requestId: string,\n ): Promise<AccessRequestStatus> {\n const status = await accessRequestClient.getAccessRequestStatus(requestId);\n // `scope` and `scopes` are both optional on the public status type, and a\n // client may return either one — require at least one approved scope rather\n // than the singular field specifically.\n if (\n !isReadReadyStatus(status.status) ||\n !status.personalServerUrl ||\n !status.grantId ||\n approvedScopes(status).length === 0\n ) {\n throw new AccessNotApprovedError(\n \"Request is not approved or is missing grantId/scope/personalServerUrl\",\n {\n requestId,\n status: status.status,\n hasPersonalServerUrl: Boolean(status.personalServerUrl),\n hasGrantId: Boolean(status.grantId),\n hasScope: approvedScopes(status).length > 0,\n },\n );\n }\n return status;\n }\n\n /** Approved scopes in approval order, falling back to the single `scope`. */\n function approvedScopes(status: AccessRequestStatus): string[] {\n if (status.scopes && status.scopes.length > 0) return status.scopes;\n return status.scope ? [status.scope] : [];\n }\n\n /**\n * Resolve which scope to read. Rejects an unapproved scope up front so it\n * never reaches the Personal Server and never settles a fee.\n */\n function resolveRequestedScope(\n status: AccessRequestStatus,\n requested?: string,\n ): string {\n const scopes = approvedScopes(status);\n if (requested === undefined) return scopes[0];\n if (!scopes.includes(requested)) {\n throw new ScopeNotApprovedError(\n `Scope \"${requested}\" is not approved on this request`,\n { requestedScope: requested, approvedScopes: scopes },\n );\n }\n return requested;\n }\n\n async function readScope<T>(\n status: AccessRequestStatus,\n scope: string,\n ): Promise<ApprovedDataResult<T>> {\n const result = await readPersonalServerData({\n personalServerUrl: status.personalServerUrl as string,\n scope,\n grantId: status.grantId as string,\n payerAddress: account.address,\n signMessage,\n escrow,\n fetchFn: config.personalServerFetch,\n transportRetry: config.personalServerTransportRetry,\n });\n return { scope, data: result.data as T, payment: result.payment };\n }\n\n async function acknowledgeQuietly(requestId: string): Promise<void> {\n try {\n await accessRequestClient.acknowledgeRead?.(requestId);\n } catch {\n // The read already succeeded; ack only drives Vana Web completion UX.\n }\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/errors.cjs
CHANGED
|
@@ -25,6 +25,7 @@ __export(errors_exports, {
|
|
|
25
25
|
DataPointVersionConflictError: () => DataPointVersionConflictError,
|
|
26
26
|
DerivativeComputeUnavailableError: () => DerivativeComputeUnavailableError,
|
|
27
27
|
DerivativeCycleError: () => DerivativeCycleError,
|
|
28
|
+
DerivativeDerivedScopeRequiredError: () => DerivativeDerivedScopeRequiredError,
|
|
28
29
|
DerivativeQuestionFailedError: () => DerivativeQuestionFailedError,
|
|
29
30
|
DerivativeQuestionInvalidError: () => DerivativeQuestionInvalidError,
|
|
30
31
|
DerivativeQuestionNotFoundError: () => DerivativeQuestionNotFoundError,
|
|
@@ -282,6 +283,17 @@ class DerivativeQuestionNotFoundError extends PersonalServerWriteError {
|
|
|
282
283
|
super(message, "DERIVATIVE_QUESTION_NOT_FOUND", 404, errorCode, details);
|
|
283
284
|
}
|
|
284
285
|
}
|
|
286
|
+
class DerivativeDerivedScopeRequiredError extends PersonalServerWriteError {
|
|
287
|
+
constructor(message, errorCode = null, details) {
|
|
288
|
+
super(
|
|
289
|
+
message,
|
|
290
|
+
"DERIVATIVE_DERIVED_SCOPE_REQUIRED",
|
|
291
|
+
400,
|
|
292
|
+
errorCode,
|
|
293
|
+
details
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
285
297
|
class DerivativeSourceNotGrantedError extends PersonalServerWriteError {
|
|
286
298
|
constructor(message, errorCode = null, details) {
|
|
287
299
|
super(message, "DERIVATIVE_SOURCE_NOT_GRANTED", 403, errorCode, details);
|
|
@@ -337,6 +349,7 @@ class DataPointVersionConflictError extends VanaError {
|
|
|
337
349
|
DataPointVersionConflictError,
|
|
338
350
|
DerivativeComputeUnavailableError,
|
|
339
351
|
DerivativeCycleError,
|
|
352
|
+
DerivativeDerivedScopeRequiredError,
|
|
340
353
|
DerivativeQuestionFailedError,
|
|
341
354
|
DerivativeQuestionInvalidError,
|
|
342
355
|
DerivativeQuestionNotFoundError,
|