@parity/product-sdk-terminal 0.0.0-dev.312.0 → 0.0.0-dev.454.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 CHANGED
@@ -50,7 +50,7 @@ const sessions = await waitForSessions(adapter, 2000);
50
50
  // 5. Sign messages via the paired wallet
51
51
  if (sessions.length > 0) {
52
52
  const session = sessions[0];
53
- const signer = createSessionSigner(session, adapter);
53
+ const signer = await createSessionSigner(session, adapter);
54
54
  // use signer with polkadot-api transactions
55
55
  }
56
56
  ```
@@ -63,7 +63,7 @@ Creates a terminal adapter backed by the host-papp SDK.
63
63
 
64
64
  **Options:**
65
65
  - `appId` -- unique app identifier (used as storage namespace)
66
- - `endpoints?` -- statement store WebSocket endpoints (defaults to Paseo)
66
+ - `endpoints?` -- statement store WebSocket endpoints (defaults to `StatementStoreNetworks.paseo`)
67
67
  - `hostMetadata?` -- optional host environment info
68
68
  - `storageDir?` -- override the on-disk session directory (defaults to `~/.polkadot-apps/`). Useful in tests and containerised environments.
69
69
 
@@ -71,34 +71,78 @@ Creates a terminal adapter backed by the host-papp SDK.
71
71
  - `appId` -- the value you passed in (re-exposed so `createSessionSigner` can pull the productId from the adapter)
72
72
  - `sso` -- auth component (`.authenticate()`, `.abortAuthentication()`, status subscriptions)
73
73
  - `sessions` -- session manager (signing, disconnect)
74
- - `destroy()` -- disconnect the WebSocket and release resources. Idempotent. Suppresses `@novasamatech/statement-store`'s noisy `Statement subscription error` log for ~50 ms after the call.
74
+ - `destroy()` -- disconnect the WebSocket and release resources. Idempotent. Drops `@novasamatech/statement-store`'s benign `Statement subscription error: Client destroyed` line, which it emits synchronously while disconnecting a live subscription — only that line; every other `console.error` passes through.
75
75
 
76
- ### `createSessionSigner(session, adapter): PolkadotSigner`
76
+ ### `StatementStoreNetworks`
77
+
78
+ The statement store endpoints terminal can pair against, keyed by network.
79
+
80
+ | Key | Network | Endpoint |
81
+ | --- | --- | --- |
82
+ | `paseo` | Paseo Next v2 | `wss://paseo-people-next-system-rpc.polkadot.io` |
83
+ | `previewnet` | zombienet, a step ahead of paseo | whatever host-papp's `SS_PREVIEW_STAGE_ENDPOINTS` resolves to, today `wss://previewnet.substrate.dev/people` |
84
+
85
+ Keyed the same way as `BULLETIN_RPCS` in `@parity/product-sdk-host`. There is deliberately no
86
+ `devnet` key: that people chain has a statement store, but no endpoint for it is published here, and
87
+ no phone build pairs against it. Pass `endpoints` explicitly to reach one.
88
+
89
+ Pairing only works when the terminal and the phone are on the **same** people chain, and the phone
90
+ picks its chain per build flavour. The Polkadot app's nightly build uses `paseo`, which is why that
91
+ is the default; a preview-flavour phone needs `endpoints: StatementStoreNetworks.previewnet`.
92
+
93
+ `SS_STABLE_STAGE_ENDPOINTS` is also exported. It is internal to the Parity network and does not
94
+ answer from outside it.
95
+
96
+ ### `isBenignTeardownError(error): boolean`
97
+
98
+ Whether `error` is the benign teardown noise above (`Client destroyed`, matched on the message so it never swallows another package's `DestroyedError`). `destroy()` already drops it during its own teardown; export it so a consumer with its own `console.error` guard can drop the same line without reinventing the match.
99
+
100
+ ### `createSessionSigner(session, adapter, publicKey?, options?): Promise<PolkadotSigner>`
77
101
 
78
102
  Creates a `PolkadotSigner` backed by a QR-paired mobile wallet session, using the session's **default account** (`derivationIndex: 0`) under the adapter's `appId`. This is the right entry point for ~all CLI flows.
79
103
 
104
+ Async because the signer needs the product's subtree public key (RFC-0022), which only the paired wallet holds. That is one consent-free round trip the first time a product is seen; afterwards it is served from a cache on disk.
105
+
80
106
  ```ts
81
107
  const [session] = adapter.sessions.sessions.read();
82
- const signer = createSessionSigner(session, adapter);
108
+ const signer = await createSessionSigner(session, adapter);
83
109
  await contract.publish.tx(domain, cid, { signer, origin });
84
110
  ```
85
111
 
86
- ### `createSessionSignerForAccount(session, ref): PolkadotSigner`
112
+ ### `createSessionSignerForAccount(session, ref, options?): Promise<PolkadotSigner>`
87
113
 
88
114
  Escape hatch for signing as a non-default sub-account of a paired session, or as a `productId` that differs from the adapter's `appId`. Most callers don't need this.
89
115
 
90
116
  `ref` is `{ productId: string; derivationIndex: number }`:
91
117
  - `productId` -- dotNS-style identifier of the requesting product. In normal usage this equals the adapter's `appId`; pass a different value only if you have an explicit reason.
92
- - `derivationIndex` -- BIP32-style child-key index. `0` is the default account; non-zero indices reach additional sub-accounts derived from the same root.
118
+ - `derivationIndex` -- child-key index within the product's subtree. `0` is the default account; non-zero indices reach additional sub-accounts of the same product.
93
119
 
94
120
  ```ts
95
- const subSigner = createSessionSignerForAccount(session, {
121
+ const subSigner = await createSessionSignerForAccount(session, {
96
122
  productId: "my-product",
97
123
  derivationIndex: 3,
98
124
  });
99
125
  ```
100
126
 
101
- > **Wire format note:** `@novasamatech/host-papp` 0.7 expects `productAccountId: [productId, derivationIndex]` in `SigningRawRequest`. Both functions above hide that tuple — pass an adapter for the default case or a named-fields object for the escape hatch.
127
+ Both factories accept `SessionSignerOptions`, extending `ProductSubtreeOptions` with optional `txExtVersion`. It defaults to the lowest pipeline version the chain metadata declares that holds every supplied extension, which is `0` on a chain with one pipeline. To force a version, pass `{ txExtVersion: 1 }` as the final `options` argument. The extension bytes supplied to `signTx` must match that version. PAPI 2.1.6's standard transaction builder encodes version `0`; this option does not change its encoder.
128
+
129
+ > **Wire format note:** `@novasamatech/host-papp` expects `productAccountId: [productId, { tag: "Index", value: derivationIndex }]` in `SigningRawRequest`. Both functions above hide that tuple — pass an adapter for the default case or a named-fields object for the escape hatch.
130
+
131
+ ### `getProductSubtreePublicKey(session, productId, options?): Promise<Uint8Array>`
132
+
133
+ Fetches the sr25519 public key of `//product//{productId}` from the paired wallet (RFC-0022). Both signer factories call it for you; reach for it directly to warm the cache before going offline, or to derive a product address without building a signer.
134
+
135
+ The request is consent-free, so it raises no prompt on the phone, but it does need the phone reachable. The result is cached in memory and on disk, so only a cold cache reaches the wallet.
136
+
137
+ ```ts
138
+ await getProductSubtreePublicKey(session, adapter.appId, { appId: adapter.appId });
139
+ ```
140
+
141
+ `options` is `ProductSubtreeOptions`:
142
+ - `appId` -- names the cache file (`~/.polkadot-apps/${appId}_ProductSubtrees.json`). Defaults to `productId`. **Pass your app id.** Anything that sweeps `~/.polkadot-apps/` by app-id prefix, such as a sign-out that clears local state, only finds the file if it is named after the app.
143
+ - `storageDir` -- overrides `~/.polkadot-apps/`, matching `createTerminalAdapter({ storageDir })`.
144
+
145
+ The cache file is written with mode `0600` and keyed by session and product, so re-pairing or switching wallet account does not reuse a stale key.
102
146
 
103
147
  ### `renderQrCode(data, options?): Promise<string>`
104
148
 
@@ -112,6 +156,19 @@ File-based storage adapter for Node.js. Data persists in `storageDir` (defaults
112
156
 
113
157
  Waits for the session list to emit at least one entry, or resolves with `[]` after `timeoutMs`.
114
158
 
159
+ ### `deriveEntropy(session, productId, key): Uint8Array`
160
+
161
+ Derives 32 bytes of deterministic entropy from a paired session, scoped to a product and a caller key (RFC-0007, host-spec §C.8). The bytes match what an in-container app gets from `@parity/product-sdk-host`'s `deriveEntropy` for the same wallet + product + key, so keys derived from them interoperate across web and terminal clients. Computed locally from the session's `rootEntropySource` — no round trip to the wallet.
162
+
163
+ ```ts
164
+ const entropy = deriveEntropy(session, "my-cli.dot", new TextEncoder().encode("repo-key"));
165
+ ```
166
+
167
+ - `productId` -- the identifier the in-container deployment is served under: the same value you pass as `productId` to `getBulletinSigner`, **not** `adapter.appId`. Used verbatim and case-sensitively, and scoped per deployment — production, PR previews and `localhost:<port>` all derive different entropy.
168
+ - `key` -- caller key, 1..32 bytes.
169
+
170
+ Throws if the session predates RFC-0007 (no `rootEntropySource`; re-pair to fix). With several paired sessions, pin the intended wallet with `sessionRootPublicKey` first: the wrong session yields valid entropy for a different wallet. The result is raw key material — don't log it or persist it unwrapped.
171
+
115
172
  ## Allowance signers — the canonical path
116
173
 
117
174
  For CLIs that need to write to Bulletin or publish to the Statement Store: ask the paired wallet for an allowance slot, get a `PolkadotSigner` back, sign extrinsics with it. This is what most consumers want.
@@ -238,20 +295,22 @@ For consumers moving from `@polkadot-apps/terminal` v0.2.0 / v0.3.0. Existing se
238
295
  | Package name | `@polkadot-apps/terminal` | `@parity/product-sdk-terminal` |
239
296
  | WASM loader hook | `--import @polkadot-apps/terminal/register` (required) | **removed** — no `--import` needed (host-papp ≥0.7.9 dropped `verifiablejs`) |
240
297
  | `createTerminalAdapter` | `async` — returned `Promise<TerminalAdapter>` | **sync** — returns `TerminalAdapter` directly. Drop the `await`. |
241
- | Default account signer | `createSessionSigner(session)` | `createSessionSigner(session, adapter)` — pass the adapter as second arg |
242
- | Non-default sub-account signer | not exposed | `createSessionSignerForAccount(session, { productId, derivationIndex })` |
298
+ | Default account signer | `createSessionSigner(session)` | `await createSessionSigner(session, adapter)` — pass the adapter as second arg, and await it |
299
+ | Non-default sub-account signer | not exposed | `await createSessionSignerForAccount(session, { productId, derivationIndex })` |
243
300
  | Override session storage dir | not supported (hard-coded `~/.polkadot-apps/`) | `createTerminalAdapter({ ..., storageDir })` option |
244
301
  | E2E test helper for sessions | none | `createTestSession` from `@parity/product-sdk-terminal/testing` |
245
302
  | Node version | any (bundled `ws`) | **≥21** (uses global `WebSocket`) |
246
- | `destroy()` shutdown noise | emitted `Statement subscription error` to stderr | suppressed; `console.error` muted for ~50 ms |
303
+ | `destroy()` shutdown noise | emitted `Statement subscription error` to stderr | only the benign `Client destroyed` line dropped (synchronously, during disconnect); other `console.error` untouched |
247
304
 
248
305
  ### Why the signer API changed
249
306
 
250
- `@novasamatech/host-papp` 0.7 replaced `SigningRawRequest.address` with `productAccountId: [productId, derivationIndex]`. The wire format requires both fields, so a session-only argument is no longer enough — the signer needs to know *which sub-account of which product is asking*. We split that into two functions to keep the common case ergonomic:
307
+ `@novasamatech/host-papp` 0.7 replaced `SigningRawRequest.address` with `productAccountId: [productId, derivationIndex]`, and 0.10 made the index a tagged `Index | Raw` selector. The wire format requires both fields, so a session-only argument is no longer enough — the signer needs to know *which sub-account of which product is asking*. We split that into two functions to keep the common case ergonomic:
251
308
 
252
- - `createSessionSigner(session, adapter)` for the default account (uses `[adapter.appId, 0]`)
309
+ - `createSessionSigner(session, adapter)` for the default account (uses `[adapter.appId, { tag: "Index", value: 0 }]`)
253
310
  - `createSessionSignerForAccount(session, { productId, derivationIndex })` for everything else
254
311
 
312
+ Both return a promise since RFC-0022: a product account hangs off `//product//{productId}`, whose two hard junctions no public key can cross, so the subtree key has to be fetched from the paired wallet once per product.
313
+
255
314
  The single-argument `createSessionSigner(session)` from `@polkadot-apps/terminal` no longer works against host-papp 0.7 regardless of which package you use.
256
315
 
257
316
  ### Migration steps
@@ -259,7 +318,7 @@ The single-argument `createSessionSigner(session)` from `@polkadot-apps/terminal
259
318
  1. **Replace the dep**: `pnpm remove @polkadot-apps/terminal && pnpm add @parity/product-sdk-terminal`
260
319
  2. **Remove the `--import @.../register` flag** from your `node` / `tsx` invocations or `package.json` scripts — the WASM loader hook no longer exists (and is no longer needed).
261
320
  3. **Drop `await`** in front of `createTerminalAdapter(...)` calls.
262
- 4. **Update each `createSessionSigner` call site**: change `createSessionSigner(session)` → `createSessionSigner(session, adapter)`.
321
+ 4. **Update each `createSessionSigner` call site**: change `createSessionSigner(session)` → `await createSessionSigner(session, adapter)`.
263
322
  5. **Verify Node version** is ≥21 (`node --version`).
264
323
 
265
324
  If your existing sessions don't appear after migrating, double-check that the `appId` is identical to what you used in `@polkadot-apps/terminal` — the on-disk file names depend on it.
@@ -300,7 +359,7 @@ const sessions = await waitForSessions(adapter);
300
359
 
301
360
  After login and attestation, the paired wallet can sign both transactions and raw messages via the statement store. The `PolkadotSigner` returned by `createSessionSigner` routes each path to the right host-papp method:
302
361
 
303
- - **Transactions** (`signTx` from polkadot-api's perspective — what `submitAndWatch`, `signSubmitAndWatch`, contract method calls, etc. invoke) go through `session.signRaw` with the `Payload` tag. polkadot-api assembles the full SCALE-encoded signing payload from runtime metadata — `callData ‖ extras ‖ additionalSigneds` for every signed extension the chain declares — and hands the bytes to the wallet as an opaque hex blob. The wallet signs the payload as-is, with no envelope wrapping. Any signed extension declared by the runtime (including extensions polkadot-api doesn't know about, e.g. `AsPgas` on Paseo Next v2) survives end-to-end because the wallet doesn't inspect the bytes — it just signs them.
362
+ - **Transactions** (`signTx`, used by `submitAndWatch`, `signSubmitAndWatch`, and contract calls) go through `session.createTransaction`. The signer forwards the account, call data, `txExtVersion`, and every extension's encoded `extra` and `additionalSigned` bytes, including unknown extensions such as `AsPgas`. The wallet decodes the transaction for display and returns the signed extrinsic.
304
363
  - **Raw bytes** (`signBytes`) go through `session.signRaw` with the `Bytes` tag. Mobile applies the standard `<Bytes>...</Bytes>` anti-phishing wrap before signing — appropriate for arbitrary data, the same behavior `signRaw` has across all Polkadot wallets.
305
364
 
306
365
  > **Note on previous bugs.** Versions prior to `0.1.1` routed *all* signing through `signRaw` with no tag distinction, producing `BadProof`-rejected signatures. Versions `0.1.1` through `0.2.x` then used `polkadot-api/pjs-signer`, which threw `PJS does not support this signed-extension: <name>` on any extension outside its eight built-in mappers (notably `AsPgas`). The current path bypasses both pitfalls.
@@ -0,0 +1,267 @@
1
+ import { createLogger } from '@parity/product-sdk-logger';
2
+ import { fromHex, toHex } from '@polkadot-api/utils';
3
+ import { readFile, mkdir, writeFile, rename } from 'fs/promises';
4
+ import { homedir } from 'os';
5
+ import { join, dirname } from 'path';
6
+ import { numberToBytesLE, bytesToNumberLE } from '@noble/curves/utils.js';
7
+ import { createDerive, sr25519Derive, sr25519 } from '@polkadot-labs/hdkd-helpers';
8
+ import { getPolkadotSigner } from 'polkadot-api/signer';
9
+
10
+ // src/host.ts
11
+ var log = createLogger("terminal");
12
+ var DEFAULT_STORAGE_DIR = join(homedir(), ".polkadot-apps");
13
+ var CACHE_FILE_MODE = 384;
14
+ function sanitizeAppId(appId) {
15
+ return appId.replace(/[^a-zA-Z0-9_.-]/g, "_");
16
+ }
17
+ function cacheFilePath(appId, kind, storageDir) {
18
+ return join(storageDir ?? DEFAULT_STORAGE_DIR, `${sanitizeAppId(appId)}_${kind}.json`);
19
+ }
20
+ async function loadJsonCache(path, version, label) {
21
+ let raw;
22
+ try {
23
+ raw = await readFile(path, "utf-8");
24
+ } catch (e) {
25
+ if (e?.code === "ENOENT") return null;
26
+ throw e;
27
+ }
28
+ try {
29
+ const parsed = JSON.parse(raw);
30
+ if (parsed?.version !== version) {
31
+ log.warn(`${label} schema mismatch; starting fresh`, { path });
32
+ return null;
33
+ }
34
+ return parsed;
35
+ } catch (e) {
36
+ log.warn(`${label} parse failed; starting fresh`, { path, error: String(e) });
37
+ return null;
38
+ }
39
+ }
40
+ async function saveJsonCache(path, value) {
41
+ await mkdir(dirname(path), { recursive: true });
42
+ const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
43
+ await writeFile(tmp, JSON.stringify(value, null, 2), { mode: CACHE_FILE_MODE });
44
+ await rename(tmp, path);
45
+ }
46
+ var locks = /* @__PURE__ */ new Map();
47
+ function withFileLock(path, fn) {
48
+ const prev = locks.get(path) ?? Promise.resolve();
49
+ const next = prev.catch(() => {
50
+ }).then(fn);
51
+ locks.set(
52
+ path,
53
+ next.catch(() => {
54
+ })
55
+ );
56
+ return next;
57
+ }
58
+
59
+ // src/host-cache.ts
60
+ function cachePath(appId, storageDir) {
61
+ return cacheFilePath(appId, "AllowanceKeys", storageDir);
62
+ }
63
+ function emptyCache() {
64
+ return { version: 2, entries: {} };
65
+ }
66
+ async function loadCache(appId, storageDir) {
67
+ const path = cachePath(appId, storageDir);
68
+ const cache = await loadJsonCache(path, 2, "allowance cache");
69
+ if (!cache || typeof cache.entries !== "object" || cache.entries === null) {
70
+ return emptyCache();
71
+ }
72
+ return cache;
73
+ }
74
+ async function saveCache(appId, cache, storageDir) {
75
+ await saveJsonCache(cachePath(appId, storageDir), cache);
76
+ }
77
+ function smartContractDest(dest) {
78
+ return dest.tag === "Index" ? `Index::${dest.value}` : `Raw::${toHex(dest.value)}`;
79
+ }
80
+ function cacheKey(resource) {
81
+ if (resource.tag === "SmartContractAllowance") {
82
+ return `${resource.tag}::${smartContractDest(resource.value)}`;
83
+ }
84
+ return resource.tag;
85
+ }
86
+ function pickOnExistingPolicy(cache, resources) {
87
+ if (resources.length === 0) return "Ignore";
88
+ const allSlotTableAndCached = resources.every((r) => {
89
+ if (r.tag !== "BulletInAllowance" && r.tag !== "StatementStoreAllowance") return false;
90
+ return Object.hasOwn(cache.entries, cacheKey(r));
91
+ });
92
+ return allSlotTableAndCached ? "Increase" : "Ignore";
93
+ }
94
+ function mergeOutcomes(cache, requested, outcomes) {
95
+ if (requested.length !== outcomes.length) {
96
+ throw new Error(
97
+ `mergeOutcomes: length mismatch \u2014 requested ${requested.length}, got ${outcomes.length}`
98
+ );
99
+ }
100
+ const entries = { ...cache.entries };
101
+ let mutated = false;
102
+ for (let i = 0; i < outcomes.length; i++) {
103
+ const req = requested[i];
104
+ const out = outcomes[i];
105
+ if (out.tag !== "Allocated") continue;
106
+ const inner = out.value;
107
+ if (inner.tag !== req.tag) {
108
+ throw new Error(
109
+ `mergeOutcomes: variant mismatch at index ${i} \u2014 requested ${req.tag}, got Allocated<${inner.tag}>`
110
+ );
111
+ }
112
+ const key = cacheKey(req);
113
+ switch (inner.tag) {
114
+ case "BulletInAllowance":
115
+ case "StatementStoreAllowance":
116
+ entries[key] = {
117
+ tag: inner.tag,
118
+ slotAccountKey: toHex(inner.value.slotAccountKey)
119
+ };
120
+ mutated = true;
121
+ break;
122
+ case "SmartContractAllowance":
123
+ if (req.tag === "SmartContractAllowance") {
124
+ entries[key] = {
125
+ tag: "SmartContractAllowance",
126
+ dest: smartContractDest(req.value)
127
+ };
128
+ mutated = true;
129
+ }
130
+ break;
131
+ case "AutoSigning":
132
+ entries[key] = {
133
+ tag: "AutoSigning",
134
+ ringVrfDomainEntropy: toHex(inner.value.ringVrfDomainEntropy),
135
+ productRootPrivateKey: toHex(inner.value.productRootPrivateKey)
136
+ };
137
+ mutated = true;
138
+ break;
139
+ }
140
+ }
141
+ return mutated ? { version: 2, entries } : cache;
142
+ }
143
+ function readCacheEntry(cache, resource) {
144
+ return cache.entries[cacheKey(resource)] ?? null;
145
+ }
146
+ function withCacheLock(appId, storageDir, fn) {
147
+ return withFileLock(cachePath(appId, storageDir), fn);
148
+ }
149
+ function canonicalSr25519SecretToEd25519Bytes(secret) {
150
+ const ed25519Scalar = numberToBytesLE(bytesToNumberLE(secret.subarray(0, 32)) << 3n, 32);
151
+ const out = new Uint8Array(64);
152
+ out.set(ed25519Scalar, 0);
153
+ out.set(secret.subarray(32, 64), 32);
154
+ return out;
155
+ }
156
+ function buildKeypair(secret) {
157
+ if (secret.length === 32) {
158
+ const derive = createDerive({ seed: secret, curve: sr25519, derive: sr25519Derive });
159
+ const keypair = derive("");
160
+ return { publicKey: keypair.publicKey, sign: (data) => keypair.sign(data) };
161
+ }
162
+ if (secret.length === 64) {
163
+ const ed25519Secret = canonicalSr25519SecretToEd25519Bytes(secret);
164
+ return {
165
+ publicKey: sr25519.getPublicKey(ed25519Secret),
166
+ sign: (data) => sr25519.sign(data, ed25519Secret)
167
+ };
168
+ }
169
+ throw new Error(
170
+ `createSlotAccountSigner: unexpected slotAccountKey length ${secret.length}. Expected 32 (mini-secret) or 64 (expanded secret).`
171
+ );
172
+ }
173
+ function buildSignerFromEntry(entry) {
174
+ if (entry.tag !== "BulletInAllowance" && entry.tag !== "StatementStoreAllowance") {
175
+ throw new Error(
176
+ `createSlotAccountSigner: ${entry.tag} does not carry a slot account key. Slot-table signing is defined for BulletInAllowance and StatementStoreAllowance only.`
177
+ );
178
+ }
179
+ const secret = fromHex(entry.slotAccountKey);
180
+ const { publicKey, sign } = buildKeypair(secret);
181
+ return getPolkadotSigner(publicKey, "Sr25519", async (data) => sign(data));
182
+ }
183
+ async function createSlotAccountSigner(adapter, resource, productId) {
184
+ const cache = await loadCache(productId ?? adapter.appId, adapter.storageDir);
185
+ const entry = readCacheEntry(cache, resource);
186
+ if (!entry) return null;
187
+ return buildSignerFromEntry(entry);
188
+ }
189
+
190
+ // src/host.ts
191
+ var log2 = createLogger("terminal");
192
+ async function requestResourceAllocation(session, adapter, resources, options = {}) {
193
+ const productId = options.productId ?? adapter.appId;
194
+ return withCacheLock(productId, adapter.storageDir, async () => {
195
+ const cache = await loadCache(productId, adapter.storageDir);
196
+ const onExisting = options.onExisting ?? pickOnExistingPolicy(cache, resources);
197
+ log2.debug("requestResourceAllocation", {
198
+ productId,
199
+ resources: resources.map((r) => r.tag),
200
+ onExisting,
201
+ autoPolicy: options.onExisting === void 0
202
+ });
203
+ const outcomes = await sendResourceAllocation(session, productId, resources, onExisting);
204
+ const next = mergeOutcomes(cache, resources, outcomes);
205
+ if (next !== cache) {
206
+ await saveCache(productId, next, adapter.storageDir);
207
+ }
208
+ return outcomes;
209
+ });
210
+ }
211
+ async function sendResourceAllocation(session, productId, resources, onExisting) {
212
+ const result = await session.requestResourceAllocation({
213
+ callingProductId: productId,
214
+ resources,
215
+ onExisting
216
+ });
217
+ if (result.isErr()) {
218
+ throw new Error(`requestResourceAllocation failed: ${result.error.message}`);
219
+ }
220
+ return result.value;
221
+ }
222
+ async function getCachedAllocation(adapter, resource, productId) {
223
+ const cache = await loadCache(productId ?? adapter.appId, adapter.storageDir);
224
+ return readCacheEntry(cache, resource);
225
+ }
226
+ async function ensureSlotAccountSigner(session, adapter, resource, productId) {
227
+ const resolvedProductId = productId ?? adapter.appId;
228
+ return withCacheLock(resolvedProductId, adapter.storageDir, async () => {
229
+ const cache = await loadCache(resolvedProductId, adapter.storageDir);
230
+ const hit = readCacheEntry(cache, resource);
231
+ if (hit) return buildSignerFromEntry(hit);
232
+ log2.debug("ensureSlotAccountSigner: cache miss, allocating", { resource: resource.tag });
233
+ const onExisting = pickOnExistingPolicy(cache, [resource]);
234
+ const outcomes = await sendResourceAllocation(
235
+ session,
236
+ resolvedProductId,
237
+ [resource],
238
+ onExisting
239
+ );
240
+ const next = mergeOutcomes(cache, [resource], outcomes);
241
+ if (next !== cache) {
242
+ await saveCache(resolvedProductId, next, adapter.storageDir);
243
+ }
244
+ const outcome = outcomes[0];
245
+ if (!outcome) {
246
+ throw new Error(
247
+ `ensureSlotAccountSigner: protocol violation \u2014 empty outcomes for ${resource.tag}`
248
+ );
249
+ }
250
+ if (outcome.tag !== "Allocated") {
251
+ throw new Error(
252
+ `ensureSlotAccountSigner: allocation ${outcome.tag} for ${resource.tag}`
253
+ );
254
+ }
255
+ const freshEntry = readCacheEntry(next, resource);
256
+ if (!freshEntry) {
257
+ throw new Error(
258
+ `ensureSlotAccountSigner: allocation succeeded but cache lookup returned null for ${resource.tag}`
259
+ );
260
+ }
261
+ return buildSignerFromEntry(freshEntry);
262
+ });
263
+ }
264
+
265
+ export { cacheFilePath, createSlotAccountSigner, ensureSlotAccountSigner, getCachedAllocation, loadJsonCache, requestResourceAllocation, saveJsonCache, sendResourceAllocation, withFileLock };
266
+ //# sourceMappingURL=chunk-EORPX5XL.js.map
267
+ //# sourceMappingURL=chunk-EORPX5XL.js.map