@parity/product-sdk-terminal 0.1.0 → 0.2.1
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 +5 -4
- package/dist/index.d.ts +54 -9
- package/dist/index.js +416 -57
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -192,18 +192,19 @@ const sessions = await waitForSessions(adapter);
|
|
|
192
192
|
|
|
193
193
|
**Limits and usage notes.**
|
|
194
194
|
|
|
195
|
-
- **Signing does not round-trip.** `session.signRaw`
|
|
195
|
+
- **Signing does not round-trip.** Both `session.signRaw` and `session.signPayload` go out over the statement store and expect a real phone to respond. Use this helper for flows that test session discovery, persistence, and logout — not happy-path signing.
|
|
196
196
|
- **Expiry tests still work.** The synthesized local account was never registered on the People chain, so any statement-store write from this session fails with `NoAllowanceError`. That's the same error the CLI sees when a previously valid session's on-chain attestation has expired.
|
|
197
197
|
- **No `expiresAt` option.** The on-disk codec has no expiry field; validity lives on chain.
|
|
198
198
|
- **Corrupted-session cases** don't need a helper — `fs.writeFile("<storageDir>/<appId>_SsoSessions.json", "not-hex")` from the test is enough.
|
|
199
199
|
|
|
200
200
|
## Signing
|
|
201
201
|
|
|
202
|
-
After login and attestation, the paired wallet can sign messages via the statement store.
|
|
202
|
+
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:
|
|
203
203
|
|
|
204
|
-
|
|
204
|
+
- **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.
|
|
205
|
+
- **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.
|
|
205
206
|
|
|
206
|
-
|
|
207
|
+
> **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.
|
|
207
208
|
|
|
208
209
|
## Notes
|
|
209
210
|
|
package/dist/index.d.ts
CHANGED
|
@@ -46,19 +46,64 @@ type TerminalAdapter = PappAdapter & {
|
|
|
46
46
|
* Disconnect the WebSocket and release resources.
|
|
47
47
|
*
|
|
48
48
|
* @remarks
|
|
49
|
-
* Idempotent.
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
49
|
+
* Idempotent. Returns a Promise that resolves once all in-flight
|
|
50
|
+
* statement-subscription teardowns have settled and the underlying
|
|
51
|
+
* substrate client has been disconnected. **Awaiting is recommended
|
|
52
|
+
* but not required** — callers that don't await get the same
|
|
53
|
+
* fire-and-forget shape the previous version had, but they may see
|
|
54
|
+
* the destroy-time RPC traffic finish after the function returns.
|
|
55
|
+
*
|
|
56
|
+
* The implementation tracks the server-side `statement_unsubscribe…`
|
|
57
|
+
* RPCs `sessions.dispose()` fires, then awaits them via
|
|
58
|
+
* `Promise.allSettled` before destroying the substrate-client request
|
|
59
|
+
* queue. No timing-based guesses; no global-state mutations of
|
|
60
|
+
* `console.error` or `process.on('unhandledRejection')`. Pending
|
|
61
|
+
* subscribes (where `onSuccess` hasn't fired yet) are cancelled by
|
|
62
|
+
* the underlying `getSubscribeFn` teardown via `cancelRequest()`,
|
|
63
|
+
* which is the in-band fast-path and doesn't surface as a rejection.
|
|
57
64
|
*/
|
|
58
|
-
destroy(): void
|
|
65
|
+
destroy(): Promise<void>;
|
|
59
66
|
};
|
|
60
67
|
declare function createTerminalAdapter(options: TerminalAdapterOptions): TerminalAdapter;
|
|
61
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Create a PolkadotSigner from a QR-paired session.
|
|
71
|
+
*
|
|
72
|
+
* Builds the `PolkadotSigner` via PAPI-native `getPolkadotSigner` and routes
|
|
73
|
+
* **transaction signing** through `session.signRaw` with the `Payload` tag
|
|
74
|
+
* — a tagged-bytes wire route in host-papp that signs the SCALE-encoded
|
|
75
|
+
* extrinsic payload as-is, with no `<Bytes>...</Bytes>` anti-phishing
|
|
76
|
+
* envelope. PAPI assembles the full payload (`callData ‖ extras ‖
|
|
77
|
+
* additionalSigneds`) from runtime metadata, so every signed extension
|
|
78
|
+
* declared by the chain — including extensions not known to PAPI's PJS
|
|
79
|
+
* adapter (e.g. `AsPgas` on Paseo Next v2) — survives end-to-end as opaque
|
|
80
|
+
* bytes that the wallet just signs.
|
|
81
|
+
*
|
|
82
|
+
* **Raw-message signing** (`signBytes`) keeps the `Bytes` tag so the wallet
|
|
83
|
+
* applies the anti-phishing wrap once on its side. Correct for arbitrary
|
|
84
|
+
* user data; wrong for extrinsic payloads (which is why the two paths split).
|
|
85
|
+
*
|
|
86
|
+
* Bypasses `getPolkadotSignerFromPjs` whose built-in mapper table covers
|
|
87
|
+
* only eight extensions and throws `PJS does not support this
|
|
88
|
+
* signed-extension: <name>` on anything else.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```ts
|
|
92
|
+
* const [session] = adapter.sessions.sessions.read();
|
|
93
|
+
*
|
|
94
|
+
* // Default account — uses [adapter.appId, 0]:
|
|
95
|
+
* const signer = createSessionSigner(session, adapter);
|
|
96
|
+
*
|
|
97
|
+
* // Non-default derivation index, or a different productId:
|
|
98
|
+
* const subSigner = createSessionSignerForAccount(session, {
|
|
99
|
+
* productId: "my-product",
|
|
100
|
+
* derivationIndex: 3,
|
|
101
|
+
* });
|
|
102
|
+
*
|
|
103
|
+
* await contract.publish.tx(domain, cid, { signer, origin });
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
|
|
62
107
|
/**
|
|
63
108
|
* Identifies which sub-account of a paired session should sign.
|
|
64
109
|
*
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,10 @@ import {
|
|
|
8
8
|
SS_STABLE_STAGE_ENDPOINTS,
|
|
9
9
|
SS_PASEO_STABLE_STAGE_ENDPOINTS
|
|
10
10
|
} from "@novasamatech/host-papp";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
createLazyClient,
|
|
13
|
+
createPapiStatementStoreAdapter
|
|
14
|
+
} from "@novasamatech/statement-store";
|
|
12
15
|
import { createLogger } from "@parity/product-sdk-logger";
|
|
13
16
|
import { getWsProvider } from "@polkadot-api/ws-provider";
|
|
14
17
|
var log = createLogger("terminal");
|
|
@@ -16,67 +19,297 @@ function createTerminalAdapter(options) {
|
|
|
16
19
|
const endpoints = options.endpoints ?? SS_PASEO_STABLE_STAGE_ENDPOINTS;
|
|
17
20
|
const storage = createNodeStorageAdapter(options.appId, options.storageDir);
|
|
18
21
|
const HEARTBEAT_NEVER_MS = 2147483647;
|
|
19
|
-
const
|
|
22
|
+
const rawLazyClient = createLazyClient(
|
|
20
23
|
getWsProvider(endpoints, { heartbeatTimeout: HEARTBEAT_NEVER_MS })
|
|
21
24
|
);
|
|
22
|
-
const
|
|
25
|
+
const trackedLazyClient = wrapLazyClient(rawLazyClient);
|
|
26
|
+
const statementStore = createPapiStatementStoreAdapter(trackedLazyClient);
|
|
23
27
|
const adapter = createPappAdapter({
|
|
24
28
|
appId: options.appId,
|
|
25
29
|
metadata: options.metadataUrl,
|
|
26
30
|
hostMetadata: options.hostMetadata,
|
|
27
31
|
adapters: {
|
|
28
32
|
storage,
|
|
29
|
-
lazyClient,
|
|
33
|
+
lazyClient: trackedLazyClient,
|
|
30
34
|
statementStore
|
|
31
35
|
}
|
|
32
36
|
});
|
|
33
|
-
let
|
|
37
|
+
let destroyPromise = null;
|
|
34
38
|
return {
|
|
35
39
|
...adapter,
|
|
36
40
|
appId: options.appId,
|
|
37
41
|
destroy() {
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
if (destroyPromise) return destroyPromise;
|
|
43
|
+
destroyPromise = teardown(adapter.sessions, trackedLazyClient);
|
|
44
|
+
return destroyPromise;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function wrapLazyClient(inner) {
|
|
49
|
+
const pendingUnsubs = /* @__PURE__ */ new Set();
|
|
50
|
+
const innerGetSubscribeFn = inner.getSubscribeFn.bind(inner);
|
|
51
|
+
return {
|
|
52
|
+
...inner,
|
|
53
|
+
getClient: inner.getClient.bind(inner),
|
|
54
|
+
getRequestFn: inner.getRequestFn.bind(inner),
|
|
55
|
+
disconnect: inner.disconnect.bind(inner),
|
|
56
|
+
getSubscribeFn() {
|
|
57
|
+
const innerSubscribe = innerGetSubscribeFn();
|
|
58
|
+
return ((method, params, onMessage, onError) => {
|
|
59
|
+
const innerTeardown = innerSubscribe(method, params, onMessage, onError);
|
|
60
|
+
return () => {
|
|
61
|
+
const tracked = new Promise((resolve) => {
|
|
62
|
+
queueMicrotask(() => queueMicrotask(resolve));
|
|
63
|
+
});
|
|
64
|
+
pendingUnsubs.add(tracked);
|
|
65
|
+
void tracked.finally(() => pendingUnsubs.delete(tracked));
|
|
66
|
+
innerTeardown();
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
async awaitPendingUnsubs() {
|
|
71
|
+
const snapshot = Array.from(pendingUnsubs);
|
|
72
|
+
await Promise.allSettled(snapshot);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
async function teardown(sessions, lazyClient) {
|
|
77
|
+
log.debug("destroying terminal adapter");
|
|
78
|
+
sessions.dispose();
|
|
79
|
+
await lazyClient.awaitPendingUnsubs();
|
|
80
|
+
try {
|
|
81
|
+
lazyClient.disconnect();
|
|
82
|
+
} catch (e) {
|
|
83
|
+
log.warn("lazyClient.disconnect threw during destroy", { error: e });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (void 0) {
|
|
87
|
+
let fakeLazyClient = function() {
|
|
88
|
+
let teardownCalls = 0;
|
|
89
|
+
let disconnectCalls = 0;
|
|
90
|
+
const fake = {
|
|
91
|
+
getClient: (() => ({})),
|
|
92
|
+
getRequestFn: (() => () => Promise.resolve()),
|
|
93
|
+
getSubscribeFn: () => ((_method, _params, _onMessage, _onError) => {
|
|
94
|
+
return () => {
|
|
95
|
+
teardownCalls += 1;
|
|
96
|
+
};
|
|
97
|
+
}),
|
|
98
|
+
disconnect: () => {
|
|
99
|
+
disconnectCalls += 1;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
return {
|
|
103
|
+
client: fake,
|
|
104
|
+
get teardownCalls() {
|
|
105
|
+
return teardownCalls;
|
|
106
|
+
},
|
|
107
|
+
get disconnectCalls() {
|
|
108
|
+
return disconnectCalls;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
fakeLazyClient2 = fakeLazyClient;
|
|
113
|
+
const { describe, test, expect, vi } = void 0;
|
|
114
|
+
describe("wrapLazyClient", () => {
|
|
115
|
+
test("passes getClient / getRequestFn / disconnect through unchanged", () => {
|
|
116
|
+
const fake = fakeLazyClient();
|
|
117
|
+
const wrapped = wrapLazyClient(fake.client);
|
|
118
|
+
wrapped.getClient();
|
|
119
|
+
wrapped.disconnect();
|
|
120
|
+
expect(fake.disconnectCalls).toBe(1);
|
|
121
|
+
expect(typeof wrapped.getRequestFn).toBe("function");
|
|
122
|
+
});
|
|
123
|
+
test("getSubscribeFn returns a wrapped subscribe whose teardown invokes the inner teardown", () => {
|
|
124
|
+
const fake = fakeLazyClient();
|
|
125
|
+
const wrapped = wrapLazyClient(fake.client);
|
|
126
|
+
const subscribe = wrapped.getSubscribeFn();
|
|
127
|
+
const teardown2 = subscribe(
|
|
128
|
+
"statement_subscribeStatement",
|
|
129
|
+
[],
|
|
130
|
+
() => {
|
|
131
|
+
},
|
|
132
|
+
() => {
|
|
133
|
+
}
|
|
134
|
+
);
|
|
135
|
+
teardown2();
|
|
136
|
+
expect(fake.teardownCalls).toBe(1);
|
|
137
|
+
});
|
|
138
|
+
test("awaitPendingUnsubs resolves after all wrapped teardowns settle", async () => {
|
|
139
|
+
const fake = fakeLazyClient();
|
|
140
|
+
const wrapped = wrapLazyClient(fake.client);
|
|
141
|
+
const subscribe = wrapped.getSubscribeFn();
|
|
142
|
+
for (let i = 0; i < 3; i++) {
|
|
143
|
+
const teardown2 = subscribe(
|
|
144
|
+
"statement_subscribeStatement",
|
|
145
|
+
[],
|
|
146
|
+
() => {
|
|
147
|
+
},
|
|
148
|
+
() => {
|
|
149
|
+
}
|
|
150
|
+
);
|
|
151
|
+
teardown2();
|
|
152
|
+
}
|
|
153
|
+
await wrapped.awaitPendingUnsubs();
|
|
154
|
+
expect(fake.teardownCalls).toBe(3);
|
|
155
|
+
});
|
|
156
|
+
test("awaitPendingUnsubs with no pending unsubs resolves immediately", async () => {
|
|
157
|
+
const fake = fakeLazyClient();
|
|
158
|
+
const wrapped = wrapLazyClient(fake.client);
|
|
159
|
+
await expect(wrapped.awaitPendingUnsubs()).resolves.toBeUndefined();
|
|
160
|
+
});
|
|
161
|
+
test("an unsubscribe whose teardown throws is still tracked and resolved", async () => {
|
|
162
|
+
const fake = {
|
|
163
|
+
getClient: (() => ({})),
|
|
164
|
+
getRequestFn: (() => () => Promise.resolve()),
|
|
165
|
+
getSubscribeFn: () => ((_method, _params, _onMessage, _onError) => {
|
|
166
|
+
return () => {
|
|
167
|
+
throw new Error("teardown boom");
|
|
168
|
+
};
|
|
169
|
+
}),
|
|
170
|
+
disconnect: () => {
|
|
45
171
|
}
|
|
46
|
-
origError.apply(console, args);
|
|
47
172
|
};
|
|
48
|
-
|
|
173
|
+
const wrapped = wrapLazyClient(fake);
|
|
174
|
+
const subscribe = wrapped.getSubscribeFn();
|
|
175
|
+
const teardown2 = subscribe(
|
|
176
|
+
"statement_subscribeStatement",
|
|
177
|
+
[],
|
|
178
|
+
() => {
|
|
179
|
+
},
|
|
180
|
+
() => {
|
|
181
|
+
}
|
|
182
|
+
);
|
|
49
183
|
try {
|
|
50
|
-
|
|
51
|
-
} catch
|
|
52
|
-
log.warn("lazyClient.disconnect threw during destroy", { error: e });
|
|
184
|
+
teardown2();
|
|
185
|
+
} catch {
|
|
53
186
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
187
|
+
await expect(wrapped.awaitPendingUnsubs()).resolves.toBeUndefined();
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
describe("teardown", () => {
|
|
191
|
+
test("orders sessions.dispose, drain, then disconnect", async () => {
|
|
192
|
+
const order = [];
|
|
193
|
+
const fake = fakeLazyClient();
|
|
194
|
+
const wrapped = wrapLazyClient(fake.client);
|
|
195
|
+
const innerDisconnect = wrapped.disconnect.bind(wrapped);
|
|
196
|
+
wrapped.disconnect = () => {
|
|
197
|
+
order.push("disconnect");
|
|
198
|
+
innerDisconnect();
|
|
199
|
+
};
|
|
200
|
+
const sessions = {
|
|
201
|
+
dispose: vi.fn(() => {
|
|
202
|
+
order.push("dispose");
|
|
203
|
+
const subscribe = wrapped.getSubscribeFn();
|
|
204
|
+
subscribe(
|
|
205
|
+
"statement_subscribeStatement",
|
|
206
|
+
[],
|
|
207
|
+
() => {
|
|
208
|
+
},
|
|
209
|
+
() => {
|
|
210
|
+
}
|
|
211
|
+
)();
|
|
212
|
+
})
|
|
213
|
+
};
|
|
214
|
+
await teardown(sessions, wrapped);
|
|
215
|
+
expect(order).toEqual(["dispose", "disconnect"]);
|
|
216
|
+
expect(sessions.dispose).toHaveBeenCalledTimes(1);
|
|
217
|
+
expect(fake.disconnectCalls).toBe(1);
|
|
218
|
+
});
|
|
219
|
+
test("disconnect runs even when there are no pending unsubs", async () => {
|
|
220
|
+
const fake = fakeLazyClient();
|
|
221
|
+
const wrapped = wrapLazyClient(fake.client);
|
|
222
|
+
const sessions = { dispose: vi.fn() };
|
|
223
|
+
await teardown(sessions, wrapped);
|
|
224
|
+
expect(fake.disconnectCalls).toBe(1);
|
|
225
|
+
});
|
|
226
|
+
test("logs a warning if disconnect throws, doesn't propagate to caller", async () => {
|
|
227
|
+
const fake = {
|
|
228
|
+
getClient: (() => ({})),
|
|
229
|
+
getRequestFn: (() => () => Promise.resolve()),
|
|
230
|
+
getSubscribeFn: () => ((_method, _params, _onMessage, _onError) => () => {
|
|
231
|
+
}),
|
|
232
|
+
disconnect: () => {
|
|
233
|
+
throw new Error("boom");
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
const wrapped = wrapLazyClient(fake);
|
|
237
|
+
const sessions = { dispose: vi.fn() };
|
|
238
|
+
await expect(teardown(sessions, wrapped)).resolves.toBeUndefined();
|
|
239
|
+
});
|
|
240
|
+
test("awaits pending unsubs before calling disconnect", async () => {
|
|
241
|
+
const fake = fakeLazyClient();
|
|
242
|
+
const wrapped = wrapLazyClient(fake.client);
|
|
243
|
+
let unsubResolved = false;
|
|
244
|
+
const innerDisconnect = wrapped.disconnect.bind(wrapped);
|
|
245
|
+
wrapped.disconnect = () => {
|
|
246
|
+
expect(unsubResolved).toBe(true);
|
|
247
|
+
innerDisconnect();
|
|
248
|
+
};
|
|
249
|
+
const sessions = {
|
|
250
|
+
dispose: () => {
|
|
251
|
+
const subscribe = wrapped.getSubscribeFn();
|
|
252
|
+
subscribe(
|
|
253
|
+
"statement_subscribeStatement",
|
|
254
|
+
[],
|
|
255
|
+
() => {
|
|
256
|
+
},
|
|
257
|
+
() => {
|
|
258
|
+
}
|
|
259
|
+
)();
|
|
260
|
+
queueMicrotask(
|
|
261
|
+
() => queueMicrotask(() => {
|
|
262
|
+
unsubResolved = true;
|
|
263
|
+
})
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
await teardown(sessions, wrapped);
|
|
268
|
+
expect(fake.disconnectCalls).toBe(1);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
59
271
|
}
|
|
272
|
+
var fakeLazyClient2;
|
|
60
273
|
|
|
61
274
|
// src/signer.ts
|
|
275
|
+
import { toHex } from "@polkadot-api/utils";
|
|
62
276
|
import { getPolkadotSigner } from "polkadot-api/signer";
|
|
277
|
+
function makeTxSignCallback(session, productAccountId) {
|
|
278
|
+
return async (toSign) => {
|
|
279
|
+
const result = await session.signRaw({
|
|
280
|
+
productAccountId,
|
|
281
|
+
data: { tag: "Payload", value: toHex(toSign) }
|
|
282
|
+
});
|
|
283
|
+
if (result.isErr()) {
|
|
284
|
+
throw new Error(`Mobile signing rejected: ${result.error.message}`);
|
|
285
|
+
}
|
|
286
|
+
return result.value.signature;
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function makeRawBytesSignCallback(session, productAccountId) {
|
|
290
|
+
return async (data) => {
|
|
291
|
+
const result = await session.signRaw({
|
|
292
|
+
productAccountId,
|
|
293
|
+
data: { tag: "Bytes", value: data }
|
|
294
|
+
});
|
|
295
|
+
if (result.isErr()) {
|
|
296
|
+
throw new Error(`Mobile signing rejected: ${result.error.message}`);
|
|
297
|
+
}
|
|
298
|
+
return result.value.signature;
|
|
299
|
+
};
|
|
300
|
+
}
|
|
63
301
|
function buildSessionSigner(session, ref) {
|
|
64
|
-
const accountId = new Uint8Array(session.remoteAccount.accountId);
|
|
65
302
|
const productAccountId = [ref.productId, ref.derivationIndex];
|
|
66
|
-
|
|
67
|
-
|
|
303
|
+
const publicKey = new Uint8Array(session.remoteAccount.accountId);
|
|
304
|
+
const base = getPolkadotSigner(
|
|
305
|
+
publicKey,
|
|
68
306
|
"Sr25519",
|
|
69
|
-
|
|
70
|
-
const result = await session.signRaw({
|
|
71
|
-
productAccountId,
|
|
72
|
-
data: { tag: "Bytes", value: data }
|
|
73
|
-
});
|
|
74
|
-
if (result.isErr()) {
|
|
75
|
-
throw new Error(`Mobile signing rejected: ${result.error.message}`);
|
|
76
|
-
}
|
|
77
|
-
return result.value.signature;
|
|
78
|
-
}
|
|
307
|
+
makeTxSignCallback(session, productAccountId)
|
|
79
308
|
);
|
|
309
|
+
return {
|
|
310
|
+
...base,
|
|
311
|
+
signBytes: makeRawBytesSignCallback(session, productAccountId)
|
|
312
|
+
};
|
|
80
313
|
}
|
|
81
314
|
function createSessionSigner(session, adapter) {
|
|
82
315
|
return buildSessionSigner(session, { productId: adapter.appId, derivationIndex: 0 });
|
|
@@ -85,10 +318,20 @@ function createSessionSignerForAccount(session, ref) {
|
|
|
85
318
|
return buildSessionSigner(session, ref);
|
|
86
319
|
}
|
|
87
320
|
if (void 0) {
|
|
88
|
-
let makeSession = function(
|
|
321
|
+
let makeSession = function(opts) {
|
|
322
|
+
const accountIdBytes = opts.accountIdBytes ?? new Array(32).fill(0).map((_, i) => i);
|
|
89
323
|
return {
|
|
90
324
|
remoteAccount: { accountId: accountIdBytes },
|
|
91
|
-
|
|
325
|
+
signPayload: vi.fn(
|
|
326
|
+
opts.signPayload ?? (async () => {
|
|
327
|
+
throw new Error("signPayload not stubbed in this test");
|
|
328
|
+
})
|
|
329
|
+
),
|
|
330
|
+
signRaw: vi.fn(
|
|
331
|
+
opts.signRaw ?? (async () => {
|
|
332
|
+
throw new Error("signRaw not stubbed in this test");
|
|
333
|
+
})
|
|
334
|
+
)
|
|
92
335
|
};
|
|
93
336
|
}, fakeAdapter = function(appId) {
|
|
94
337
|
return { appId };
|
|
@@ -96,47 +339,161 @@ if (void 0) {
|
|
|
96
339
|
makeSession2 = makeSession, fakeAdapter2 = fakeAdapter;
|
|
97
340
|
const { describe, test, expect, vi } = void 0;
|
|
98
341
|
const { ok, err } = await null;
|
|
342
|
+
const { fromHex } = await null;
|
|
99
343
|
describe("createSessionSigner", () => {
|
|
100
344
|
test("exposes Sr25519 public key matching remoteAccount.accountId", () => {
|
|
101
345
|
const bytes = Array.from({ length: 32 }, (_, i) => i);
|
|
102
346
|
const signer = createSessionSigner(
|
|
103
|
-
makeSession(
|
|
347
|
+
makeSession({ accountIdBytes: bytes }),
|
|
104
348
|
fakeAdapter("test-app")
|
|
105
349
|
);
|
|
106
350
|
expect(signer.publicKey).toEqual(new Uint8Array(bytes));
|
|
107
351
|
});
|
|
108
|
-
test("signBytes
|
|
352
|
+
test("signBytes routes through session.signRaw with the Bytes tag", async () => {
|
|
109
353
|
const sig = new Uint8Array([9, 8, 7, 6, 5, 4, 3, 2, 1]);
|
|
110
|
-
const
|
|
354
|
+
const captured = [];
|
|
355
|
+
const session = makeSession({
|
|
356
|
+
signRaw: async (req2) => {
|
|
357
|
+
captured.push(req2);
|
|
358
|
+
return ok({ signature: sig });
|
|
359
|
+
}
|
|
360
|
+
});
|
|
111
361
|
const signer = createSessionSigner(session, fakeAdapter("test-app"));
|
|
112
362
|
const out = await signer.signBytes(new Uint8Array([1, 2, 3]));
|
|
113
363
|
expect(out).toEqual(sig);
|
|
364
|
+
expect(captured).toHaveLength(1);
|
|
365
|
+
const req = captured[0];
|
|
366
|
+
expect(req.productAccountId).toEqual(["test-app", 0]);
|
|
367
|
+
expect(req.data.tag).toBe("Bytes");
|
|
368
|
+
expect(req.data.value).toEqual(new Uint8Array([1, 2, 3]));
|
|
114
369
|
});
|
|
115
|
-
test("
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
captured.push(req2);
|
|
119
|
-
return ok({ signature: new Uint8Array([1]) });
|
|
370
|
+
test("signBytes never calls session.signPayload (legacy-path regression guard)", async () => {
|
|
371
|
+
const session = makeSession({
|
|
372
|
+
signRaw: async () => ok({ signature: new Uint8Array([1]) })
|
|
120
373
|
});
|
|
121
|
-
const signer = createSessionSigner(session, fakeAdapter("
|
|
374
|
+
const signer = createSessionSigner(session, fakeAdapter("test-app"));
|
|
122
375
|
await signer.signBytes(new Uint8Array([1, 2, 3]));
|
|
123
|
-
const
|
|
124
|
-
expect(
|
|
376
|
+
const sessionWithSpies = session;
|
|
377
|
+
expect(sessionWithSpies.signPayload.mock.calls).toHaveLength(0);
|
|
378
|
+
expect(sessionWithSpies.signRaw.mock.calls).toHaveLength(1);
|
|
125
379
|
});
|
|
126
|
-
test("signBytes throws when mobile
|
|
127
|
-
const session = makeSession(
|
|
380
|
+
test("signBytes throws with a clear error when the mobile rejects", async () => {
|
|
381
|
+
const session = makeSession({
|
|
382
|
+
signRaw: async () => err({ message: "user declined" })
|
|
383
|
+
});
|
|
128
384
|
const signer = createSessionSigner(session, fakeAdapter("test-app"));
|
|
129
385
|
await expect(signer.signBytes(new Uint8Array([1]))).rejects.toThrow(
|
|
130
386
|
"Mobile signing rejected: user declined"
|
|
131
387
|
);
|
|
132
388
|
});
|
|
133
389
|
});
|
|
390
|
+
describe("makeTxSignCallback \u2014 tx signing path (the AsPgas fix)", () => {
|
|
391
|
+
test("forwards bytes-to-sign under the Payload tag with the right productAccountId", async () => {
|
|
392
|
+
const captured = [];
|
|
393
|
+
const session = makeSession({
|
|
394
|
+
signRaw: async (req2) => {
|
|
395
|
+
captured.push(req2);
|
|
396
|
+
return ok({ signature: new Uint8Array([170, 187]) });
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
const callback = makeTxSignCallback(session, ["my-app", 0]);
|
|
400
|
+
await callback(new Uint8Array([222, 173, 190, 239]));
|
|
401
|
+
expect(captured).toHaveLength(1);
|
|
402
|
+
const req = captured[0];
|
|
403
|
+
expect(req.productAccountId).toEqual(["my-app", 0]);
|
|
404
|
+
expect(req.data.tag).toBe("Payload");
|
|
405
|
+
expect(req.data.value).toBe("0xdeadbeef");
|
|
406
|
+
});
|
|
407
|
+
test("never calls session.signPayload (legacy PJS-shape regression guard)", async () => {
|
|
408
|
+
const session = makeSession({
|
|
409
|
+
signRaw: async () => ok({ signature: new Uint8Array([1]) })
|
|
410
|
+
});
|
|
411
|
+
const callback = makeTxSignCallback(session, ["my-app", 0]);
|
|
412
|
+
await callback(new Uint8Array([1, 2, 3]));
|
|
413
|
+
const sessionWithSpies = session;
|
|
414
|
+
expect(sessionWithSpies.signPayload.mock.calls).toHaveLength(0);
|
|
415
|
+
expect(sessionWithSpies.signRaw.mock.calls).toHaveLength(1);
|
|
416
|
+
});
|
|
417
|
+
test("returns the raw signature bytes from the mobile response", async () => {
|
|
418
|
+
const sig = new Uint8Array([171, 205, 239]);
|
|
419
|
+
const session = makeSession({
|
|
420
|
+
signRaw: async () => ok({ signature: sig })
|
|
421
|
+
});
|
|
422
|
+
const callback = makeTxSignCallback(session, ["my-app", 0]);
|
|
423
|
+
const out = await callback(new Uint8Array([0]));
|
|
424
|
+
expect(out).toBe(sig);
|
|
425
|
+
});
|
|
426
|
+
test("preserves arbitrary signed-extension payload bytes verbatim", async () => {
|
|
427
|
+
const customExtensionBytes = new Uint8Array([
|
|
428
|
+
// arbitrary scale-encoded extension payload — content
|
|
429
|
+
// doesn't matter, only that it round-trips exactly
|
|
430
|
+
1,
|
|
431
|
+
2,
|
|
432
|
+
3,
|
|
433
|
+
255,
|
|
434
|
+
254,
|
|
435
|
+
253,
|
|
436
|
+
66,
|
|
437
|
+
67,
|
|
438
|
+
68
|
|
439
|
+
]);
|
|
440
|
+
const captured = [];
|
|
441
|
+
const session = makeSession({
|
|
442
|
+
signRaw: async (req2) => {
|
|
443
|
+
captured.push(req2);
|
|
444
|
+
return ok({ signature: new Uint8Array([0]) });
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
const callback = makeTxSignCallback(session, ["my-app", 0]);
|
|
448
|
+
await callback(customExtensionBytes);
|
|
449
|
+
const req = captured[0];
|
|
450
|
+
expect(fromHex(req.data.value)).toEqual(customExtensionBytes);
|
|
451
|
+
});
|
|
452
|
+
test("throws a clear error when the mobile rejects", async () => {
|
|
453
|
+
const session = makeSession({
|
|
454
|
+
signRaw: async () => err({ message: "user declined" })
|
|
455
|
+
});
|
|
456
|
+
const callback = makeTxSignCallback(session, ["my-app", 0]);
|
|
457
|
+
await expect(callback(new Uint8Array([0]))).rejects.toThrow(
|
|
458
|
+
"Mobile signing rejected: user declined"
|
|
459
|
+
);
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
describe("makeRawBytesSignCallback", () => {
|
|
463
|
+
test("forwards data verbatim under the Bytes tag with the right productAccountId", async () => {
|
|
464
|
+
const captured = [];
|
|
465
|
+
const session = makeSession({
|
|
466
|
+
signRaw: async (req2) => {
|
|
467
|
+
captured.push(req2);
|
|
468
|
+
return ok({ signature: new Uint8Array([255]) });
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
const callback = makeRawBytesSignCallback(session, ["my-app", 5]);
|
|
472
|
+
await callback(new Uint8Array([222, 173, 190, 239]));
|
|
473
|
+
expect(captured).toHaveLength(1);
|
|
474
|
+
const req = captured[0];
|
|
475
|
+
expect(req.productAccountId).toEqual(["my-app", 5]);
|
|
476
|
+
expect(req.data.tag).toBe("Bytes");
|
|
477
|
+
expect(Array.from(req.data.value)).toEqual([222, 173, 190, 239]);
|
|
478
|
+
});
|
|
479
|
+
test("returns the raw signature bytes from the mobile response", async () => {
|
|
480
|
+
const sig = new Uint8Array([66]);
|
|
481
|
+
const session = makeSession({
|
|
482
|
+
signRaw: async () => ok({ signature: sig })
|
|
483
|
+
});
|
|
484
|
+
const callback = makeRawBytesSignCallback(session, ["my-app", 0]);
|
|
485
|
+
const out = await callback(new Uint8Array([0]));
|
|
486
|
+
expect(out).toBe(sig);
|
|
487
|
+
});
|
|
488
|
+
});
|
|
134
489
|
describe("createSessionSignerForAccount", () => {
|
|
135
|
-
test("forwards
|
|
490
|
+
test("forwards productAccountId from the explicit ref", async () => {
|
|
136
491
|
const captured = [];
|
|
137
|
-
const session = makeSession(
|
|
138
|
-
|
|
139
|
-
|
|
492
|
+
const session = makeSession({
|
|
493
|
+
signRaw: async (req2) => {
|
|
494
|
+
captured.push(req2);
|
|
495
|
+
return ok({ signature: new Uint8Array([42]) });
|
|
496
|
+
}
|
|
140
497
|
});
|
|
141
498
|
const signer = createSessionSignerForAccount(session, {
|
|
142
499
|
productId: "my-app",
|
|
@@ -151,9 +508,11 @@ if (void 0) {
|
|
|
151
508
|
});
|
|
152
509
|
test("supports a productId different from any adapter's appId", async () => {
|
|
153
510
|
const captured = [];
|
|
154
|
-
const session = makeSession(
|
|
155
|
-
|
|
156
|
-
|
|
511
|
+
const session = makeSession({
|
|
512
|
+
signRaw: async (req2) => {
|
|
513
|
+
captured.push(req2);
|
|
514
|
+
return ok({ signature: new Uint8Array([0]) });
|
|
515
|
+
}
|
|
157
516
|
});
|
|
158
517
|
const signer = createSessionSignerForAccount(session, {
|
|
159
518
|
productId: "external-product",
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/adapter.ts","../src/signer.ts","../src/sessions.ts","../src/qr-encode.ts"],"sourcesContent":["/**\n * Node.js adapter for the Polkadot host-papp SDK.\n *\n * Provides Node.js-compatible implementations of the SDK's storage and\n * transport layers, enabling QR login, attestation, and signing in\n * terminal/CLI environments.\n */\nimport {\n createPappAdapter,\n type PappAdapter,\n type HostMetadata,\n SS_STABLE_STAGE_ENDPOINTS,\n SS_PASEO_STABLE_STAGE_ENDPOINTS,\n} from \"@novasamatech/host-papp\";\nimport { createLazyClient, createPapiStatementStoreAdapter } from \"@novasamatech/statement-store\";\nimport { createLogger } from \"@parity/product-sdk-logger\";\nimport { getWsProvider } from \"@polkadot-api/ws-provider\";\n\nimport { createNodeStorageAdapter } from \"./node-storage.js\";\n\nconst log = createLogger(\"terminal\");\n\n/** Options for creating a terminal adapter. */\nexport interface TerminalAdapterOptions {\n /** Unique app identifier. Used as the storage namespace. */\n appId: string;\n /** URL to the app's metadata JSON (name + icon), shown during pairing. */\n metadataUrl: string;\n /** Statement store WebSocket endpoints. Defaults to Paseo stable endpoints. */\n endpoints?: string[];\n /** Optional host metadata for the Sign-In screen. */\n hostMetadata?: HostMetadata;\n /**\n * Directory where session files are persisted. Defaults to\n * `~/.polkadot-apps/`. Override in tests to point at a temporary\n * directory populated with `createTestSession` from\n * `@parity/product-sdk-terminal/testing`.\n */\n storageDir?: string;\n}\n\n/**\n * Create a terminal adapter backed by the host-papp SDK.\n *\n * This sets up:\n * - File-based storage in `~/.polkadot-apps/` (since Node.js has no localStorage)\n * - WebSocket connection to the statement store\n * - The full SSO flow: QR pairing + on-chain attestation\n * - Session manager for signing requests\n */\n/** A PappAdapter with the `appId` it was created with and a `destroy` method for cleanup. */\nexport type TerminalAdapter = PappAdapter & {\n /** The `appId` passed to {@link createTerminalAdapter}. Useful for {@link createSessionSigner}. */\n readonly appId: string;\n /**\n * Disconnect the WebSocket and release resources.\n *\n * @remarks\n * Idempotent. While this method is running and for ~50 ms after, the global\n * `console.error` is monkey-patched to suppress the noisy\n * `\"Statement subscription error: …\"` message that\n * `@novasamatech/statement-store` emits when its WebSocket disconnects with\n * live subscriptions still attached. Unrelated `console.error` calls in\n * that window may be silently swallowed if their first argument is a string\n * starting with `\"Statement subscription\"`. This is a pragmatic workaround\n * for the upstream noise; ideally we contribute a `silent` option upstream.\n */\n destroy(): void;\n};\n\nexport function createTerminalAdapter(options: TerminalAdapterOptions): TerminalAdapter {\n const endpoints = options.endpoints ?? SS_PASEO_STABLE_STAGE_ENDPOINTS;\n\n const storage = createNodeStorageAdapter(options.appId, options.storageDir);\n // ws-provider 0.9 takes endpoints positionally; relies on the global\n // WebSocket (Node ≥21) unless `websocketClass` is supplied.\n //\n // heartbeatTimeout uses setTimeout under the hood, which clamps to a\n // 32-bit signed integer. Passing Infinity triggers a noisy\n // `TimeoutOverflowWarning` on every reschedule. Use the int32 max\n // (~24.8 days) — effectively-never for any CLI session.\n const HEARTBEAT_NEVER_MS = 2_147_483_647;\n const lazyClient = createLazyClient(\n getWsProvider(endpoints, { heartbeatTimeout: HEARTBEAT_NEVER_MS }),\n );\n const statementStore = createPapiStatementStoreAdapter(lazyClient);\n\n const adapter = createPappAdapter({\n appId: options.appId,\n metadata: options.metadataUrl,\n hostMetadata: options.hostMetadata,\n adapters: {\n storage,\n lazyClient,\n statementStore,\n },\n });\n\n let destroyed = false;\n return {\n ...adapter,\n appId: options.appId,\n destroy() {\n if (destroyed) return;\n destroyed = true;\n log.debug(\"destroying terminal adapter; suppressing statement-store teardown noise\");\n\n // The statement-store logs `console.error(\"Statement subscription error:\", err)`\n // when the WebSocket disconnects while subscriptions are still active.\n // This is expected during teardown. Temporarily mute it.\n const origError = console.error;\n console.error = (...args: unknown[]) => {\n if (typeof args[0] === \"string\" && args[0].includes(\"Statement subscription\")) {\n return;\n }\n origError.apply(console, args);\n };\n\n adapter.sessions.dispose();\n try {\n lazyClient.disconnect();\n } catch (e) {\n log.warn(\"lazyClient.disconnect threw during destroy\", { error: e });\n }\n\n setTimeout(() => {\n console.error = origError;\n }, 50);\n },\n };\n}\n\nexport { SS_STABLE_STAGE_ENDPOINTS, SS_PASEO_STABLE_STAGE_ENDPOINTS };\n","/**\n * Create a PolkadotSigner from a QR-paired session.\n *\n * Bridges the host-papp session's `signRaw()` to polkadot-api's\n * `PolkadotSigner` interface via `getPolkadotSigner`, enabling\n * mobile-approved signing for on-chain transactions from the CLI.\n *\n * @example\n * ```ts\n * const [session] = adapter.sessions.sessions.read();\n *\n * // Default account — uses [adapter.appId, 0]:\n * const signer = createSessionSigner(session, adapter);\n *\n * // Non-default derivation index, or a different productId:\n * const subSigner = createSessionSignerForAccount(session, {\n * productId: \"my-product\",\n * derivationIndex: 3,\n * });\n *\n * await contract.publish.tx(domain, cid, { signer, origin });\n * ```\n */\nimport { getPolkadotSigner } from \"polkadot-api/signer\";\nimport type { PolkadotSigner } from \"polkadot-api\";\nimport type { UserSession } from \"@novasamatech/host-papp\";\nimport type { TerminalAdapter } from \"./adapter.js\";\n\n/**\n * Identifies which sub-account of a paired session should sign.\n *\n * Mirrors the `host-papp` wire format `productAccountId: [productId, derivationIndex]`:\n * `productId` is the dotNS-style identifier for the requesting product (matches\n * the adapter's `appId` in normal usage); `derivationIndex` is the BIP32-style\n * child-key index, where `0` is the session's default account.\n */\nexport interface ProductAccountRef {\n /** The product identifier. Usually equal to the adapter's `appId`. */\n productId: string;\n /** Child-key derivation index. `0` is the default account. */\n derivationIndex: number;\n}\n\nfunction buildSessionSigner(session: UserSession, ref: ProductAccountRef): PolkadotSigner {\n const accountId = new Uint8Array(session.remoteAccount.accountId);\n const productAccountId: [string, number] = [ref.productId, ref.derivationIndex];\n\n return getPolkadotSigner(\n accountId,\n \"Sr25519\",\n async (data: Uint8Array): Promise<Uint8Array> => {\n const result = await session.signRaw({\n productAccountId,\n data: { tag: \"Bytes\" as const, value: data },\n });\n\n if (result.isErr()) {\n throw new Error(`Mobile signing rejected: ${result.error.message}`);\n }\n\n return result.value.signature;\n },\n );\n}\n\n/**\n * Create a `PolkadotSigner` backed by a QR-paired mobile wallet session,\n * using the session's **default account** (`derivationIndex: 0`).\n *\n * For non-default sub-accounts, use {@link createSessionSignerForAccount}.\n *\n * @param session The paired user session.\n * @param adapter The {@link TerminalAdapter} that loaded the session. Its `appId`\n * is used as the `productId` in the wire request.\n */\nexport function createSessionSigner(\n session: UserSession,\n adapter: TerminalAdapter,\n): PolkadotSigner {\n return buildSessionSigner(session, { productId: adapter.appId, derivationIndex: 0 });\n}\n\n/**\n * Create a `PolkadotSigner` for a specific sub-account of a paired session.\n *\n * Use this when you need a derivation index other than `0`, or a `productId`\n * different from the adapter's `appId`. For the common default-account case,\n * prefer {@link createSessionSigner}.\n *\n * @param session The paired user session.\n * @param ref The product account to sign as: `{ productId, derivationIndex }`.\n */\nexport function createSessionSignerForAccount(\n session: UserSession,\n ref: ProductAccountRef,\n): PolkadotSigner {\n return buildSessionSigner(session, ref);\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect, vi } = import.meta.vitest;\n const { ok, err } = await import(\"neverthrow\");\n\n /**\n * Build a minimal `UserSession`-shaped stub whose `signRaw` is a Vitest spy.\n * Only the fields used by `createSessionSigner` are populated.\n */\n function makeSession(\n signRaw: (req: unknown) => Promise<unknown>,\n accountIdBytes: number[] = new Array(32).fill(0).map((_, i) => i),\n ): UserSession {\n return {\n remoteAccount: { accountId: accountIdBytes },\n signRaw: vi.fn(signRaw),\n } as unknown as UserSession;\n }\n\n function fakeAdapter(appId: string): TerminalAdapter {\n // Only the `appId` field matters for these tests.\n return { appId } as unknown as TerminalAdapter;\n }\n\n describe(\"createSessionSigner\", () => {\n test(\"exposes Sr25519 public key matching remoteAccount.accountId\", () => {\n const bytes = Array.from({ length: 32 }, (_, i) => i);\n const signer = createSessionSigner(\n makeSession(async () => ok({ signature: new Uint8Array() }), bytes),\n fakeAdapter(\"test-app\"),\n );\n expect(signer.publicKey).toEqual(new Uint8Array(bytes));\n });\n\n test(\"signBytes returns signature on success\", async () => {\n const sig = new Uint8Array([9, 8, 7, 6, 5, 4, 3, 2, 1]);\n const session = makeSession(async () => ok({ signature: sig }));\n const signer = createSessionSigner(session, fakeAdapter(\"test-app\"));\n\n const out = await signer.signBytes(new Uint8Array([1, 2, 3]));\n expect(out).toEqual(sig);\n });\n\n test(\"forwards request with productAccountId = [adapter.appId, 0]\", async () => {\n const captured: unknown[] = [];\n const session = makeSession(async (req) => {\n captured.push(req);\n return ok({ signature: new Uint8Array([1]) });\n });\n\n const signer = createSessionSigner(session, fakeAdapter(\"inferred-app\"));\n await signer.signBytes(new Uint8Array([1, 2, 3]));\n\n const req = captured[0] as { productAccountId: [string, number] };\n expect(req.productAccountId).toEqual([\"inferred-app\", 0]);\n });\n\n test(\"signBytes throws when mobile signing is rejected\", async () => {\n const session = makeSession(async () => err({ message: \"user declined\" }));\n const signer = createSessionSigner(session, fakeAdapter(\"test-app\"));\n\n await expect(signer.signBytes(new Uint8Array([1]))).rejects.toThrow(\n \"Mobile signing rejected: user declined\",\n );\n });\n });\n\n describe(\"createSessionSignerForAccount\", () => {\n test(\"forwards request with the given productId and derivationIndex\", async () => {\n const captured: unknown[] = [];\n const session = makeSession(async (req) => {\n captured.push(req);\n return ok({ signature: new Uint8Array([42]) });\n });\n\n const signer = createSessionSignerForAccount(session, {\n productId: \"my-app\",\n derivationIndex: 7,\n });\n\n // Note: polkadot-api wraps signBytes payloads in <Bytes>...</Bytes>\n // before invoking the underlying callback. We only care here that\n // our wrapping (`{ tag: 'Bytes', value }` envelope + productAccountId\n // tuple) is correct — not the byte-level payload contents.\n await signer.signBytes(new Uint8Array([10, 20, 30]));\n\n expect(captured).toHaveLength(1);\n const req = captured[0] as {\n productAccountId: [string, number];\n data: { tag: string; value: Uint8Array };\n };\n expect(req.productAccountId).toEqual([\"my-app\", 7]);\n expect(req.data.tag).toBe(\"Bytes\");\n expect(req.data.value).toBeInstanceOf(Uint8Array);\n });\n\n test(\"supports a productId different from any adapter's appId\", async () => {\n const captured: unknown[] = [];\n const session = makeSession(async (req) => {\n captured.push(req);\n return ok({ signature: new Uint8Array([0]) });\n });\n\n const signer = createSessionSignerForAccount(session, {\n productId: \"external-product\",\n derivationIndex: 0,\n });\n await signer.signBytes(new Uint8Array([1]));\n\n const req = captured[0] as { productAccountId: [string, number] };\n expect(req.productAccountId).toEqual([\"external-product\", 0]);\n });\n });\n}\n","/**\n * Helpers for working with persisted sessions on a `TerminalAdapter`.\n */\nimport type { UserSession } from \"@novasamatech/host-papp\";\n\nimport type { TerminalAdapter } from \"./adapter.js\";\n\n/**\n * Wait for the adapter to load at least one persisted session, or resolve\n * with an empty array after `timeoutMs`.\n *\n * The session manager loads sessions from storage asynchronously, so a\n * synchronous `adapter.sessions.sessions.read()` immediately after\n * `createTerminalAdapter()` may return `[]` even when sessions exist on\n * disk. Use this helper to give the loader a chance to populate before\n * deciding whether the user is logged in.\n */\nexport function waitForSessions(\n adapter: TerminalAdapter,\n timeoutMs = 3000,\n): Promise<UserSession[]> {\n return new Promise((resolve) => {\n let resolved = false;\n let unsub: (() => void) | null = null;\n\n const finish = (sessions: UserSession[]) => {\n if (resolved) return;\n resolved = true;\n // Defer unsub so the synchronous initial emission can complete\n // before we tear down the subscription.\n queueMicrotask(() => unsub?.());\n resolve(sessions);\n };\n\n const timer = setTimeout(() => finish([]), timeoutMs);\n\n unsub = adapter.sessions.sessions.subscribe((sessions: UserSession[]) => {\n if (sessions.length > 0) {\n clearTimeout(timer);\n finish(sessions);\n }\n });\n });\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect, vi } = import.meta.vitest;\n\n type Subscriber = (sessions: UserSession[]) => void;\n\n function fakeAdapter(): {\n adapter: TerminalAdapter;\n emit: (sessions: UserSession[]) => void;\n subscriberCount: () => number;\n } {\n const subscribers = new Set<Subscriber>();\n const adapter = {\n sessions: {\n sessions: {\n subscribe(cb: Subscriber) {\n subscribers.add(cb);\n return () => {\n subscribers.delete(cb);\n };\n },\n },\n },\n } as unknown as TerminalAdapter;\n return {\n adapter,\n emit: (sessions) => {\n for (const cb of subscribers) cb(sessions);\n },\n subscriberCount: () => subscribers.size,\n };\n }\n\n describe(\"waitForSessions\", () => {\n test(\"resolves with sessions on first non-empty emission\", async () => {\n const { adapter, emit } = fakeAdapter();\n const promise = waitForSessions(adapter, 1000);\n const session = { remoteAccount: { accountId: new Uint8Array(32) } } as UserSession;\n emit([session]);\n await expect(promise).resolves.toEqual([session]);\n });\n\n test(\"ignores empty emissions and waits for a non-empty one\", async () => {\n const { adapter, emit } = fakeAdapter();\n const promise = waitForSessions(adapter, 1000);\n emit([]);\n emit([]);\n const session = { remoteAccount: { accountId: new Uint8Array(32) } } as UserSession;\n emit([session]);\n await expect(promise).resolves.toEqual([session]);\n });\n\n test(\"resolves with [] after timeout when nothing is emitted\", async () => {\n vi.useFakeTimers();\n try {\n const { adapter } = fakeAdapter();\n const promise = waitForSessions(adapter, 50);\n vi.advanceTimersByTime(50);\n await expect(promise).resolves.toEqual([]);\n } finally {\n vi.useRealTimers();\n }\n });\n\n test(\"unsubscribes after resolving\", async () => {\n const { adapter, emit, subscriberCount } = fakeAdapter();\n const session = { remoteAccount: { accountId: new Uint8Array(32) } } as UserSession;\n const promise = waitForSessions(adapter, 1000);\n emit([session]);\n await promise;\n // Wait for queueMicrotask\n await Promise.resolve();\n expect(subscriberCount()).toBe(0);\n });\n\n test(\"uses default 3000ms timeout when none is passed\", async () => {\n vi.useFakeTimers();\n try {\n const { adapter } = fakeAdapter();\n // No timeoutMs argument — exercises the `?? 3000` default branch.\n const promise = waitForSessions(adapter);\n // Just short of 3000ms: still pending.\n vi.advanceTimersByTime(2999);\n let settled = false;\n promise.then(() => {\n settled = true;\n });\n await Promise.resolve();\n expect(settled).toBe(false);\n // Cross the default boundary.\n vi.advanceTimersByTime(1);\n await expect(promise).resolves.toEqual([]);\n } finally {\n vi.useRealTimers();\n }\n });\n\n test(\"handles synchronous initial emission\", async () => {\n // Some subscribables emit current value synchronously inside subscribe().\n const session = { remoteAccount: { accountId: new Uint8Array(32) } } as UserSession;\n const adapter = {\n sessions: {\n sessions: {\n subscribe(cb: Subscriber) {\n cb([session]);\n return () => {};\n },\n },\n },\n } as unknown as TerminalAdapter;\n await expect(waitForSessions(adapter, 1000)).resolves.toEqual([session]);\n });\n });\n}\n","/**\n * Terminal QR code rendering using Unicode half-block characters.\n *\n * Wraps the `qrcode` npm package's UTF-8 renderer, which packs two\n * module rows per terminal line using half-block characters.\n */\n\n/** Options for QR code rendering. */\nexport interface QrRenderOptions {\n /** Error correction level. Default: \"M\". */\n errorCorrectionLevel?: \"L\" | \"M\" | \"Q\" | \"H\";\n /** Quiet zone size in modules. Default: 2. */\n margin?: number;\n}\n\n/**\n * Encode a string as a QR code rendered in Unicode half-block characters.\n *\n * Returns a multi-line string suitable for `console.log`.\n */\nexport async function renderQrCode(data: string, options?: QrRenderOptions): Promise<string> {\n const QRCode = await import(\"qrcode\");\n const result = await QRCode.toString(data, {\n type: \"utf8\",\n errorCorrectionLevel: options?.errorCorrectionLevel ?? \"M\",\n margin: options?.margin ?? 2,\n });\n return result;\n}\n\nif (import.meta.vitest) {\n const { test, expect } = import.meta.vitest;\n\n test(\"renderQrCode produces non-empty output\", async () => {\n const result = await renderQrCode(\"https://example.com\");\n expect(result.length).toBeGreaterThan(0);\n expect(result).toContain(\"\\n\");\n });\n\n test(\"renderQrCode contains Unicode block characters\", async () => {\n const result = await renderQrCode(\"test\");\n expect(/[▀▄█]/.test(result)).toBe(true);\n });\n\n test(\"different inputs produce different QR codes\", async () => {\n const a = await renderQrCode(\"hello\");\n const b = await renderQrCode(\"world\");\n expect(a).not.toBe(b);\n });\n}\n"],"mappings":"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","names":["makeSession","fakeAdapter","req","fakeAdapter"]}
|
|
1
|
+
{"version":3,"sources":["../src/adapter.ts","../src/signer.ts","../src/sessions.ts","../src/qr-encode.ts"],"sourcesContent":["/**\n * Node.js adapter for the Polkadot host-papp SDK.\n *\n * Provides Node.js-compatible implementations of the SDK's storage and\n * transport layers, enabling QR login, attestation, and signing in\n * terminal/CLI environments.\n */\nimport {\n createPappAdapter,\n type PappAdapter,\n type HostMetadata,\n SS_STABLE_STAGE_ENDPOINTS,\n SS_PASEO_STABLE_STAGE_ENDPOINTS,\n} from \"@novasamatech/host-papp\";\nimport {\n createLazyClient,\n createPapiStatementStoreAdapter,\n type LazyClient,\n} from \"@novasamatech/statement-store\";\nimport { createLogger } from \"@parity/product-sdk-logger\";\nimport { getWsProvider } from \"@polkadot-api/ws-provider\";\n\nimport { createNodeStorageAdapter } from \"./node-storage.js\";\n\nconst log = createLogger(\"terminal\");\n\n/** Options for creating a terminal adapter. */\nexport interface TerminalAdapterOptions {\n /** Unique app identifier. Used as the storage namespace. */\n appId: string;\n /** URL to the app's metadata JSON (name + icon), shown during pairing. */\n metadataUrl: string;\n /** Statement store WebSocket endpoints. Defaults to Paseo stable endpoints. */\n endpoints?: string[];\n /** Optional host metadata for the Sign-In screen. */\n hostMetadata?: HostMetadata;\n /**\n * Directory where session files are persisted. Defaults to\n * `~/.polkadot-apps/`. Override in tests to point at a temporary\n * directory populated with `createTestSession` from\n * `@parity/product-sdk-terminal/testing`.\n */\n storageDir?: string;\n}\n\n/**\n * Create a terminal adapter backed by the host-papp SDK.\n *\n * This sets up:\n * - File-based storage in `~/.polkadot-apps/` (since Node.js has no localStorage)\n * - WebSocket connection to the statement store\n * - The full SSO flow: QR pairing + on-chain attestation\n * - Session manager for signing requests\n */\n/** A PappAdapter with the `appId` it was created with and a `destroy` method for cleanup. */\nexport type TerminalAdapter = PappAdapter & {\n /** The `appId` passed to {@link createTerminalAdapter}. Useful for {@link createSessionSigner}. */\n readonly appId: string;\n /**\n * Disconnect the WebSocket and release resources.\n *\n * @remarks\n * Idempotent. Returns a Promise that resolves once all in-flight\n * statement-subscription teardowns have settled and the underlying\n * substrate client has been disconnected. **Awaiting is recommended\n * but not required** — callers that don't await get the same\n * fire-and-forget shape the previous version had, but they may see\n * the destroy-time RPC traffic finish after the function returns.\n *\n * The implementation tracks the server-side `statement_unsubscribe…`\n * RPCs `sessions.dispose()` fires, then awaits them via\n * `Promise.allSettled` before destroying the substrate-client request\n * queue. No timing-based guesses; no global-state mutations of\n * `console.error` or `process.on('unhandledRejection')`. Pending\n * subscribes (where `onSuccess` hasn't fired yet) are cancelled by\n * the underlying `getSubscribeFn` teardown via `cancelRequest()`,\n * which is the in-band fast-path and doesn't surface as a rejection.\n */\n destroy(): Promise<void>;\n};\n\nexport function createTerminalAdapter(options: TerminalAdapterOptions): TerminalAdapter {\n const endpoints = options.endpoints ?? SS_PASEO_STABLE_STAGE_ENDPOINTS;\n\n const storage = createNodeStorageAdapter(options.appId, options.storageDir);\n // ws-provider 0.9 takes endpoints positionally; relies on the global\n // WebSocket (Node ≥21) unless `websocketClass` is supplied.\n //\n // heartbeatTimeout uses setTimeout under the hood, which clamps to a\n // 32-bit signed integer. Passing Infinity triggers a noisy\n // `TimeoutOverflowWarning` on every reschedule. Use the int32 max\n // (~24.8 days) — effectively-never for any CLI session.\n const HEARTBEAT_NEVER_MS = 2_147_483_647;\n const rawLazyClient = createLazyClient(\n getWsProvider(endpoints, { heartbeatTimeout: HEARTBEAT_NEVER_MS }),\n );\n const trackedLazyClient = wrapLazyClient(rawLazyClient);\n const statementStore = createPapiStatementStoreAdapter(trackedLazyClient);\n\n const adapter = createPappAdapter({\n appId: options.appId,\n metadata: options.metadataUrl,\n hostMetadata: options.hostMetadata,\n adapters: {\n storage,\n lazyClient: trackedLazyClient,\n statementStore,\n },\n });\n\n let destroyPromise: Promise<void> | null = null;\n return {\n ...adapter,\n appId: options.appId,\n destroy(): Promise<void> {\n if (destroyPromise) return destroyPromise;\n destroyPromise = teardown(adapter.sessions, trackedLazyClient);\n return destroyPromise;\n },\n };\n}\n\n/**\n * Lazy-client wrapper that tracks server-side unsubscribe RPCs as Promises.\n *\n * `lazyClient.getSubscribeFn` returns a function whose teardown callback\n * fires `c._request(unsubscribeMethod, ...)` with `noop` `onSuccess` /\n * `onError` — the unsubscribe is fire-and-forget by upstream design. We\n * intercept those requests by replacing their callbacks with handlers\n * that resolve a tracking Promise on either outcome (success OR error —\n * we just need to know the request settled, not whether it succeeded).\n *\n * `awaitPendingUnsubs()` returns a Promise that settles when every\n * tracked unsubscribe has completed. Used by `teardown` to drain before\n * calling `disconnect`.\n *\n * The wrapper is otherwise transparent: `getClient`, `getRequestFn`,\n * `disconnect` pass through unchanged.\n */\ntype TrackedLazyClient = LazyClient & {\n awaitPendingUnsubs(): Promise<void>;\n};\n\nfunction wrapLazyClient(inner: LazyClient): TrackedLazyClient {\n const pendingUnsubs = new Set<Promise<void>>();\n const innerGetSubscribeFn = inner.getSubscribeFn.bind(inner);\n\n return {\n ...inner,\n getClient: inner.getClient.bind(inner),\n getRequestFn: inner.getRequestFn.bind(inner),\n disconnect: inner.disconnect.bind(inner),\n\n getSubscribeFn() {\n // Each call returns a new SubscribeFn. We wrap the teardown\n // callback so any unsubscribe RPC it fires gets tracked.\n const innerSubscribe = innerGetSubscribeFn();\n return ((method, params, onMessage, onError) => {\n const innerTeardown = innerSubscribe(method, params, onMessage, onError);\n return () => {\n // Track the unsubscribe with a Promise that resolves\n // after the microtask queue drains. The upstream code\n // uses `noop` callbacks on the actual RPC, so we\n // can't directly observe completion — but by the\n // time `innerTeardown()` returns, the `_request`\n // has been queued. Two microtask hops are enough\n // for the request to flush through the\n // substrate-client's send pipeline.\n //\n // If `innerTeardown()` throws synchronously, we let\n // the throw escape (the caller — `sessions.dispose()`\n // — is in the best position to decide what to do)\n // but the tracker still resolves so\n // `awaitPendingUnsubs()` doesn't hang and\n // `destroy()` still completes.\n const tracked = new Promise<void>((resolve) => {\n queueMicrotask(() => queueMicrotask(resolve));\n });\n pendingUnsubs.add(tracked);\n void tracked.finally(() => pendingUnsubs.delete(tracked));\n innerTeardown();\n };\n }) as ReturnType<LazyClient[\"getSubscribeFn\"]>;\n },\n\n async awaitPendingUnsubs(): Promise<void> {\n // Snapshot so additions made during the await don't extend\n // the wait indefinitely (sessions.dispose() should have fired\n // them all synchronously by the time we're called).\n const snapshot = Array.from(pendingUnsubs);\n await Promise.allSettled(snapshot);\n },\n };\n}\n\n/**\n * Drain pending unsubscribes, then disconnect. Order matters and is now\n * deterministic — no `setTimeout` guesses, no global-state mutations.\n *\n * 1. `sessions.dispose()` walks each open statement-subscription and\n * triggers their RPC unsubscribe via the wrapped subscribe-fn's\n * teardown callback. Each unsubscribe is recorded as a tracked\n * Promise on the wrapper.\n * 2. `awaitPendingUnsubs()` waits for those tracked Promises to\n * settle (resolution OR rejection — we just need confirmation the\n * RPC has left the substrate-client send pipeline).\n * 3. `disconnect()` calls `substrateClient.destroy()`. Nothing is\n * pending at this point, so no `DestroyedError` rejections fire.\n *\n * If the disconnect call itself throws, log and continue rather than\n * propagating — caller can `await destroy()` without `try/catch`.\n */\nasync function teardown(\n sessions: { dispose(): void },\n lazyClient: TrackedLazyClient,\n): Promise<void> {\n log.debug(\"destroying terminal adapter\");\n sessions.dispose();\n await lazyClient.awaitPendingUnsubs();\n try {\n lazyClient.disconnect();\n } catch (e) {\n log.warn(\"lazyClient.disconnect threw during destroy\", { error: e });\n }\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect, vi } = import.meta.vitest;\n\n /**\n * Build a fake LazyClient whose `getSubscribeFn` records each subscribe\n * call and whose returned teardown is observable. Lets tests assert on\n * teardown invocation and on `disconnect` ordering.\n */\n function fakeLazyClient(): {\n client: LazyClient;\n teardownCalls: number;\n disconnectCalls: number;\n } {\n let teardownCalls = 0;\n let disconnectCalls = 0;\n const fake = {\n getClient: (() =>\n ({}) as ReturnType<LazyClient[\"getClient\"]>) as LazyClient[\"getClient\"],\n getRequestFn: (() => () => Promise.resolve()) as LazyClient[\"getRequestFn\"],\n getSubscribeFn: () =>\n ((_method, _params, _onMessage, _onError) => {\n return () => {\n teardownCalls += 1;\n };\n }) as ReturnType<LazyClient[\"getSubscribeFn\"]>,\n disconnect: () => {\n disconnectCalls += 1;\n },\n } as LazyClient;\n return {\n client: fake,\n get teardownCalls() {\n return teardownCalls;\n },\n get disconnectCalls() {\n return disconnectCalls;\n },\n };\n }\n\n describe(\"wrapLazyClient\", () => {\n test(\"passes getClient / getRequestFn / disconnect through unchanged\", () => {\n const fake = fakeLazyClient();\n const wrapped = wrapLazyClient(fake.client);\n\n wrapped.getClient();\n wrapped.disconnect();\n expect(fake.disconnectCalls).toBe(1);\n expect(typeof wrapped.getRequestFn).toBe(\"function\");\n });\n\n test(\"getSubscribeFn returns a wrapped subscribe whose teardown invokes the inner teardown\", () => {\n const fake = fakeLazyClient();\n const wrapped = wrapLazyClient(fake.client);\n\n const subscribe = wrapped.getSubscribeFn();\n const teardown = subscribe(\n \"statement_subscribeStatement\",\n [],\n () => {},\n () => {},\n );\n teardown();\n\n expect(fake.teardownCalls).toBe(1);\n });\n\n test(\"awaitPendingUnsubs resolves after all wrapped teardowns settle\", async () => {\n const fake = fakeLazyClient();\n const wrapped = wrapLazyClient(fake.client);\n const subscribe = wrapped.getSubscribeFn();\n\n // Simulate three live subscriptions being torn down in the\n // same tick (what `sessions.dispose()` does internally).\n for (let i = 0; i < 3; i++) {\n const teardown = subscribe(\n \"statement_subscribeStatement\",\n [],\n () => {},\n () => {},\n );\n teardown();\n }\n\n // All three pending — but resolves once microtasks flush.\n await wrapped.awaitPendingUnsubs();\n expect(fake.teardownCalls).toBe(3);\n });\n\n test(\"awaitPendingUnsubs with no pending unsubs resolves immediately\", async () => {\n const fake = fakeLazyClient();\n const wrapped = wrapLazyClient(fake.client);\n await expect(wrapped.awaitPendingUnsubs()).resolves.toBeUndefined();\n });\n\n test(\"an unsubscribe whose teardown throws is still tracked and resolved\", async () => {\n // Even if the inner teardown throws, the tracker still needs\n // to settle — otherwise destroy() would hang forever.\n const fake = {\n getClient: (() =>\n ({}) as ReturnType<LazyClient[\"getClient\"]>) as LazyClient[\"getClient\"],\n getRequestFn: (() => () => Promise.resolve()) as LazyClient[\"getRequestFn\"],\n getSubscribeFn: () =>\n ((_method, _params, _onMessage, _onError) => {\n return () => {\n throw new Error(\"teardown boom\");\n };\n }) as ReturnType<LazyClient[\"getSubscribeFn\"]>,\n disconnect: () => {},\n } as LazyClient;\n\n const wrapped = wrapLazyClient(fake);\n const subscribe = wrapped.getSubscribeFn();\n const teardown = subscribe(\n \"statement_subscribeStatement\",\n [],\n () => {},\n () => {},\n );\n\n // The wrapper's inner try/finally lets the throw escape (as\n // expected — we don't swallow user-visible errors), but the\n // tracker still resolves. Catch-and-await pattern:\n try {\n teardown();\n } catch {\n // expected\n }\n await expect(wrapped.awaitPendingUnsubs()).resolves.toBeUndefined();\n });\n });\n\n describe(\"teardown\", () => {\n test(\"orders sessions.dispose, drain, then disconnect\", async () => {\n const order: string[] = [];\n\n const fake = fakeLazyClient();\n const wrapped = wrapLazyClient(fake.client);\n // Override disconnect to capture ordering.\n const innerDisconnect = wrapped.disconnect.bind(wrapped);\n wrapped.disconnect = () => {\n order.push(\"disconnect\");\n innerDisconnect();\n };\n\n const sessions = {\n dispose: vi.fn(() => {\n order.push(\"dispose\");\n // Simulate sessions.dispose firing one teardown.\n const subscribe = wrapped.getSubscribeFn();\n subscribe(\n \"statement_subscribeStatement\",\n [],\n () => {},\n () => {},\n )();\n }),\n };\n\n await teardown(sessions, wrapped);\n\n expect(order).toEqual([\"dispose\", \"disconnect\"]);\n expect(sessions.dispose).toHaveBeenCalledTimes(1);\n expect(fake.disconnectCalls).toBe(1);\n });\n\n test(\"disconnect runs even when there are no pending unsubs\", async () => {\n const fake = fakeLazyClient();\n const wrapped = wrapLazyClient(fake.client);\n const sessions = { dispose: vi.fn() };\n\n await teardown(sessions, wrapped);\n\n expect(fake.disconnectCalls).toBe(1);\n });\n\n test(\"logs a warning if disconnect throws, doesn't propagate to caller\", async () => {\n const fake = {\n getClient: (() =>\n ({}) as ReturnType<LazyClient[\"getClient\"]>) as LazyClient[\"getClient\"],\n getRequestFn: (() => () => Promise.resolve()) as LazyClient[\"getRequestFn\"],\n getSubscribeFn: () =>\n ((_method, _params, _onMessage, _onError) => () => {}) as ReturnType<\n LazyClient[\"getSubscribeFn\"]\n >,\n disconnect: () => {\n throw new Error(\"boom\");\n },\n } as LazyClient;\n\n const wrapped = wrapLazyClient(fake);\n const sessions = { dispose: vi.fn() };\n\n await expect(teardown(sessions, wrapped)).resolves.toBeUndefined();\n });\n\n test(\"awaits pending unsubs before calling disconnect\", async () => {\n // The whole point of the fix: disconnect must not run while\n // unsubscribe RPCs are still queued. Verify ordering even\n // when the unsubs take multiple microtasks to settle.\n const fake = fakeLazyClient();\n const wrapped = wrapLazyClient(fake.client);\n\n let unsubResolved = false;\n const innerDisconnect = wrapped.disconnect.bind(wrapped);\n wrapped.disconnect = () => {\n // Disconnect must not run before the unsubscribe has\n // resolved. If it does, this assertion fires.\n expect(unsubResolved).toBe(true);\n innerDisconnect();\n };\n\n const sessions = {\n dispose: () => {\n const subscribe = wrapped.getSubscribeFn();\n subscribe(\n \"statement_subscribeStatement\",\n [],\n () => {},\n () => {},\n )();\n // mark resolution after the microtasks the wrapper queues\n queueMicrotask(() =>\n queueMicrotask(() => {\n unsubResolved = true;\n }),\n );\n },\n };\n\n await teardown(sessions, wrapped);\n expect(fake.disconnectCalls).toBe(1);\n });\n });\n}\n\nexport { SS_STABLE_STAGE_ENDPOINTS, SS_PASEO_STABLE_STAGE_ENDPOINTS };\n","/**\n * Create a PolkadotSigner from a QR-paired session.\n *\n * Builds the `PolkadotSigner` via PAPI-native `getPolkadotSigner` and routes\n * **transaction signing** through `session.signRaw` with the `Payload` tag\n * — a tagged-bytes wire route in host-papp that signs the SCALE-encoded\n * extrinsic payload as-is, with no `<Bytes>...</Bytes>` anti-phishing\n * envelope. PAPI assembles the full payload (`callData ‖ extras ‖\n * additionalSigneds`) from runtime metadata, so every signed extension\n * declared by the chain — including extensions not known to PAPI's PJS\n * adapter (e.g. `AsPgas` on Paseo Next v2) — survives end-to-end as opaque\n * bytes that the wallet just signs.\n *\n * **Raw-message signing** (`signBytes`) keeps the `Bytes` tag so the wallet\n * applies the anti-phishing wrap once on its side. Correct for arbitrary\n * user data; wrong for extrinsic payloads (which is why the two paths split).\n *\n * Bypasses `getPolkadotSignerFromPjs` whose built-in mapper table covers\n * only eight extensions and throws `PJS does not support this\n * signed-extension: <name>` on anything else.\n *\n * @example\n * ```ts\n * const [session] = adapter.sessions.sessions.read();\n *\n * // Default account — uses [adapter.appId, 0]:\n * const signer = createSessionSigner(session, adapter);\n *\n * // Non-default derivation index, or a different productId:\n * const subSigner = createSessionSignerForAccount(session, {\n * productId: \"my-product\",\n * derivationIndex: 3,\n * });\n *\n * await contract.publish.tx(domain, cid, { signer, origin });\n * ```\n */\nimport type { UserSession } from \"@novasamatech/host-papp\";\nimport { toHex } from \"@polkadot-api/utils\";\nimport type { PolkadotSigner } from \"polkadot-api\";\nimport { getPolkadotSigner } from \"polkadot-api/signer\";\n\nimport type { TerminalAdapter } from \"./adapter.js\";\n\n/**\n * Identifies which sub-account of a paired session should sign.\n *\n * Mirrors the `host-papp` wire format `productAccountId: [productId, derivationIndex]`:\n * `productId` is the dotNS-style identifier for the requesting product (matches\n * the adapter's `appId` in normal usage); `derivationIndex` is the BIP32-style\n * child-key index, where `0` is the session's default account.\n */\nexport interface ProductAccountRef {\n /** The product identifier. Usually equal to the adapter's `appId`. */\n productId: string;\n /** Child-key derivation index. `0` is the default account. */\n derivationIndex: number;\n}\n\n/**\n * Sign function PAPI calls for transaction signing.\n *\n * PAPI's `getPolkadotSigner` assembles `callData ‖ extras ‖ additionalSigneds`\n * for every extension declared in `metadata.extrinsic.signedExtensions`,\n * concatenates them, blake2-hashes the result if >256 bytes, and hands the\n * bytes here. We forward them to the mobile wallet under the `Payload` tag\n * — opaque hex; no envelope — and return the raw signature.\n *\n * Any extension carried by the chain survives because the bytes are the\n * source of truth: the wallet does not inspect the payload's structure, it\n * just signs it.\n */\nfunction makeTxSignCallback(session: UserSession, productAccountId: [string, number]) {\n return async (toSign: Uint8Array): Promise<Uint8Array> => {\n const result = await session.signRaw({\n productAccountId,\n data: { tag: \"Payload\", value: toHex(toSign) },\n });\n\n if (result.isErr()) {\n throw new Error(`Mobile signing rejected: ${result.error.message}`);\n }\n\n return result.value.signature;\n };\n}\n\n/**\n * Sign function for arbitrary-byte signing (`PolkadotSigner.signBytes`).\n *\n * Routes to `session.signRaw` under the `Bytes` tag — the wallet's\n * raw-bytes interactor, which applies the standard `<Bytes>...</Bytes>`\n * anti-phishing envelope before signing.\n *\n * Provided as a separate function so `buildSessionSigner` can override\n * the `signBytes` produced by `getPolkadotSigner` (which would otherwise\n * funnel raw-bytes signing through the same `sign` callback as tx signing\n * — the wrong wire tag for arbitrary user data).\n */\nfunction makeRawBytesSignCallback(session: UserSession, productAccountId: [string, number]) {\n return async (data: Uint8Array): Promise<Uint8Array> => {\n const result = await session.signRaw({\n productAccountId,\n data: { tag: \"Bytes\", value: data },\n });\n\n if (result.isErr()) {\n throw new Error(`Mobile signing rejected: ${result.error.message}`);\n }\n\n return result.value.signature;\n };\n}\n\nfunction buildSessionSigner(session: UserSession, ref: ProductAccountRef): PolkadotSigner {\n const productAccountId: [string, number] = [ref.productId, ref.derivationIndex];\n const publicKey = new Uint8Array(session.remoteAccount.accountId);\n\n // host-papp pairs sr25519 accounts only — `createSr25519Secret` /\n // `deriveSr25519PublicKey` are the sole key-derivation primitives in\n // its SSO flow. If host-papp ever supports mixed schemes, this becomes\n // a session-driven field.\n const base = getPolkadotSigner(\n publicKey,\n \"Sr25519\",\n makeTxSignCallback(session, productAccountId),\n );\n\n // Override `signBytes`: `getPolkadotSigner` returns `signBytes:\n // getSignBytes(sign)`, which wraps incoming data with\n // `<Bytes>...</Bytes>` and then funnels it through the same `sign`\n // callback as tx signing. Routing pre-wrapped bytes through the\n // `Payload` tag would let the wallet sign the wrap as-is (the\n // `Payload` route deliberately skips the wallet-side envelope),\n // producing signatures that don't verify against the un-wrapped user\n // data. Routing `signBytes` through the `Bytes` tag instead applies\n // the envelope exactly once, on the wallet side.\n return {\n ...base,\n signBytes: makeRawBytesSignCallback(session, productAccountId),\n };\n}\n\n/**\n * Create a `PolkadotSigner` backed by a QR-paired mobile wallet session,\n * using the session's **default account** (`derivationIndex: 0`).\n *\n * For non-default sub-accounts, use {@link createSessionSignerForAccount}.\n *\n * @param session The paired user session.\n * @param adapter The {@link TerminalAdapter} that loaded the session. Its `appId`\n * is used as the `productId` in the wire request.\n */\nexport function createSessionSigner(\n session: UserSession,\n adapter: TerminalAdapter,\n): PolkadotSigner {\n return buildSessionSigner(session, { productId: adapter.appId, derivationIndex: 0 });\n}\n\n/**\n * Create a `PolkadotSigner` for a specific sub-account of a paired session.\n *\n * Use this when you need a derivation index other than `0`, or a `productId`\n * different from the adapter's `appId`. For the common default-account case,\n * prefer {@link createSessionSigner}.\n *\n * @param session The paired user session.\n * @param ref The product account to sign as: `{ productId, derivationIndex }`.\n */\nexport function createSessionSignerForAccount(\n session: UserSession,\n ref: ProductAccountRef,\n): PolkadotSigner {\n return buildSessionSigner(session, ref);\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect, vi } = import.meta.vitest;\n const { ok, err } = await import(\"neverthrow\");\n const { fromHex } = await import(\"@polkadot-api/utils\");\n\n /**\n * Build a minimal `UserSession`-shaped stub. Both `signPayload` and\n * `signRaw` accept request-capturing functions so tests can assert on\n * exactly which host-papp method got called and with what payload.\n */\n function makeSession(opts: {\n signPayload?: (req: unknown) => Promise<unknown>;\n signRaw?: (req: unknown) => Promise<unknown>;\n accountIdBytes?: number[];\n }): UserSession {\n const accountIdBytes = opts.accountIdBytes ?? new Array(32).fill(0).map((_, i) => i);\n return {\n remoteAccount: { accountId: accountIdBytes },\n signPayload: vi.fn(\n opts.signPayload ??\n (async () => {\n throw new Error(\"signPayload not stubbed in this test\");\n }),\n ),\n signRaw: vi.fn(\n opts.signRaw ??\n (async () => {\n throw new Error(\"signRaw not stubbed in this test\");\n }),\n ),\n } as unknown as UserSession;\n }\n\n function fakeAdapter(appId: string): TerminalAdapter {\n // Only the `appId` field matters for these tests.\n return { appId } as unknown as TerminalAdapter;\n }\n\n describe(\"createSessionSigner\", () => {\n test(\"exposes Sr25519 public key matching remoteAccount.accountId\", () => {\n const bytes = Array.from({ length: 32 }, (_, i) => i);\n const signer = createSessionSigner(\n makeSession({ accountIdBytes: bytes }),\n fakeAdapter(\"test-app\"),\n );\n expect(signer.publicKey).toEqual(new Uint8Array(bytes));\n });\n\n test(\"signBytes routes through session.signRaw with the Bytes tag\", async () => {\n const sig = new Uint8Array([9, 8, 7, 6, 5, 4, 3, 2, 1]);\n const captured: unknown[] = [];\n const session = makeSession({\n signRaw: async (req) => {\n captured.push(req);\n return ok({ signature: sig });\n },\n });\n const signer = createSessionSigner(session, fakeAdapter(\"test-app\"));\n\n const out = await signer.signBytes(new Uint8Array([1, 2, 3]));\n expect(out).toEqual(sig);\n\n // The bytes route forwards data verbatim under the `Bytes` tag.\n // Mobile applies the <Bytes>...</Bytes> envelope on its side.\n expect(captured).toHaveLength(1);\n const req = captured[0] as {\n productAccountId: [string, number];\n data: { tag: string; value: Uint8Array };\n };\n expect(req.productAccountId).toEqual([\"test-app\", 0]);\n expect(req.data.tag).toBe(\"Bytes\");\n expect(req.data.value).toEqual(new Uint8Array([1, 2, 3]));\n });\n\n test(\"signBytes never calls session.signPayload (legacy-path regression guard)\", async () => {\n // The PJS-bridged path used to route tx signing through\n // `session.signPayload`. The new PAPI-native path retires\n // `signPayload` entirely for this signer. This test guards\n // against accidentally re-introducing the legacy call.\n const session = makeSession({\n signRaw: async () => ok({ signature: new Uint8Array([1]) }),\n });\n const signer = createSessionSigner(session, fakeAdapter(\"test-app\"));\n\n await signer.signBytes(new Uint8Array([1, 2, 3]));\n\n const sessionWithSpies = session as unknown as {\n signPayload: { mock: { calls: unknown[] } };\n signRaw: { mock: { calls: unknown[] } };\n };\n expect(sessionWithSpies.signPayload.mock.calls).toHaveLength(0);\n expect(sessionWithSpies.signRaw.mock.calls).toHaveLength(1);\n });\n\n test(\"signBytes throws with a clear error when the mobile rejects\", async () => {\n const session = makeSession({\n signRaw: async () => err({ message: \"user declined\" }),\n });\n const signer = createSessionSigner(session, fakeAdapter(\"test-app\"));\n\n await expect(signer.signBytes(new Uint8Array([1]))).rejects.toThrow(\n \"Mobile signing rejected: user declined\",\n );\n });\n });\n\n describe(\"makeTxSignCallback — tx signing path (the AsPgas fix)\", () => {\n // Bypasses PAPI's getPolkadotSigner wrapper to assert directly on\n // the wire translation. The full PAPI signTx → callback → chain\n // roundtrip is exercised by the manual smoke test in\n // `manual-tests/qr-pair-and-sign.mjs` since CI cannot drive a real\n // phone.\n\n test(\"forwards bytes-to-sign under the Payload tag with the right productAccountId\", async () => {\n const captured: unknown[] = [];\n const session = makeSession({\n signRaw: async (req) => {\n captured.push(req);\n return ok({ signature: new Uint8Array([0xaa, 0xbb]) });\n },\n });\n\n const callback = makeTxSignCallback(session, [\"my-app\", 0]);\n await callback(new Uint8Array([0xde, 0xad, 0xbe, 0xef]));\n\n expect(captured).toHaveLength(1);\n const req = captured[0] as {\n productAccountId: [string, number];\n data: { tag: string; value: string };\n };\n expect(req.productAccountId).toEqual([\"my-app\", 0]);\n expect(req.data.tag).toBe(\"Payload\");\n // Value is the 0x-prefixed hex of the input bytes — the wallet\n // signs this opaque payload without inspecting it, which is\n // what lets new signed extensions (e.g. AsPgas) survive.\n expect(req.data.value).toBe(\"0xdeadbeef\");\n });\n\n test(\"never calls session.signPayload (legacy PJS-shape regression guard)\", async () => {\n // The whole point of the AsPgas fix: tx signing must hit\n // session.signRaw with the Payload tag, never session.signPayload.\n // session.signPayload's wire codec has fixed slots and cannot\n // carry arbitrary signed-extension extras.\n const session = makeSession({\n signRaw: async () => ok({ signature: new Uint8Array([1]) }),\n });\n\n const callback = makeTxSignCallback(session, [\"my-app\", 0]);\n await callback(new Uint8Array([1, 2, 3]));\n\n const sessionWithSpies = session as unknown as {\n signPayload: { mock: { calls: unknown[] } };\n signRaw: { mock: { calls: unknown[] } };\n };\n expect(sessionWithSpies.signPayload.mock.calls).toHaveLength(0);\n expect(sessionWithSpies.signRaw.mock.calls).toHaveLength(1);\n });\n\n test(\"returns the raw signature bytes from the mobile response\", async () => {\n const sig = new Uint8Array([0xab, 0xcd, 0xef]);\n const session = makeSession({\n signRaw: async () => ok({ signature: sig }),\n });\n\n const callback = makeTxSignCallback(session, [\"my-app\", 0]);\n const out = await callback(new Uint8Array([0]));\n\n expect(out).toBe(sig);\n });\n\n test(\"preserves arbitrary signed-extension payload bytes verbatim\", async () => {\n // Simulates what PAPI hands the callback when the chain\n // declares a custom signed extension: an opaque byte stream\n // built from `callData ‖ extras ‖ additionalSigneds`. The\n // callback must not interpret or reshape these bytes; it\n // forwards them as-is under the Payload tag.\n const customExtensionBytes = new Uint8Array([\n // arbitrary scale-encoded extension payload — content\n // doesn't matter, only that it round-trips exactly\n 0x01, 0x02, 0x03, 0xff, 0xfe, 0xfd, 0x42, 0x43, 0x44,\n ]);\n const captured: unknown[] = [];\n const session = makeSession({\n signRaw: async (req) => {\n captured.push(req);\n return ok({ signature: new Uint8Array([0]) });\n },\n });\n\n const callback = makeTxSignCallback(session, [\"my-app\", 0]);\n await callback(customExtensionBytes);\n\n const req = captured[0] as { data: { value: string } };\n expect(fromHex(req.data.value)).toEqual(customExtensionBytes);\n });\n\n test(\"throws a clear error when the mobile rejects\", async () => {\n const session = makeSession({\n signRaw: async () => err({ message: \"user declined\" }),\n });\n\n const callback = makeTxSignCallback(session, [\"my-app\", 0]);\n\n await expect(callback(new Uint8Array([0]))).rejects.toThrow(\n \"Mobile signing rejected: user declined\",\n );\n });\n });\n\n describe(\"makeRawBytesSignCallback\", () => {\n test(\"forwards data verbatim under the Bytes tag with the right productAccountId\", async () => {\n const captured: unknown[] = [];\n const session = makeSession({\n signRaw: async (req) => {\n captured.push(req);\n return ok({ signature: new Uint8Array([0xff]) });\n },\n });\n\n const callback = makeRawBytesSignCallback(session, [\"my-app\", 5]);\n await callback(new Uint8Array([0xde, 0xad, 0xbe, 0xef]));\n\n expect(captured).toHaveLength(1);\n const req = captured[0] as {\n productAccountId: [string, number];\n data: { tag: string; value: Uint8Array };\n };\n expect(req.productAccountId).toEqual([\"my-app\", 5]);\n expect(req.data.tag).toBe(\"Bytes\");\n expect(Array.from(req.data.value)).toEqual([0xde, 0xad, 0xbe, 0xef]);\n });\n\n test(\"returns the raw signature bytes from the mobile response\", async () => {\n const sig = new Uint8Array([0x42]);\n const session = makeSession({\n signRaw: async () => ok({ signature: sig }),\n });\n\n const callback = makeRawBytesSignCallback(session, [\"my-app\", 0]);\n const out = await callback(new Uint8Array([0]));\n\n expect(out).toBe(sig);\n });\n });\n\n describe(\"createSessionSignerForAccount\", () => {\n test(\"forwards productAccountId from the explicit ref\", async () => {\n const captured: unknown[] = [];\n const session = makeSession({\n signRaw: async (req) => {\n captured.push(req);\n return ok({ signature: new Uint8Array([42]) });\n },\n });\n\n const signer = createSessionSignerForAccount(session, {\n productId: \"my-app\",\n derivationIndex: 7,\n });\n await signer.signBytes(new Uint8Array([10, 20, 30]));\n\n expect(captured).toHaveLength(1);\n const req = captured[0] as {\n productAccountId: [string, number];\n data: { tag: string; value: Uint8Array };\n };\n expect(req.productAccountId).toEqual([\"my-app\", 7]);\n expect(req.data.tag).toBe(\"Bytes\");\n expect(req.data.value).toBeInstanceOf(Uint8Array);\n });\n\n test(\"supports a productId different from any adapter's appId\", async () => {\n const captured: unknown[] = [];\n const session = makeSession({\n signRaw: async (req) => {\n captured.push(req);\n return ok({ signature: new Uint8Array([0]) });\n },\n });\n\n const signer = createSessionSignerForAccount(session, {\n productId: \"external-product\",\n derivationIndex: 0,\n });\n await signer.signBytes(new Uint8Array([1]));\n\n const req = captured[0] as { productAccountId: [string, number] };\n expect(req.productAccountId).toEqual([\"external-product\", 0]);\n });\n });\n}\n","/**\n * Helpers for working with persisted sessions on a `TerminalAdapter`.\n */\nimport type { UserSession } from \"@novasamatech/host-papp\";\n\nimport type { TerminalAdapter } from \"./adapter.js\";\n\n/**\n * Wait for the adapter to load at least one persisted session, or resolve\n * with an empty array after `timeoutMs`.\n *\n * The session manager loads sessions from storage asynchronously, so a\n * synchronous `adapter.sessions.sessions.read()` immediately after\n * `createTerminalAdapter()` may return `[]` even when sessions exist on\n * disk. Use this helper to give the loader a chance to populate before\n * deciding whether the user is logged in.\n */\nexport function waitForSessions(\n adapter: TerminalAdapter,\n timeoutMs = 3000,\n): Promise<UserSession[]> {\n return new Promise((resolve) => {\n let resolved = false;\n let unsub: (() => void) | null = null;\n\n const finish = (sessions: UserSession[]) => {\n if (resolved) return;\n resolved = true;\n // Defer unsub so the synchronous initial emission can complete\n // before we tear down the subscription.\n queueMicrotask(() => unsub?.());\n resolve(sessions);\n };\n\n const timer = setTimeout(() => finish([]), timeoutMs);\n\n unsub = adapter.sessions.sessions.subscribe((sessions: UserSession[]) => {\n if (sessions.length > 0) {\n clearTimeout(timer);\n finish(sessions);\n }\n });\n });\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect, vi } = import.meta.vitest;\n\n type Subscriber = (sessions: UserSession[]) => void;\n\n function fakeAdapter(): {\n adapter: TerminalAdapter;\n emit: (sessions: UserSession[]) => void;\n subscriberCount: () => number;\n } {\n const subscribers = new Set<Subscriber>();\n const adapter = {\n sessions: {\n sessions: {\n subscribe(cb: Subscriber) {\n subscribers.add(cb);\n return () => {\n subscribers.delete(cb);\n };\n },\n },\n },\n } as unknown as TerminalAdapter;\n return {\n adapter,\n emit: (sessions) => {\n for (const cb of subscribers) cb(sessions);\n },\n subscriberCount: () => subscribers.size,\n };\n }\n\n describe(\"waitForSessions\", () => {\n test(\"resolves with sessions on first non-empty emission\", async () => {\n const { adapter, emit } = fakeAdapter();\n const promise = waitForSessions(adapter, 1000);\n const session = { remoteAccount: { accountId: new Uint8Array(32) } } as UserSession;\n emit([session]);\n await expect(promise).resolves.toEqual([session]);\n });\n\n test(\"ignores empty emissions and waits for a non-empty one\", async () => {\n const { adapter, emit } = fakeAdapter();\n const promise = waitForSessions(adapter, 1000);\n emit([]);\n emit([]);\n const session = { remoteAccount: { accountId: new Uint8Array(32) } } as UserSession;\n emit([session]);\n await expect(promise).resolves.toEqual([session]);\n });\n\n test(\"resolves with [] after timeout when nothing is emitted\", async () => {\n vi.useFakeTimers();\n try {\n const { adapter } = fakeAdapter();\n const promise = waitForSessions(adapter, 50);\n vi.advanceTimersByTime(50);\n await expect(promise).resolves.toEqual([]);\n } finally {\n vi.useRealTimers();\n }\n });\n\n test(\"unsubscribes after resolving\", async () => {\n const { adapter, emit, subscriberCount } = fakeAdapter();\n const session = { remoteAccount: { accountId: new Uint8Array(32) } } as UserSession;\n const promise = waitForSessions(adapter, 1000);\n emit([session]);\n await promise;\n // Wait for queueMicrotask\n await Promise.resolve();\n expect(subscriberCount()).toBe(0);\n });\n\n test(\"uses default 3000ms timeout when none is passed\", async () => {\n vi.useFakeTimers();\n try {\n const { adapter } = fakeAdapter();\n // No timeoutMs argument — exercises the `?? 3000` default branch.\n const promise = waitForSessions(adapter);\n // Just short of 3000ms: still pending.\n vi.advanceTimersByTime(2999);\n let settled = false;\n promise.then(() => {\n settled = true;\n });\n await Promise.resolve();\n expect(settled).toBe(false);\n // Cross the default boundary.\n vi.advanceTimersByTime(1);\n await expect(promise).resolves.toEqual([]);\n } finally {\n vi.useRealTimers();\n }\n });\n\n test(\"handles synchronous initial emission\", async () => {\n // Some subscribables emit current value synchronously inside subscribe().\n const session = { remoteAccount: { accountId: new Uint8Array(32) } } as UserSession;\n const adapter = {\n sessions: {\n sessions: {\n subscribe(cb: Subscriber) {\n cb([session]);\n return () => {};\n },\n },\n },\n } as unknown as TerminalAdapter;\n await expect(waitForSessions(adapter, 1000)).resolves.toEqual([session]);\n });\n });\n}\n","/**\n * Terminal QR code rendering using Unicode half-block characters.\n *\n * Wraps the `qrcode` npm package's UTF-8 renderer, which packs two\n * module rows per terminal line using half-block characters.\n */\n\n/** Options for QR code rendering. */\nexport interface QrRenderOptions {\n /** Error correction level. Default: \"M\". */\n errorCorrectionLevel?: \"L\" | \"M\" | \"Q\" | \"H\";\n /** Quiet zone size in modules. Default: 2. */\n margin?: number;\n}\n\n/**\n * Encode a string as a QR code rendered in Unicode half-block characters.\n *\n * Returns a multi-line string suitable for `console.log`.\n */\nexport async function renderQrCode(data: string, options?: QrRenderOptions): Promise<string> {\n const QRCode = await import(\"qrcode\");\n const result = await QRCode.toString(data, {\n type: \"utf8\",\n errorCorrectionLevel: options?.errorCorrectionLevel ?? \"M\",\n margin: options?.margin ?? 2,\n });\n return result;\n}\n\nif (import.meta.vitest) {\n const { test, expect } = import.meta.vitest;\n\n test(\"renderQrCode produces non-empty output\", async () => {\n const result = await renderQrCode(\"https://example.com\");\n expect(result.length).toBeGreaterThan(0);\n expect(result).toContain(\"\\n\");\n });\n\n test(\"renderQrCode contains Unicode block characters\", async () => {\n const result = await renderQrCode(\"test\");\n expect(/[▀▄█]/.test(result)).toBe(true);\n });\n\n test(\"different inputs produce different QR codes\", async () => {\n const a = await renderQrCode(\"hello\");\n const b = await renderQrCode(\"world\");\n expect(a).not.toBe(b);\n });\n}\n"],"mappings":"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","names":["fakeLazyClient","teardown","makeSession","fakeAdapter","req","fakeAdapter"]}
|
package/package.json
CHANGED