@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 +75 -16
- package/dist/chunk-EORPX5XL.js +267 -0
- package/dist/chunk-EORPX5XL.js.map +1 -0
- package/dist/chunk-GHOFKVUG.js +8 -133
- package/dist/chunk-GHOFKVUG.js.map +1 -1
- package/dist/{host-C3KVvc1j.d.ts → host-_G9M0isV.d.ts} +36 -10
- package/dist/host.d.ts +1 -1
- package/dist/host.js +2 -14
- package/dist/host.js.map +1 -1
- package/dist/index.d.ts +162 -37
- package/dist/index.js +158 -872
- package/dist/index.js.map +1 -1
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +24 -234
- package/dist/testing.js.map +1 -1
- package/package.json +9 -9
- package/dist/chunk-YS3GW2XW.js +0 -1508
- package/dist/chunk-YS3GW2XW.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,27 +1,35 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
} from
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
} from
|
|
1
|
+
import { cacheFilePath, withFileLock, loadJsonCache, saveJsonCache } from './chunk-EORPX5XL.js';
|
|
2
|
+
export { requestResourceAllocation, sendResourceAllocation } from './chunk-EORPX5XL.js';
|
|
3
|
+
import { createNodeStorageAdapter, sanitizeKey } from './chunk-GHOFKVUG.js';
|
|
4
|
+
export { createNodeStorageAdapter } from './chunk-GHOFKVUG.js';
|
|
5
|
+
import { SS_PREVIEW_STAGE_ENDPOINTS, createPappAdapter, AllowanceError } from '@novasamatech/host-papp';
|
|
6
|
+
export { AllowanceError, SS_STABLE_STAGE_ENDPOINTS } from '@novasamatech/host-papp';
|
|
7
|
+
import { createLazyClient, createPapiStatementStoreAdapter, NoAllowanceError } from '@novasamatech/statement-store';
|
|
8
|
+
import { createLogger } from '@parity/product-sdk-logger';
|
|
9
|
+
import { getWsProvider } from '@polkadot-api/ws-provider';
|
|
10
|
+
import { resolveTxExtVersion } from '@parity/product-sdk-host/tx-ext-version';
|
|
11
|
+
import { deriveProductAccountPublicKey } from '@parity/product-sdk-keys';
|
|
12
|
+
import { AllowanceExpiredError } from '@parity/product-sdk-signer/errors';
|
|
13
|
+
export { AllowanceExpiredError, SignerError } from '@parity/product-sdk-signer/errors';
|
|
14
|
+
import { toHex, fromHex } from '@polkadot-api/utils';
|
|
15
|
+
import { gcm } from '@noble/ciphers/aes.js';
|
|
16
|
+
import { blake2b } from '@noble/hashes/blake2.js';
|
|
17
|
+
import { readFile } from 'fs/promises';
|
|
18
|
+
import { join } from 'path';
|
|
19
|
+
import { Enum, _void, Struct, str, Bytes, Vector } from 'scale-ts';
|
|
20
|
+
import '@noble/hashes/utils.js';
|
|
21
|
+
|
|
22
|
+
var StatementStoreNetworks = {
|
|
23
|
+
// host-papp has no constant for this host; its `SS_PASEO_STABLE_STAGE_ENDPOINTS`
|
|
24
|
+
// still names the pre-rename chain.
|
|
25
|
+
paseo: ["wss://paseo-people-next-system-rpc.polkadot.io"],
|
|
26
|
+
previewnet: SS_PREVIEW_STAGE_ENDPOINTS
|
|
27
|
+
};
|
|
9
28
|
|
|
10
29
|
// src/adapter.ts
|
|
11
|
-
import {
|
|
12
|
-
createPappAdapter,
|
|
13
|
-
SS_STABLE_STAGE_ENDPOINTS,
|
|
14
|
-
SS_PASEO_STABLE_STAGE_ENDPOINTS
|
|
15
|
-
} from "@novasamatech/host-papp";
|
|
16
|
-
import {
|
|
17
|
-
createLazyClient,
|
|
18
|
-
createPapiStatementStoreAdapter
|
|
19
|
-
} from "@novasamatech/statement-store";
|
|
20
|
-
import { createLogger } from "@parity/product-sdk-logger";
|
|
21
|
-
import { getWsProvider } from "@polkadot-api/ws-provider";
|
|
22
30
|
var log = createLogger("terminal");
|
|
23
31
|
function createTerminalAdapter(options) {
|
|
24
|
-
const endpoints = options.endpoints ??
|
|
32
|
+
const endpoints = options.endpoints ?? StatementStoreNetworks.paseo;
|
|
25
33
|
const storage = createNodeStorageAdapter(options.appId, options.storageDir);
|
|
26
34
|
const HEARTBEAT_NEVER_MS = 2147483647;
|
|
27
35
|
const rawLazyClient = createLazyClient(
|
|
@@ -78,209 +86,98 @@ function wrapLazyClient(inner) {
|
|
|
78
86
|
}
|
|
79
87
|
};
|
|
80
88
|
}
|
|
89
|
+
function isBenignTeardownError(error) {
|
|
90
|
+
const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
|
|
91
|
+
return /Client destroyed/.test(message);
|
|
92
|
+
}
|
|
93
|
+
function suppressBenignTeardownErrors() {
|
|
94
|
+
const original = console.error;
|
|
95
|
+
const wrapper = (...args) => {
|
|
96
|
+
if (args.some(isBenignTeardownError)) return;
|
|
97
|
+
original.apply(console, args);
|
|
98
|
+
};
|
|
99
|
+
console.error = wrapper;
|
|
100
|
+
return () => {
|
|
101
|
+
if (console.error === wrapper) console.error = original;
|
|
102
|
+
};
|
|
103
|
+
}
|
|
81
104
|
async function teardown(sessions, lazyClient) {
|
|
82
105
|
log.debug("destroying terminal adapter");
|
|
83
106
|
sessions.dispose();
|
|
84
107
|
await lazyClient.awaitPendingUnsubs();
|
|
108
|
+
const restore = suppressBenignTeardownErrors();
|
|
85
109
|
try {
|
|
86
110
|
lazyClient.disconnect();
|
|
87
111
|
} catch (e) {
|
|
88
112
|
log.warn("lazyClient.disconnect threw during destroy", { error: e });
|
|
113
|
+
} finally {
|
|
114
|
+
restore();
|
|
89
115
|
}
|
|
90
116
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
);
|
|
140
|
-
teardown2();
|
|
141
|
-
expect(fake.teardownCalls).toBe(1);
|
|
142
|
-
});
|
|
143
|
-
test("awaitPendingUnsubs resolves after all wrapped teardowns settle", async () => {
|
|
144
|
-
const fake = fakeLazyClient();
|
|
145
|
-
const wrapped = wrapLazyClient(fake.client);
|
|
146
|
-
const subscribe = wrapped.getSubscribeFn();
|
|
147
|
-
for (let i = 0; i < 3; i++) {
|
|
148
|
-
const teardown2 = subscribe(
|
|
149
|
-
"statement_subscribeStatement",
|
|
150
|
-
[],
|
|
151
|
-
() => {
|
|
152
|
-
},
|
|
153
|
-
() => {
|
|
154
|
-
}
|
|
155
|
-
);
|
|
156
|
-
teardown2();
|
|
157
|
-
}
|
|
158
|
-
await wrapped.awaitPendingUnsubs();
|
|
159
|
-
expect(fake.teardownCalls).toBe(3);
|
|
160
|
-
});
|
|
161
|
-
test("awaitPendingUnsubs with no pending unsubs resolves immediately", async () => {
|
|
162
|
-
const fake = fakeLazyClient();
|
|
163
|
-
const wrapped = wrapLazyClient(fake.client);
|
|
164
|
-
await expect(wrapped.awaitPendingUnsubs()).resolves.toBeUndefined();
|
|
165
|
-
});
|
|
166
|
-
test("an unsubscribe whose teardown throws is still tracked and resolved", async () => {
|
|
167
|
-
const fake = {
|
|
168
|
-
getClient: (() => ({})),
|
|
169
|
-
getRequestFn: (() => () => Promise.resolve()),
|
|
170
|
-
getSubscribeFn: () => ((_method, _params, _onMessage, _onError) => {
|
|
171
|
-
return () => {
|
|
172
|
-
throw new Error("teardown boom");
|
|
173
|
-
};
|
|
174
|
-
}),
|
|
175
|
-
disconnect: () => {
|
|
176
|
-
}
|
|
177
|
-
};
|
|
178
|
-
const wrapped = wrapLazyClient(fake);
|
|
179
|
-
const subscribe = wrapped.getSubscribeFn();
|
|
180
|
-
const teardown2 = subscribe(
|
|
181
|
-
"statement_subscribeStatement",
|
|
182
|
-
[],
|
|
183
|
-
() => {
|
|
184
|
-
},
|
|
185
|
-
() => {
|
|
186
|
-
}
|
|
187
|
-
);
|
|
188
|
-
try {
|
|
189
|
-
teardown2();
|
|
190
|
-
} catch {
|
|
191
|
-
}
|
|
192
|
-
await expect(wrapped.awaitPendingUnsubs()).resolves.toBeUndefined();
|
|
193
|
-
});
|
|
194
|
-
});
|
|
195
|
-
describe("teardown", () => {
|
|
196
|
-
test("orders sessions.dispose, drain, then disconnect", async () => {
|
|
197
|
-
const order = [];
|
|
198
|
-
const fake = fakeLazyClient();
|
|
199
|
-
const wrapped = wrapLazyClient(fake.client);
|
|
200
|
-
const innerDisconnect = wrapped.disconnect.bind(wrapped);
|
|
201
|
-
wrapped.disconnect = () => {
|
|
202
|
-
order.push("disconnect");
|
|
203
|
-
innerDisconnect();
|
|
204
|
-
};
|
|
205
|
-
const sessions = {
|
|
206
|
-
dispose: vi.fn(() => {
|
|
207
|
-
order.push("dispose");
|
|
208
|
-
const subscribe = wrapped.getSubscribeFn();
|
|
209
|
-
subscribe(
|
|
210
|
-
"statement_subscribeStatement",
|
|
211
|
-
[],
|
|
212
|
-
() => {
|
|
213
|
-
},
|
|
214
|
-
() => {
|
|
215
|
-
}
|
|
216
|
-
)();
|
|
217
|
-
})
|
|
218
|
-
};
|
|
219
|
-
await teardown(sessions, wrapped);
|
|
220
|
-
expect(order).toEqual(["dispose", "disconnect"]);
|
|
221
|
-
expect(sessions.dispose).toHaveBeenCalledTimes(1);
|
|
222
|
-
expect(fake.disconnectCalls).toBe(1);
|
|
223
|
-
});
|
|
224
|
-
test("disconnect runs even when there are no pending unsubs", async () => {
|
|
225
|
-
const fake = fakeLazyClient();
|
|
226
|
-
const wrapped = wrapLazyClient(fake.client);
|
|
227
|
-
const sessions = { dispose: vi.fn() };
|
|
228
|
-
await teardown(sessions, wrapped);
|
|
229
|
-
expect(fake.disconnectCalls).toBe(1);
|
|
230
|
-
});
|
|
231
|
-
test("logs a warning if disconnect throws, doesn't propagate to caller", async () => {
|
|
232
|
-
const fake = {
|
|
233
|
-
getClient: (() => ({})),
|
|
234
|
-
getRequestFn: (() => () => Promise.resolve()),
|
|
235
|
-
getSubscribeFn: () => ((_method, _params, _onMessage, _onError) => () => {
|
|
236
|
-
}),
|
|
237
|
-
disconnect: () => {
|
|
238
|
-
throw new Error("boom");
|
|
239
|
-
}
|
|
240
|
-
};
|
|
241
|
-
const wrapped = wrapLazyClient(fake);
|
|
242
|
-
const sessions = { dispose: vi.fn() };
|
|
243
|
-
await expect(teardown(sessions, wrapped)).resolves.toBeUndefined();
|
|
244
|
-
});
|
|
245
|
-
test("awaits pending unsubs before calling disconnect", async () => {
|
|
246
|
-
const fake = fakeLazyClient();
|
|
247
|
-
const wrapped = wrapLazyClient(fake.client);
|
|
248
|
-
let unsubResolved = false;
|
|
249
|
-
const innerDisconnect = wrapped.disconnect.bind(wrapped);
|
|
250
|
-
wrapped.disconnect = () => {
|
|
251
|
-
expect(unsubResolved).toBe(true);
|
|
252
|
-
innerDisconnect();
|
|
253
|
-
};
|
|
254
|
-
const sessions = {
|
|
255
|
-
dispose: () => {
|
|
256
|
-
const subscribe = wrapped.getSubscribeFn();
|
|
257
|
-
subscribe(
|
|
258
|
-
"statement_subscribeStatement",
|
|
259
|
-
[],
|
|
260
|
-
() => {
|
|
261
|
-
},
|
|
262
|
-
() => {
|
|
263
|
-
}
|
|
264
|
-
)();
|
|
265
|
-
queueMicrotask(
|
|
266
|
-
() => queueMicrotask(() => {
|
|
267
|
-
unsubResolved = true;
|
|
268
|
-
})
|
|
269
|
-
);
|
|
270
|
-
}
|
|
271
|
-
};
|
|
272
|
-
await teardown(sessions, wrapped);
|
|
273
|
-
expect(fake.disconnectCalls).toBe(1);
|
|
274
|
-
});
|
|
117
|
+
var log2 = createLogger("terminal");
|
|
118
|
+
var CACHE_KIND = "ProductSubtrees";
|
|
119
|
+
var CACHE_VERSION = 1;
|
|
120
|
+
var SUBTREE_KEY_BYTES = 32;
|
|
121
|
+
var memo = /* @__PURE__ */ new Map();
|
|
122
|
+
function entryKey(session, productId) {
|
|
123
|
+
return `${session.id}::${productId}`;
|
|
124
|
+
}
|
|
125
|
+
async function requestSubtree(session, productId) {
|
|
126
|
+
const result = await session.getProductSubtree(productId);
|
|
127
|
+
if (result.isErr()) {
|
|
128
|
+
throw new Error(`Could not fetch the product subtree key: ${result.error.message}`);
|
|
129
|
+
}
|
|
130
|
+
const key = new Uint8Array(result.value);
|
|
131
|
+
if (key.length !== SUBTREE_KEY_BYTES) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`Product subtree key must be ${SUBTREE_KEY_BYTES} bytes, got ${key.length}`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
return key;
|
|
137
|
+
}
|
|
138
|
+
function getProductSubtreePublicKey(session, productId, options = {}) {
|
|
139
|
+
const path = cacheFilePath(options.appId ?? productId, CACHE_KIND, options.storageDir);
|
|
140
|
+
const key = `${path}::${entryKey(session, productId)}`;
|
|
141
|
+
const inFlight = memo.get(key);
|
|
142
|
+
if (inFlight) return inFlight;
|
|
143
|
+
const pending = resolveSubtree(session, productId, path, entryKey(session, productId));
|
|
144
|
+
memo.set(key, pending);
|
|
145
|
+
pending.catch(() => memo.delete(key));
|
|
146
|
+
return pending;
|
|
147
|
+
}
|
|
148
|
+
async function resolveSubtree(session, productId, path, entry) {
|
|
149
|
+
return withFileLock(path, async () => {
|
|
150
|
+
const cache = await loadJsonCache(
|
|
151
|
+
path,
|
|
152
|
+
CACHE_VERSION,
|
|
153
|
+
"product-subtree cache"
|
|
154
|
+
);
|
|
155
|
+
const stored = readEntry(cache, entry, path);
|
|
156
|
+
if (stored) return stored;
|
|
157
|
+
const fetched = await requestSubtree(session, productId);
|
|
158
|
+
const entries = typeof cache?.entries === "object" && cache.entries !== null ? cache.entries : {};
|
|
159
|
+
await saveJsonCache(path, {
|
|
160
|
+
version: CACHE_VERSION,
|
|
161
|
+
entries: { ...entries, [entry]: toHex(fetched) }
|
|
162
|
+
});
|
|
163
|
+
return fetched;
|
|
275
164
|
});
|
|
276
165
|
}
|
|
277
|
-
|
|
166
|
+
function readEntry(cache, entry, path) {
|
|
167
|
+
const stored = cache?.entries?.[entry];
|
|
168
|
+
if (typeof stored !== "string") return null;
|
|
169
|
+
let bytes;
|
|
170
|
+
try {
|
|
171
|
+
bytes = fromHex(stored);
|
|
172
|
+
} catch {
|
|
173
|
+
bytes = new Uint8Array();
|
|
174
|
+
}
|
|
175
|
+
if (bytes.length === SUBTREE_KEY_BYTES) return bytes;
|
|
176
|
+
log2.warn("product-subtree cache entry is malformed; refetching", { path });
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
278
179
|
|
|
279
180
|
// src/signer.ts
|
|
280
|
-
import { NoAllowanceError } from "@novasamatech/statement-store";
|
|
281
|
-
import { decAnyMetadata, unifyMetadata } from "@polkadot-api/substrate-bindings";
|
|
282
|
-
import { deriveProductAccountPublicKey } from "@parity/product-sdk-keys";
|
|
283
|
-
import { AllowanceExpiredError } from "@parity/product-sdk-signer/errors";
|
|
284
181
|
function isNoAllowanceError(error) {
|
|
285
182
|
for (let current = error, depth = 0; current != null && depth < 8; depth++) {
|
|
286
183
|
if (current instanceof NoAllowanceError) return true;
|
|
@@ -298,6 +195,9 @@ function isNoAllowanceError(error) {
|
|
|
298
195
|
function toAllowanceExpiredError(error) {
|
|
299
196
|
return isNoAllowanceError(error) ? new AllowanceExpiredError("statementStore", error) : null;
|
|
300
197
|
}
|
|
198
|
+
function toProductAccountId(ref) {
|
|
199
|
+
return [ref.productId, { tag: "Index", value: ref.derivationIndex }];
|
|
200
|
+
}
|
|
301
201
|
function buildCreateTransactionRequest(productAccountId, callData, signedExtensions, txExtVersion) {
|
|
302
202
|
const checkGenesis = signedExtensions.CheckGenesis;
|
|
303
203
|
if (!checkGenesis) {
|
|
@@ -311,7 +211,8 @@ function buildCreateTransactionRequest(productAccountId, callData, signedExtensi
|
|
|
311
211
|
value: {
|
|
312
212
|
signer: productAccountId,
|
|
313
213
|
// CheckGenesis carries the genesis hash as its additionalSigned (implicit) data.
|
|
314
|
-
|
|
214
|
+
// `toHex` is typed `string`; the codec wants the 0x literal type.
|
|
215
|
+
genesisHash: toHex(checkGenesis.additionalSigned),
|
|
315
216
|
callData,
|
|
316
217
|
extensions: Object.values(signedExtensions).map(
|
|
317
218
|
({ identifier, value, additionalSigned }) => ({
|
|
@@ -325,11 +226,6 @@ function buildCreateTransactionRequest(productAccountId, callData, signedExtensi
|
|
|
325
226
|
}
|
|
326
227
|
};
|
|
327
228
|
}
|
|
328
|
-
function txExtVersionFromMetadata(metadata) {
|
|
329
|
-
const decoded = unifyMetadata(decAnyMetadata(metadata));
|
|
330
|
-
const latestVersion = decoded.extrinsic.version.reduce((max, v) => Math.max(max, v), 0);
|
|
331
|
-
return latestVersion === 4 ? 0 : latestVersion;
|
|
332
|
-
}
|
|
333
229
|
async function requestSignedTransaction(session, request) {
|
|
334
230
|
const result = await session.createTransaction(request);
|
|
335
231
|
if (result.isErr()) {
|
|
@@ -339,14 +235,14 @@ async function requestSignedTransaction(session, request) {
|
|
|
339
235
|
}
|
|
340
236
|
return result.value;
|
|
341
237
|
}
|
|
342
|
-
function makeTxSignTx(session, productAccountId) {
|
|
238
|
+
function makeTxSignTx(session, productAccountId, txExtVersion) {
|
|
343
239
|
return async (callData, signedExtensions, metadata) => requestSignedTransaction(
|
|
344
240
|
session,
|
|
345
241
|
buildCreateTransactionRequest(
|
|
346
242
|
productAccountId,
|
|
347
243
|
callData,
|
|
348
244
|
signedExtensions,
|
|
349
|
-
|
|
245
|
+
txExtVersion ?? resolveTxExtVersion(metadata, signedExtensions)
|
|
350
246
|
)
|
|
351
247
|
);
|
|
352
248
|
}
|
|
@@ -373,390 +269,35 @@ function sessionRootPublicKey(session) {
|
|
|
373
269
|
}
|
|
374
270
|
return publicKey;
|
|
375
271
|
}
|
|
376
|
-
function deriveProductPublicKey(session, ref) {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
ref.productId,
|
|
380
|
-
ref.derivationIndex
|
|
381
|
-
);
|
|
272
|
+
async function deriveProductPublicKey(session, ref, options) {
|
|
273
|
+
const subtree = await getProductSubtreePublicKey(session, ref.productId, options);
|
|
274
|
+
return deriveProductAccountPublicKey(subtree, { tag: "Index", value: ref.derivationIndex });
|
|
382
275
|
}
|
|
383
|
-
function buildSessionSigner(session, ref) {
|
|
384
|
-
const productAccountId =
|
|
385
|
-
const publicKey = ref.publicKey ?? deriveProductPublicKey(session, ref);
|
|
276
|
+
async function buildSessionSigner(session, ref, options) {
|
|
277
|
+
const productAccountId = toProductAccountId(ref);
|
|
278
|
+
const publicKey = ref.publicKey ?? await deriveProductPublicKey(session, ref, options);
|
|
386
279
|
return {
|
|
387
280
|
publicKey,
|
|
388
281
|
// Transaction signing → host-papp `createTransaction`: the wallet builds
|
|
389
282
|
// and signs the extrinsic, so it can show a decoded tx (no `<Bytes>`
|
|
390
283
|
// envelope) and unknown signed extensions (e.g. `AsPgas`) survive.
|
|
391
|
-
signTx: makeTxSignTx(session, productAccountId),
|
|
284
|
+
signTx: makeTxSignTx(session, productAccountId, options?.txExtVersion),
|
|
392
285
|
// Arbitrary-byte signing keeps the `Bytes` tag so the wallet applies the
|
|
393
286
|
// `<Bytes>...</Bytes>` anti-phishing envelope exactly once on its side
|
|
394
287
|
// (correct for non-extrinsic user data).
|
|
395
288
|
signBytes: makeRawBytesSignCallback(session, productAccountId)
|
|
396
289
|
};
|
|
397
290
|
}
|
|
398
|
-
function createSessionSigner(session, adapter, publicKey) {
|
|
399
|
-
return buildSessionSigner(
|
|
291
|
+
function createSessionSigner(session, adapter, publicKey, options) {
|
|
292
|
+
return buildSessionSigner(
|
|
293
|
+
session,
|
|
294
|
+
{ productId: adapter.appId, derivationIndex: 0, publicKey },
|
|
295
|
+
{ appId: adapter.appId, ...options }
|
|
296
|
+
);
|
|
400
297
|
}
|
|
401
|
-
function createSessionSignerForAccount(session, ref) {
|
|
402
|
-
return buildSessionSigner(session, ref);
|
|
298
|
+
function createSessionSignerForAccount(session, ref, options) {
|
|
299
|
+
return buildSessionSigner(session, ref, options);
|
|
403
300
|
}
|
|
404
|
-
if (void 0) {
|
|
405
|
-
let makeSession = function(opts) {
|
|
406
|
-
const accountIdBytes = opts.accountIdBytes ?? new Array(32).fill(0).map((_, i) => i);
|
|
407
|
-
const rootAccountId = opts.rootAccountId === void 0 ? Array.from(DEV_ROOT_PUBLIC_KEY) : opts.rootAccountId;
|
|
408
|
-
return {
|
|
409
|
-
remoteAccount: { accountId: accountIdBytes },
|
|
410
|
-
...rootAccountId === null ? {} : { rootAccountId: new Uint8Array(rootAccountId) },
|
|
411
|
-
signPayload: vi.fn(
|
|
412
|
-
opts.signPayload ?? (async () => {
|
|
413
|
-
throw new Error("signPayload not stubbed in this test");
|
|
414
|
-
})
|
|
415
|
-
),
|
|
416
|
-
signRaw: vi.fn(
|
|
417
|
-
opts.signRaw ?? (async () => {
|
|
418
|
-
throw new Error("signRaw not stubbed in this test");
|
|
419
|
-
})
|
|
420
|
-
),
|
|
421
|
-
createTransaction: vi.fn(
|
|
422
|
-
opts.createTransaction ?? (async () => {
|
|
423
|
-
throw new Error("createTransaction not stubbed in this test");
|
|
424
|
-
})
|
|
425
|
-
)
|
|
426
|
-
};
|
|
427
|
-
}, ext = function(identifier, value, additionalSigned) {
|
|
428
|
-
return {
|
|
429
|
-
identifier,
|
|
430
|
-
value: new Uint8Array(value),
|
|
431
|
-
additionalSigned: new Uint8Array(additionalSigned)
|
|
432
|
-
};
|
|
433
|
-
}, fakeAdapter = function(appId) {
|
|
434
|
-
return { appId };
|
|
435
|
-
};
|
|
436
|
-
makeSession2 = makeSession, ext2 = ext, fakeAdapter2 = fakeAdapter;
|
|
437
|
-
const { describe, test, expect, vi } = void 0;
|
|
438
|
-
const { ok, err } = await null;
|
|
439
|
-
const { seedToAccount } = await null;
|
|
440
|
-
const DEV_ROOT_PUBLIC_KEY = seedToAccount(
|
|
441
|
-
"bottom drive obey lake curtain smoke basket hold race lonely fit walk",
|
|
442
|
-
""
|
|
443
|
-
).publicKey;
|
|
444
|
-
describe("createSessionSigner", () => {
|
|
445
|
-
test("self-derives the product key from the session root when none is passed", () => {
|
|
446
|
-
const walletBytes = Array.from({ length: 32 }, (_, i) => i);
|
|
447
|
-
const signer = createSessionSigner(
|
|
448
|
-
makeSession({
|
|
449
|
-
accountIdBytes: walletBytes,
|
|
450
|
-
rootAccountId: Array.from(DEV_ROOT_PUBLIC_KEY)
|
|
451
|
-
}),
|
|
452
|
-
fakeAdapter("test-app")
|
|
453
|
-
);
|
|
454
|
-
const expected = deriveProductAccountPublicKey(DEV_ROOT_PUBLIC_KEY, "test-app", 0);
|
|
455
|
-
expect(signer.publicKey).toEqual(expected);
|
|
456
|
-
expect(signer.publicKey).not.toEqual(new Uint8Array(walletBytes));
|
|
457
|
-
});
|
|
458
|
-
test("throws on a stale session with no root account key", () => {
|
|
459
|
-
expect(
|
|
460
|
-
() => createSessionSigner(makeSession({ rootAccountId: null }), fakeAdapter("test-app"))
|
|
461
|
-
).toThrow(INCOMPLETE_SESSION_MESSAGE);
|
|
462
|
-
});
|
|
463
|
-
test("uses the host-supplied product-account public key when provided", () => {
|
|
464
|
-
const walletBytes = Array.from({ length: 32 }, (_, i) => i);
|
|
465
|
-
const productKey = new Uint8Array(32).fill(171);
|
|
466
|
-
const signer = createSessionSigner(
|
|
467
|
-
makeSession({ accountIdBytes: walletBytes }),
|
|
468
|
-
fakeAdapter("test-app"),
|
|
469
|
-
productKey
|
|
470
|
-
);
|
|
471
|
-
expect(signer.publicKey).toEqual(productKey);
|
|
472
|
-
expect(signer.publicKey).not.toEqual(new Uint8Array(walletBytes));
|
|
473
|
-
});
|
|
474
|
-
test("signBytes routes through session.signRaw with the Bytes tag", async () => {
|
|
475
|
-
const sig = new Uint8Array([9, 8, 7, 6, 5, 4, 3, 2, 1]);
|
|
476
|
-
const captured = [];
|
|
477
|
-
const session = makeSession({
|
|
478
|
-
signRaw: async (req2) => {
|
|
479
|
-
captured.push(req2);
|
|
480
|
-
return ok({ signature: sig });
|
|
481
|
-
}
|
|
482
|
-
});
|
|
483
|
-
const signer = createSessionSigner(session, fakeAdapter("test-app"));
|
|
484
|
-
const out = await signer.signBytes(new Uint8Array([1, 2, 3]));
|
|
485
|
-
expect(out).toEqual(sig);
|
|
486
|
-
expect(captured).toHaveLength(1);
|
|
487
|
-
const req = captured[0];
|
|
488
|
-
expect(req.productAccountId).toEqual(["test-app", 0]);
|
|
489
|
-
expect(req.data.tag).toBe("Bytes");
|
|
490
|
-
expect(req.data.value).toEqual(new Uint8Array([1, 2, 3]));
|
|
491
|
-
});
|
|
492
|
-
test("signBytes never calls session.signPayload (legacy-path regression guard)", async () => {
|
|
493
|
-
const session = makeSession({
|
|
494
|
-
signRaw: async () => ok({ signature: new Uint8Array([1]) })
|
|
495
|
-
});
|
|
496
|
-
const signer = createSessionSigner(session, fakeAdapter("test-app"));
|
|
497
|
-
await signer.signBytes(new Uint8Array([1, 2, 3]));
|
|
498
|
-
const sessionWithSpies = session;
|
|
499
|
-
expect(sessionWithSpies.signPayload.mock.calls).toHaveLength(0);
|
|
500
|
-
expect(sessionWithSpies.signRaw.mock.calls).toHaveLength(1);
|
|
501
|
-
});
|
|
502
|
-
test("signBytes throws with a clear error when the mobile rejects", async () => {
|
|
503
|
-
const session = makeSession({
|
|
504
|
-
signRaw: async () => err({ message: "user declined" })
|
|
505
|
-
});
|
|
506
|
-
const signer = createSessionSigner(session, fakeAdapter("test-app"));
|
|
507
|
-
await expect(signer.signBytes(new Uint8Array([1]))).rejects.toThrow(
|
|
508
|
-
"Mobile signing rejected: user declined"
|
|
509
|
-
);
|
|
510
|
-
});
|
|
511
|
-
});
|
|
512
|
-
describe("buildCreateTransactionRequest \u2014 tx payload (the AsPgas fix)", () => {
|
|
513
|
-
const checkGenesis = ext("CheckGenesis", [], [17, 34, 51]);
|
|
514
|
-
test("wraps the payload as v1 with signer, callData, and txExtVersion", () => {
|
|
515
|
-
const callData = new Uint8Array([202, 17]);
|
|
516
|
-
const req = buildCreateTransactionRequest(
|
|
517
|
-
["my-app", 3],
|
|
518
|
-
callData,
|
|
519
|
-
{ CheckGenesis: checkGenesis },
|
|
520
|
-
5
|
|
521
|
-
);
|
|
522
|
-
expect(req.payload.tag).toBe("v1");
|
|
523
|
-
expect(req.payload.value.signer).toEqual(["my-app", 3]);
|
|
524
|
-
expect(req.payload.value.callData).toEqual(callData);
|
|
525
|
-
expect(req.payload.value.txExtVersion).toBe(5);
|
|
526
|
-
});
|
|
527
|
-
test("takes the genesis hash from CheckGenesis.additionalSigned", () => {
|
|
528
|
-
const req = buildCreateTransactionRequest(
|
|
529
|
-
["my-app", 0],
|
|
530
|
-
new Uint8Array([0]),
|
|
531
|
-
{ CheckGenesis: checkGenesis },
|
|
532
|
-
0
|
|
533
|
-
);
|
|
534
|
-
expect(req.payload.value.genesisHash).toEqual(new Uint8Array([17, 34, 51]));
|
|
535
|
-
});
|
|
536
|
-
test("maps every signed extension to { id, extra, additionalSigned }", () => {
|
|
537
|
-
const req = buildCreateTransactionRequest(
|
|
538
|
-
["my-app", 0],
|
|
539
|
-
new Uint8Array([0]),
|
|
540
|
-
{ CheckGenesis: checkGenesis, CheckNonce: ext("CheckNonce", [7], []) },
|
|
541
|
-
0
|
|
542
|
-
);
|
|
543
|
-
expect(req.payload.value.extensions).toEqual([
|
|
544
|
-
{
|
|
545
|
-
id: "CheckGenesis",
|
|
546
|
-
extra: new Uint8Array([]),
|
|
547
|
-
additionalSigned: new Uint8Array([17, 34, 51])
|
|
548
|
-
},
|
|
549
|
-
{
|
|
550
|
-
id: "CheckNonce",
|
|
551
|
-
extra: new Uint8Array([7]),
|
|
552
|
-
additionalSigned: new Uint8Array([])
|
|
553
|
-
}
|
|
554
|
-
]);
|
|
555
|
-
});
|
|
556
|
-
test("preserves an unknown signed extension verbatim (e.g. AsPgas)", () => {
|
|
557
|
-
const req = buildCreateTransactionRequest(
|
|
558
|
-
["my-app", 0],
|
|
559
|
-
new Uint8Array([0]),
|
|
560
|
-
{ CheckGenesis: checkGenesis, AsPgas: ext("AsPgas", [222, 173], [190, 239]) },
|
|
561
|
-
0
|
|
562
|
-
);
|
|
563
|
-
expect(req.payload.value.extensions).toContainEqual({
|
|
564
|
-
id: "AsPgas",
|
|
565
|
-
extra: new Uint8Array([222, 173]),
|
|
566
|
-
additionalSigned: new Uint8Array([190, 239])
|
|
567
|
-
});
|
|
568
|
-
});
|
|
569
|
-
test("throws a clear error when CheckGenesis is absent", () => {
|
|
570
|
-
expect(
|
|
571
|
-
() => buildCreateTransactionRequest(["my-app", 0], new Uint8Array([0]), {}, 0)
|
|
572
|
-
).toThrow(/CheckGenesis/);
|
|
573
|
-
});
|
|
574
|
-
});
|
|
575
|
-
describe("requestSignedTransaction \u2014 SSO round-trip", () => {
|
|
576
|
-
const request = buildCreateTransactionRequest(
|
|
577
|
-
["my-app", 0],
|
|
578
|
-
new Uint8Array([0]),
|
|
579
|
-
{ CheckGenesis: ext("CheckGenesis", [], [1]) },
|
|
580
|
-
0
|
|
581
|
-
);
|
|
582
|
-
test("returns the signed extrinsic bytes from the mobile response", async () => {
|
|
583
|
-
const signed = new Uint8Array([171, 205, 239]);
|
|
584
|
-
const session = makeSession({ createTransaction: async () => ok(signed) });
|
|
585
|
-
const out = await requestSignedTransaction(session, request);
|
|
586
|
-
expect(out).toBe(signed);
|
|
587
|
-
});
|
|
588
|
-
test("calls session.createTransaction \u2014 never signRaw/signPayload \u2014 with the request", async () => {
|
|
589
|
-
const captured = [];
|
|
590
|
-
const session = makeSession({
|
|
591
|
-
createTransaction: async (req) => {
|
|
592
|
-
captured.push(req);
|
|
593
|
-
return ok(new Uint8Array([1]));
|
|
594
|
-
}
|
|
595
|
-
});
|
|
596
|
-
await requestSignedTransaction(session, request);
|
|
597
|
-
expect(captured).toEqual([request]);
|
|
598
|
-
const spies = session;
|
|
599
|
-
expect(spies.createTransaction.mock.calls).toHaveLength(1);
|
|
600
|
-
expect(spies.signPayload.mock.calls).toHaveLength(0);
|
|
601
|
-
expect(spies.signRaw.mock.calls).toHaveLength(0);
|
|
602
|
-
});
|
|
603
|
-
test("throws a clear error when the mobile rejects", async () => {
|
|
604
|
-
const session = makeSession({
|
|
605
|
-
createTransaction: async () => err({ message: "user declined" })
|
|
606
|
-
});
|
|
607
|
-
await expect(requestSignedTransaction(session, request)).rejects.toThrow(
|
|
608
|
-
"Mobile transaction signing rejected: user declined"
|
|
609
|
-
);
|
|
610
|
-
});
|
|
611
|
-
test("throws AllowanceExpiredError when the failure is a NoAllowanceError", async () => {
|
|
612
|
-
const underlying = new NoAllowanceError();
|
|
613
|
-
const session = makeSession({
|
|
614
|
-
createTransaction: async () => err(underlying)
|
|
615
|
-
});
|
|
616
|
-
await expect(requestSignedTransaction(session, request)).rejects.toBeInstanceOf(
|
|
617
|
-
AllowanceExpiredError
|
|
618
|
-
);
|
|
619
|
-
await expect(requestSignedTransaction(session, request)).rejects.toMatchObject({
|
|
620
|
-
name: "AllowanceExpiredError",
|
|
621
|
-
resource: "statementStore",
|
|
622
|
-
cause: underlying
|
|
623
|
-
});
|
|
624
|
-
});
|
|
625
|
-
test("throws AllowanceExpiredError for a wire-serialized failure (message match only)", async () => {
|
|
626
|
-
expect(new NoAllowanceError().message).toBe(
|
|
627
|
-
"Submit failed, no allowance set for account"
|
|
628
|
-
);
|
|
629
|
-
const serialized = new Error("Submit failed, no allowance set for account");
|
|
630
|
-
const session = makeSession({
|
|
631
|
-
createTransaction: async () => err(serialized)
|
|
632
|
-
});
|
|
633
|
-
await expect(requestSignedTransaction(session, request)).rejects.toBeInstanceOf(
|
|
634
|
-
AllowanceExpiredError
|
|
635
|
-
);
|
|
636
|
-
});
|
|
637
|
-
test("throws AllowanceExpiredError when the NoAllowanceError is wrapped as a cause", async () => {
|
|
638
|
-
const wrapped = new Error("submitRequest failed", { cause: new NoAllowanceError() });
|
|
639
|
-
const session = makeSession({
|
|
640
|
-
createTransaction: async () => err(wrapped)
|
|
641
|
-
});
|
|
642
|
-
await expect(requestSignedTransaction(session, request)).rejects.toBeInstanceOf(
|
|
643
|
-
AllowanceExpiredError
|
|
644
|
-
);
|
|
645
|
-
});
|
|
646
|
-
test("a non-allowance Error still surfaces as the generic rejection", async () => {
|
|
647
|
-
const session = makeSession({
|
|
648
|
-
createTransaction: async () => err(new Error("transport exploded"))
|
|
649
|
-
});
|
|
650
|
-
await expect(requestSignedTransaction(session, request)).rejects.toThrow(
|
|
651
|
-
"Mobile transaction signing rejected: transport exploded"
|
|
652
|
-
);
|
|
653
|
-
});
|
|
654
|
-
});
|
|
655
|
-
describe("makeRawBytesSignCallback", () => {
|
|
656
|
-
test("forwards data verbatim under the Bytes tag with the right productAccountId", async () => {
|
|
657
|
-
const captured = [];
|
|
658
|
-
const session = makeSession({
|
|
659
|
-
signRaw: async (req2) => {
|
|
660
|
-
captured.push(req2);
|
|
661
|
-
return ok({ signature: new Uint8Array([255]) });
|
|
662
|
-
}
|
|
663
|
-
});
|
|
664
|
-
const callback = makeRawBytesSignCallback(session, ["my-app", 5]);
|
|
665
|
-
await callback(new Uint8Array([222, 173, 190, 239]));
|
|
666
|
-
expect(captured).toHaveLength(1);
|
|
667
|
-
const req = captured[0];
|
|
668
|
-
expect(req.productAccountId).toEqual(["my-app", 5]);
|
|
669
|
-
expect(req.data.tag).toBe("Bytes");
|
|
670
|
-
expect(Array.from(req.data.value)).toEqual([222, 173, 190, 239]);
|
|
671
|
-
});
|
|
672
|
-
test("returns the raw signature bytes from the mobile response", async () => {
|
|
673
|
-
const sig = new Uint8Array([66]);
|
|
674
|
-
const session = makeSession({
|
|
675
|
-
signRaw: async () => ok({ signature: sig })
|
|
676
|
-
});
|
|
677
|
-
const callback = makeRawBytesSignCallback(session, ["my-app", 0]);
|
|
678
|
-
const out = await callback(new Uint8Array([0]));
|
|
679
|
-
expect(out).toBe(sig);
|
|
680
|
-
});
|
|
681
|
-
test("throws AllowanceExpiredError when the failure is a NoAllowanceError", async () => {
|
|
682
|
-
const underlying = new NoAllowanceError();
|
|
683
|
-
const session = makeSession({
|
|
684
|
-
signRaw: async () => err(underlying)
|
|
685
|
-
});
|
|
686
|
-
const callback = makeRawBytesSignCallback(session, ["my-app", 0]);
|
|
687
|
-
await expect(callback(new Uint8Array([1]))).rejects.toBeInstanceOf(
|
|
688
|
-
AllowanceExpiredError
|
|
689
|
-
);
|
|
690
|
-
await expect(callback(new Uint8Array([1]))).rejects.toMatchObject({
|
|
691
|
-
resource: "statementStore",
|
|
692
|
-
cause: underlying
|
|
693
|
-
});
|
|
694
|
-
});
|
|
695
|
-
test("a non-allowance failure still surfaces as the generic rejection", async () => {
|
|
696
|
-
const session = makeSession({
|
|
697
|
-
signRaw: async () => err(new Error("user declined"))
|
|
698
|
-
});
|
|
699
|
-
const callback = makeRawBytesSignCallback(session, ["my-app", 0]);
|
|
700
|
-
await expect(callback(new Uint8Array([1]))).rejects.toThrow(
|
|
701
|
-
"Mobile signing rejected: user declined"
|
|
702
|
-
);
|
|
703
|
-
});
|
|
704
|
-
});
|
|
705
|
-
describe("createSessionSignerForAccount", () => {
|
|
706
|
-
test("forwards productAccountId from the explicit ref", async () => {
|
|
707
|
-
const captured = [];
|
|
708
|
-
const session = makeSession({
|
|
709
|
-
signRaw: async (req2) => {
|
|
710
|
-
captured.push(req2);
|
|
711
|
-
return ok({ signature: new Uint8Array([42]) });
|
|
712
|
-
}
|
|
713
|
-
});
|
|
714
|
-
const signer = createSessionSignerForAccount(session, {
|
|
715
|
-
productId: "my-app",
|
|
716
|
-
derivationIndex: 7
|
|
717
|
-
});
|
|
718
|
-
await signer.signBytes(new Uint8Array([10, 20, 30]));
|
|
719
|
-
expect(captured).toHaveLength(1);
|
|
720
|
-
const req = captured[0];
|
|
721
|
-
expect(req.productAccountId).toEqual(["my-app", 7]);
|
|
722
|
-
expect(req.data.tag).toBe("Bytes");
|
|
723
|
-
expect(req.data.value).toBeInstanceOf(Uint8Array);
|
|
724
|
-
});
|
|
725
|
-
test("supports a productId different from any adapter's appId", async () => {
|
|
726
|
-
const captured = [];
|
|
727
|
-
const session = makeSession({
|
|
728
|
-
signRaw: async (req2) => {
|
|
729
|
-
captured.push(req2);
|
|
730
|
-
return ok({ signature: new Uint8Array([0]) });
|
|
731
|
-
}
|
|
732
|
-
});
|
|
733
|
-
const signer = createSessionSignerForAccount(session, {
|
|
734
|
-
productId: "external-product",
|
|
735
|
-
derivationIndex: 0
|
|
736
|
-
});
|
|
737
|
-
await signer.signBytes(new Uint8Array([1]));
|
|
738
|
-
const req = captured[0];
|
|
739
|
-
expect(req.productAccountId).toEqual(["external-product", 0]);
|
|
740
|
-
});
|
|
741
|
-
});
|
|
742
|
-
}
|
|
743
|
-
var makeSession2;
|
|
744
|
-
var ext2;
|
|
745
|
-
var fakeAdapter2;
|
|
746
|
-
|
|
747
|
-
// src/index.ts
|
|
748
|
-
import { AllowanceExpiredError as AllowanceExpiredError2, SignerError } from "@parity/product-sdk-signer/errors";
|
|
749
|
-
|
|
750
|
-
// src/allowance.ts
|
|
751
|
-
import { AllowanceError } from "@novasamatech/host-papp";
|
|
752
|
-
|
|
753
|
-
// src/allowance-cache.ts
|
|
754
|
-
import { gcm } from "@noble/ciphers/aes.js";
|
|
755
|
-
import { blake2b } from "@noble/hashes/blake2.js";
|
|
756
|
-
import { fromHex, toHex } from "@polkadot-api/utils";
|
|
757
|
-
import { readFile, writeFile } from "fs/promises";
|
|
758
|
-
import { join } from "path";
|
|
759
|
-
import { Bytes, Enum, Struct, Vector, _void, str } from "scale-ts";
|
|
760
301
|
var AllowanceResourceKindCodec = Enum({
|
|
761
302
|
bulletin: _void,
|
|
762
303
|
statementStore: _void
|
|
@@ -842,154 +383,6 @@ async function hasBulletinAllowance(adapter, productId, sessionId) {
|
|
|
842
383
|
async function hasStatementStoreAllowance(adapter, productId, sessionId) {
|
|
843
384
|
return hasAllowanceForResource(adapter, productId, "statementStore", sessionId);
|
|
844
385
|
}
|
|
845
|
-
if (void 0) {
|
|
846
|
-
let makeAdapter = function(opts) {
|
|
847
|
-
return {
|
|
848
|
-
sessions: {
|
|
849
|
-
sessions: {
|
|
850
|
-
read: () => opts.sessions
|
|
851
|
-
}
|
|
852
|
-
},
|
|
853
|
-
allowance: {
|
|
854
|
-
getBulletinSigner: opts.getBulletinSigner ?? vi.fn(() => ok({})),
|
|
855
|
-
getStatementStoreProver: opts.getStatementStoreProver ?? vi.fn(() => ok({}))
|
|
856
|
-
}
|
|
857
|
-
// The convenience never touches the rest of the adapter; leave it unstubbed.
|
|
858
|
-
};
|
|
859
|
-
};
|
|
860
|
-
makeAdapter2 = makeAdapter;
|
|
861
|
-
const { describe, test, expect, vi } = void 0;
|
|
862
|
-
const { ok, err } = await null;
|
|
863
|
-
describe("getBulletinSigner \u2014 sessionId defaulting", () => {
|
|
864
|
-
test("uses the only paired session when sessionId is omitted", async () => {
|
|
865
|
-
const fakeSigner = { _tag: "signer" };
|
|
866
|
-
const getBulletinSignerFn = vi.fn(() => ok(fakeSigner));
|
|
867
|
-
const adapter = makeAdapter({
|
|
868
|
-
sessions: [{ id: "sess-only" }],
|
|
869
|
-
getBulletinSigner: getBulletinSignerFn
|
|
870
|
-
});
|
|
871
|
-
const result = await getBulletinSigner(adapter, "my-app.dot");
|
|
872
|
-
expect(result).toBe(fakeSigner);
|
|
873
|
-
expect(getBulletinSignerFn).toHaveBeenCalledWith("sess-only", "my-app.dot");
|
|
874
|
-
});
|
|
875
|
-
test("uses the explicit sessionId when supplied", async () => {
|
|
876
|
-
const fakeSigner = { _tag: "signer" };
|
|
877
|
-
const getBulletinSignerFn = vi.fn(() => ok(fakeSigner));
|
|
878
|
-
const adapter = makeAdapter({
|
|
879
|
-
sessions: [{ id: "sess-a" }, { id: "sess-b" }],
|
|
880
|
-
getBulletinSigner: getBulletinSignerFn
|
|
881
|
-
});
|
|
882
|
-
await getBulletinSigner(adapter, "my-app.dot", "sess-b");
|
|
883
|
-
expect(getBulletinSignerFn).toHaveBeenCalledWith("sess-b", "my-app.dot");
|
|
884
|
-
});
|
|
885
|
-
test("throws AllowanceError('NoSession') when zero sessions and no id", async () => {
|
|
886
|
-
const adapter = makeAdapter({ sessions: [] });
|
|
887
|
-
await expect(getBulletinSigner(adapter, "my-app.dot")).rejects.toBeInstanceOf(
|
|
888
|
-
AllowanceError
|
|
889
|
-
);
|
|
890
|
-
await expect(getBulletinSigner(adapter, "my-app.dot")).rejects.toMatchObject({
|
|
891
|
-
reason: "NoSession"
|
|
892
|
-
});
|
|
893
|
-
});
|
|
894
|
-
test("throws AllowanceError('NoSession') when multiple sessions and no id", async () => {
|
|
895
|
-
const adapter = makeAdapter({ sessions: [{ id: "a" }, { id: "b" }] });
|
|
896
|
-
await expect(getBulletinSigner(adapter, "my-app.dot")).rejects.toBeInstanceOf(
|
|
897
|
-
AllowanceError
|
|
898
|
-
);
|
|
899
|
-
await expect(getBulletinSigner(adapter, "my-app.dot")).rejects.toMatchObject({
|
|
900
|
-
reason: "NoSession"
|
|
901
|
-
});
|
|
902
|
-
});
|
|
903
|
-
});
|
|
904
|
-
describe("getBulletinSigner \u2014 error unwrapping", () => {
|
|
905
|
-
test("rethrows the AllowanceError from the underlying service as a thrown exception", async () => {
|
|
906
|
-
const underlyingErr = new AllowanceError("Rejected", "user said no");
|
|
907
|
-
const adapter = makeAdapter({
|
|
908
|
-
sessions: [{ id: "s" }],
|
|
909
|
-
getBulletinSigner: vi.fn(() => err(underlyingErr))
|
|
910
|
-
});
|
|
911
|
-
await expect(getBulletinSigner(adapter, "p")).rejects.toBe(underlyingErr);
|
|
912
|
-
});
|
|
913
|
-
});
|
|
914
|
-
describe("getStatementStoreProver \u2014 same defaulting", () => {
|
|
915
|
-
test("uses the only paired session when sessionId is omitted", async () => {
|
|
916
|
-
const fakeProver = { _tag: "prover" };
|
|
917
|
-
const getStatementStoreProverFn = vi.fn(() => ok(fakeProver));
|
|
918
|
-
const adapter = makeAdapter({
|
|
919
|
-
sessions: [{ id: "sess-only" }],
|
|
920
|
-
getStatementStoreProver: getStatementStoreProverFn
|
|
921
|
-
});
|
|
922
|
-
const result = await getStatementStoreProver(adapter, "my-app.dot");
|
|
923
|
-
expect(result).toBe(fakeProver);
|
|
924
|
-
expect(getStatementStoreProverFn).toHaveBeenCalledWith("sess-only", "my-app.dot");
|
|
925
|
-
});
|
|
926
|
-
test("uses the explicit sessionId when supplied", async () => {
|
|
927
|
-
const fakeProver = { _tag: "prover" };
|
|
928
|
-
const getStatementStoreProverFn = vi.fn(() => ok(fakeProver));
|
|
929
|
-
const adapter = makeAdapter({
|
|
930
|
-
sessions: [{ id: "sess-a" }, { id: "sess-b" }],
|
|
931
|
-
getStatementStoreProver: getStatementStoreProverFn
|
|
932
|
-
});
|
|
933
|
-
await getStatementStoreProver(adapter, "my-app.dot", "sess-b");
|
|
934
|
-
expect(getStatementStoreProverFn).toHaveBeenCalledWith("sess-b", "my-app.dot");
|
|
935
|
-
});
|
|
936
|
-
test("throws AllowanceError('NoSession') when zero sessions and no id", async () => {
|
|
937
|
-
const adapter = makeAdapter({ sessions: [] });
|
|
938
|
-
await expect(getStatementStoreProver(adapter, "my-app.dot")).rejects.toBeInstanceOf(
|
|
939
|
-
AllowanceError
|
|
940
|
-
);
|
|
941
|
-
await expect(getStatementStoreProver(adapter, "my-app.dot")).rejects.toMatchObject({
|
|
942
|
-
reason: "NoSession"
|
|
943
|
-
});
|
|
944
|
-
});
|
|
945
|
-
test("throws AllowanceError('NoSession') when multiple sessions and no id", async () => {
|
|
946
|
-
const adapter = makeAdapter({ sessions: [{ id: "a" }, { id: "b" }] });
|
|
947
|
-
await expect(getStatementStoreProver(adapter, "my-app.dot")).rejects.toBeInstanceOf(
|
|
948
|
-
AllowanceError
|
|
949
|
-
);
|
|
950
|
-
await expect(getStatementStoreProver(adapter, "my-app.dot")).rejects.toMatchObject({
|
|
951
|
-
reason: "NoSession"
|
|
952
|
-
});
|
|
953
|
-
});
|
|
954
|
-
});
|
|
955
|
-
describe("getStatementStoreProver \u2014 error unwrapping", () => {
|
|
956
|
-
test("rethrows the AllowanceError from the underlying service as a thrown exception", async () => {
|
|
957
|
-
const underlyingErr = new AllowanceError("Rejected", "user said no");
|
|
958
|
-
const adapter = makeAdapter({
|
|
959
|
-
sessions: [{ id: "s" }],
|
|
960
|
-
getStatementStoreProver: vi.fn(() => err(underlyingErr))
|
|
961
|
-
});
|
|
962
|
-
await expect(getStatementStoreProver(adapter, "p")).rejects.toBe(underlyingErr);
|
|
963
|
-
});
|
|
964
|
-
});
|
|
965
|
-
describe("has*Allowance \u2014 sessionId defaulting + storageDir fallback", () => {
|
|
966
|
-
test("hasBulletinAllowance throws AllowanceError('NoSession') when zero sessions and no id", async () => {
|
|
967
|
-
const adapter = makeAdapter({ sessions: [] });
|
|
968
|
-
await expect(hasBulletinAllowance(adapter, "p")).rejects.toBeInstanceOf(AllowanceError);
|
|
969
|
-
await expect(hasBulletinAllowance(adapter, "p")).rejects.toMatchObject({
|
|
970
|
-
reason: "NoSession"
|
|
971
|
-
});
|
|
972
|
-
});
|
|
973
|
-
test("hasBulletinAllowance returns false (conservative) when storageDir is undefined", async () => {
|
|
974
|
-
const adapter = makeAdapter({ sessions: [{ id: "sess-only" }] });
|
|
975
|
-
expect(await hasBulletinAllowance(adapter, "p")).toBe(false);
|
|
976
|
-
});
|
|
977
|
-
test("hasStatementStoreAllowance throws AllowanceError('NoSession') when zero sessions and no id", async () => {
|
|
978
|
-
const adapter = makeAdapter({ sessions: [] });
|
|
979
|
-
await expect(hasStatementStoreAllowance(adapter, "p")).rejects.toBeInstanceOf(
|
|
980
|
-
AllowanceError
|
|
981
|
-
);
|
|
982
|
-
});
|
|
983
|
-
test("hasStatementStoreAllowance returns false (conservative) when storageDir is undefined", async () => {
|
|
984
|
-
const adapter = makeAdapter({ sessions: [{ id: "sess-only" }] });
|
|
985
|
-
expect(await hasStatementStoreAllowance(adapter, "p")).toBe(false);
|
|
986
|
-
});
|
|
987
|
-
});
|
|
988
|
-
}
|
|
989
|
-
var makeAdapter2;
|
|
990
|
-
|
|
991
|
-
// src/index.ts
|
|
992
|
-
import { AllowanceError as AllowanceError2 } from "@novasamatech/host-papp";
|
|
993
386
|
|
|
994
387
|
// src/sessions.ts
|
|
995
388
|
function waitForSessions(adapter, timeoutMs = 3e3) {
|
|
@@ -1011,108 +404,37 @@ function waitForSessions(adapter, timeoutMs = 3e3) {
|
|
|
1011
404
|
});
|
|
1012
405
|
});
|
|
1013
406
|
}
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
const
|
|
1039
|
-
|
|
1040
|
-
test("resolves with sessions on first non-empty emission", async () => {
|
|
1041
|
-
const { adapter, emit } = fakeAdapter();
|
|
1042
|
-
const promise = waitForSessions(adapter, 1e3);
|
|
1043
|
-
const session = { remoteAccount: { accountId: new Uint8Array(32) } };
|
|
1044
|
-
emit([session]);
|
|
1045
|
-
await expect(promise).resolves.toEqual([session]);
|
|
1046
|
-
});
|
|
1047
|
-
test("ignores empty emissions and waits for a non-empty one", async () => {
|
|
1048
|
-
const { adapter, emit } = fakeAdapter();
|
|
1049
|
-
const promise = waitForSessions(adapter, 1e3);
|
|
1050
|
-
emit([]);
|
|
1051
|
-
emit([]);
|
|
1052
|
-
const session = { remoteAccount: { accountId: new Uint8Array(32) } };
|
|
1053
|
-
emit([session]);
|
|
1054
|
-
await expect(promise).resolves.toEqual([session]);
|
|
1055
|
-
});
|
|
1056
|
-
test("resolves with [] after timeout when nothing is emitted", async () => {
|
|
1057
|
-
vi.useFakeTimers();
|
|
1058
|
-
try {
|
|
1059
|
-
const { adapter } = fakeAdapter();
|
|
1060
|
-
const promise = waitForSessions(adapter, 50);
|
|
1061
|
-
vi.advanceTimersByTime(50);
|
|
1062
|
-
await expect(promise).resolves.toEqual([]);
|
|
1063
|
-
} finally {
|
|
1064
|
-
vi.useRealTimers();
|
|
1065
|
-
}
|
|
1066
|
-
});
|
|
1067
|
-
test("unsubscribes after resolving", async () => {
|
|
1068
|
-
const { adapter, emit, subscriberCount } = fakeAdapter();
|
|
1069
|
-
const session = { remoteAccount: { accountId: new Uint8Array(32) } };
|
|
1070
|
-
const promise = waitForSessions(adapter, 1e3);
|
|
1071
|
-
emit([session]);
|
|
1072
|
-
await promise;
|
|
1073
|
-
await Promise.resolve();
|
|
1074
|
-
expect(subscriberCount()).toBe(0);
|
|
1075
|
-
});
|
|
1076
|
-
test("uses default 3000ms timeout when none is passed", async () => {
|
|
1077
|
-
vi.useFakeTimers();
|
|
1078
|
-
try {
|
|
1079
|
-
const { adapter } = fakeAdapter();
|
|
1080
|
-
const promise = waitForSessions(adapter);
|
|
1081
|
-
vi.advanceTimersByTime(2999);
|
|
1082
|
-
let settled = false;
|
|
1083
|
-
promise.then(() => {
|
|
1084
|
-
settled = true;
|
|
1085
|
-
});
|
|
1086
|
-
await Promise.resolve();
|
|
1087
|
-
expect(settled).toBe(false);
|
|
1088
|
-
vi.advanceTimersByTime(1);
|
|
1089
|
-
await expect(promise).resolves.toEqual([]);
|
|
1090
|
-
} finally {
|
|
1091
|
-
vi.useRealTimers();
|
|
1092
|
-
}
|
|
1093
|
-
});
|
|
1094
|
-
test("handles synchronous initial emission", async () => {
|
|
1095
|
-
const session = { remoteAccount: { accountId: new Uint8Array(32) } };
|
|
1096
|
-
const adapter = {
|
|
1097
|
-
sessions: {
|
|
1098
|
-
sessions: {
|
|
1099
|
-
subscribe(cb) {
|
|
1100
|
-
cb([session]);
|
|
1101
|
-
return () => {
|
|
1102
|
-
};
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1106
|
-
};
|
|
1107
|
-
await expect(waitForSessions(adapter, 1e3)).resolves.toEqual([session]);
|
|
1108
|
-
});
|
|
1109
|
-
});
|
|
407
|
+
var ROOT_ENTROPY_SOURCE_LEN = 32;
|
|
408
|
+
var MAX_KEY_LEN = 32;
|
|
409
|
+
var textEncoder = new TextEncoder();
|
|
410
|
+
var b2 = (message, key) => key ? blake2b(message, { dkLen: 32, key }) : blake2b(message, { dkLen: 32 });
|
|
411
|
+
function deriveEntropy(session, productId, key) {
|
|
412
|
+
const rootEntropySource = session.rootEntropySource;
|
|
413
|
+
if (!rootEntropySource) {
|
|
414
|
+
throw new Error(
|
|
415
|
+
'deriveEntropy: stored login session is missing rootEntropySource. Run "logout" and then "login" to pair again with an RFC-0007 host.'
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
if (rootEntropySource.length !== ROOT_ENTROPY_SOURCE_LEN) {
|
|
419
|
+
throw new Error(
|
|
420
|
+
`deriveEntropy: rootEntropySource must be ${ROOT_ENTROPY_SOURCE_LEN} bytes, got ${rootEntropySource.length}. The session is corrupt or the host emitted a non-conforming width; re-pairing will not help if the host is at fault.`
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
if (typeof productId !== "string" || productId.length === 0) {
|
|
424
|
+
throw new Error(
|
|
425
|
+
`deriveEntropy: productId must be a non-empty string, got ${typeof productId === "string" ? "an empty string" : String(productId)}`
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
if (key.length === 0 || key.length > MAX_KEY_LEN) {
|
|
429
|
+
throw new Error(`deriveEntropy: key must be 1..${MAX_KEY_LEN} bytes, got ${key.length}`);
|
|
430
|
+
}
|
|
431
|
+
const perProduct = b2(rootEntropySource, b2(textEncoder.encode(productId)));
|
|
432
|
+
return b2(perProduct, key);
|
|
1110
433
|
}
|
|
1111
|
-
var fakeAdapter2;
|
|
1112
434
|
|
|
1113
435
|
// src/qr-encode.ts
|
|
1114
436
|
async function renderQrCode(data, options) {
|
|
1115
|
-
const QRCode = await import(
|
|
437
|
+
const QRCode = await import('qrcode');
|
|
1116
438
|
const result = await QRCode.toString(data, {
|
|
1117
439
|
type: "utf8",
|
|
1118
440
|
errorCorrectionLevel: options?.errorCorrectionLevel ?? "M",
|
|
@@ -1120,43 +442,7 @@ async function renderQrCode(data, options) {
|
|
|
1120
442
|
});
|
|
1121
443
|
return result;
|
|
1122
444
|
}
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
const result = await renderQrCode("https://example.com");
|
|
1127
|
-
expect(result.length).toBeGreaterThan(0);
|
|
1128
|
-
expect(result).toContain("\n");
|
|
1129
|
-
});
|
|
1130
|
-
test("renderQrCode contains Unicode block characters", async () => {
|
|
1131
|
-
const result = await renderQrCode("test");
|
|
1132
|
-
expect(/[▀▄█]/.test(result)).toBe(true);
|
|
1133
|
-
});
|
|
1134
|
-
test("different inputs produce different QR codes", async () => {
|
|
1135
|
-
const a = await renderQrCode("hello");
|
|
1136
|
-
const b = await renderQrCode("world");
|
|
1137
|
-
expect(a).not.toBe(b);
|
|
1138
|
-
});
|
|
1139
|
-
}
|
|
1140
|
-
export {
|
|
1141
|
-
AllowanceError2 as AllowanceError,
|
|
1142
|
-
AllowanceExpiredError2 as AllowanceExpiredError,
|
|
1143
|
-
INCOMPLETE_SESSION_MESSAGE,
|
|
1144
|
-
SS_PASEO_STABLE_STAGE_ENDPOINTS,
|
|
1145
|
-
SS_STABLE_STAGE_ENDPOINTS,
|
|
1146
|
-
SignerError,
|
|
1147
|
-
createNodeStorageAdapter,
|
|
1148
|
-
createSessionSigner,
|
|
1149
|
-
createSessionSignerForAccount,
|
|
1150
|
-
createTerminalAdapter,
|
|
1151
|
-
deriveProductPublicKey,
|
|
1152
|
-
getBulletinSigner,
|
|
1153
|
-
getStatementStoreProver,
|
|
1154
|
-
hasBulletinAllowance,
|
|
1155
|
-
hasStatementStoreAllowance,
|
|
1156
|
-
renderQrCode,
|
|
1157
|
-
requestResourceAllocation,
|
|
1158
|
-
sendResourceAllocation,
|
|
1159
|
-
sessionRootPublicKey,
|
|
1160
|
-
waitForSessions
|
|
1161
|
-
};
|
|
445
|
+
|
|
446
|
+
export { INCOMPLETE_SESSION_MESSAGE, StatementStoreNetworks, createSessionSigner, createSessionSignerForAccount, createTerminalAdapter, deriveEntropy, deriveProductPublicKey, getBulletinSigner, getProductSubtreePublicKey, getStatementStoreProver, hasBulletinAllowance, hasStatementStoreAllowance, isBenignTeardownError, renderQrCode, sessionRootPublicKey, waitForSessions };
|
|
447
|
+
//# sourceMappingURL=index.js.map
|
|
1162
448
|
//# sourceMappingURL=index.js.map
|