Skip to content

Commit 8c97f2c

Browse files
authored
release: 0.7.8 (#164)
1 parent c1560bf commit 8c97f2c

68 files changed

Lines changed: 4352 additions & 22230 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,34 @@
1+
## 0.7.8 (2026-05-08)
2+
3+
### 🚀 Features
4+
5+
- **product-bulletin:** new `@novasamatech/product-bulletin` package — a Bulletin Chain client adapter for product apps.
6+
- **host-papp:** the paired Polkadot Mobile app now reports a dedicated identity account alongside the remote signing account, and `UserSession` exposes it as `identityAccountId`. The `useSessionIdentity` hook reads from this field, so on-chain identity (display name, avatar) resolves against the user's identity account rather than the per-product signing account.
7+
- **host-worker-sandbox:** `fetchResolver` now receives the in-VM `Request`'s `mode`, `credentials`, and `redirect`, so the host can apply CORS / auth / redirect-handling policy per request.
8+
9+
### ⚠️ Breaking Changes
10+
11+
- **host-papp:** the SSO handshake response payload now carries an `identityAccountId` field. Older paired Polkadot Mobile clients that don't send this field will fail to handshake — both ends must be on a compatible version.
12+
- **storage-adapter:** `createLocalStorageAdapter` now writes under a `polkadot_<prefix>_` key namespace instead of `PAPP_<prefix>_`. Data written by earlier versions will not be found after upgrade — hosts that need to preserve user state must migrate the old keys.
13+
14+
### 🩹 Fixes
15+
16+
- **host-worker-sandbox:** sandbox `console` output is sanitized before being forwarded to the host logger — control characters (including ANSI escapes) are stripped and each string argument is capped at 64 KiB, so sandbox code can no longer drive a terminal-aware logger or dump multi-megabyte strings into host logs.
17+
- **host-worker-sandbox:** `crypto.subtle` algorithm names are canonicalized at the bridge (e.g. `aes-gcm``AES-GCM`), so a resolver doing its own case-sensitive switch on `algorithm.name` can't be bypassed by case confusion; prototype-pollution keys (`__proto__`, `constructor`, `prototype`) are also stripped from values crossing the bridge in either direction.
18+
- **host-worker-sandbox:** `setTimeout` / `setInterval` delays are floored at 4 ms and `queueMicrotask` is capped at 1024 pending callbacks per sandbox, so sandbox code cannot flood the host event loop with zero-delay timers or microtask spam.
19+
- **host-worker-sandbox:** per-port `message` listeners are capped at 32 — sandbox code that registers fresh closures in a loop can no longer grow the host-side handle array unbounded and exhaust the QuickJS heap.
20+
- **host-worker-sandbox:** sandboxes whose wrappers are garbage-collected without an explicit `dispose()` now free the underlying WASM context via a `FinalizationRegistry`, so a leaked sandbox no longer pins its QuickJS runtime indefinitely.
21+
22+
### Chore
23+
24+
- Refined e2e and unit tests
25+
26+
### ❤️ Thank You
27+
28+
- Sergey Zhuravlev @johnthecat
29+
- Filippo
30+
- RafalMirowski1
31+
132
## 0.7.7 (2026-05-07)
233

334
### 🚀 Features

__tests__/hostApi/accounts.spec.ts

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -340,38 +340,6 @@ describe('Host API: Accounts', () => {
340340

341341
await expect(signer.signBytes(new Uint8Array([1, 2, 3]))).rejects.toEqual(error);
342342
});
343-
344-
it('should sign payload via handleSignPayload', async () => {
345-
const { container, accountsProvider } = setup();
346-
const signatureBytes = new Uint8Array(64).fill(0xcd);
347-
let handlerCalled = false;
348-
349-
container.handleSignPayload((_, { ok }) => {
350-
handlerCalled = true;
351-
return ok({ signature: toHex(signatureBytes), signedTransaction: undefined });
352-
});
353-
354-
const signer = accountsProvider.getProductAccountSigner(mockAccount);
355-
// Invoke via the PJS signPayload interface underlying the PolkadotSigner
356-
const pjsSignPayload = (signer as unknown as { _signPayload: (p: unknown) => Promise<unknown> })._signPayload;
357-
if (pjsSignPayload) {
358-
await pjsSignPayload.call(signer, {
359-
address: '0x00',
360-
genesisHash: '0x00',
361-
nonce: '0x00',
362-
method: '0x0002',
363-
blockHash: '0x00',
364-
blockNumber: '0x00',
365-
era: '0x00',
366-
version: 4,
367-
specVersion: '0x00',
368-
tip: '0x00',
369-
signedExtensions: [],
370-
transactionVersion: '0x00',
371-
});
372-
expect(handlerCalled).toBe(true);
373-
}
374-
});
375343
});
376344

377345
describe('getLegacyAccountSigner', () => {

__tests__/hostApi/chainInteraction.e2e.spec.ts

Lines changed: 85 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,34 @@
1-
/* eslint-disable @typescript-eslint/no-non-null-assertion,@typescript-eslint/no-explicit-any */
1+
/* eslint-disable @typescript-eslint/no-non-null-assertion */
22

33
import { createTransport } from '@novasamatech/host-api';
44
import { createContainer } from '@novasamatech/host-container';
55
import { WellKnownChain, createPapiProvider } from '@novasamatech/product-sdk';
66

7-
import type { JsonRpcMessage, JsonRpcProvider } from '@polkadot-api/json-rpc-provider';
7+
import type { JsonRpcMessage, JsonRpcProvider, JsonRpcRequest, JsonRpcResponse } from '@polkadot-api/json-rpc-provider';
8+
import { isRequest, isResponse } from '@polkadot-api/json-rpc-provider';
89
import { describe, expect, it } from 'vitest';
910

11+
// chainHead_v1_followEvent notification shape — params carries the subscription
12+
// id and a result envelope with an event-discriminated body.
13+
type FollowNotification = JsonRpcRequest<{
14+
subscription: string;
15+
result: { event: string; operationId?: string; [key: string]: unknown };
16+
}>;
17+
18+
const isFollowEventOf =
19+
(event: string, operationId?: string) =>
20+
(m: JsonRpcMessage): m is FollowNotification => {
21+
if (!isRequest(m) || m.method !== 'chainHead_v1_followEvent') return false;
22+
const params = m.params;
23+
if (!params || params.result?.event !== event) return false;
24+
return operationId === undefined || params.result?.operationId === operationId;
25+
};
26+
27+
const isResponseFor =
28+
(id: number) =>
29+
(m: JsonRpcMessage): m is JsonRpcResponse =>
30+
isResponse(m) && m.id === id && ('result' in m || 'error' in m);
31+
1032
import { delay } from './__mocks__/helpers.js';
1133
import { createHostApiProviders } from './__mocks__/hostApiProviders.js';
1234

@@ -45,9 +67,21 @@ function createWebSocketProvider(url: string): JsonRpcProvider {
4567
* Polls for a message matching `predicate` in `messages`.
4668
* Returns the matching message or undefined if not found within the timeout.
4769
*/
70+
async function pollForMessage<T extends JsonRpcMessage = JsonRpcMessage>(
71+
messages: JsonRpcMessage[],
72+
predicate: (parsed: JsonRpcMessage) => parsed is T,
73+
maxIterations?: number,
74+
interval?: number,
75+
): Promise<T | undefined>;
4876
async function pollForMessage(
4977
messages: JsonRpcMessage[],
50-
predicate: (parsed: Record<string, unknown>) => boolean,
78+
predicate: (parsed: JsonRpcMessage) => boolean,
79+
maxIterations?: number,
80+
interval?: number,
81+
): Promise<JsonRpcMessage | undefined>;
82+
async function pollForMessage(
83+
messages: JsonRpcMessage[],
84+
predicate: (parsed: JsonRpcMessage) => boolean,
5185
maxIterations = 150,
5286
interval = 200,
5387
): Promise<JsonRpcMessage | undefined> {
@@ -111,17 +145,14 @@ async function createChainHeadSetup(): Promise<ChainHeadSetup> {
111145

112146
setup.sdkConnection.send({ jsonrpc: '2.0', id: 1, method: 'chainHead_v1_follow', params: [false] });
113147

114-
const followResp = await pollForMessage(setup.receivedMessages, p => p.id === 1 && p.result !== undefined);
115-
if (!followResp) throw new Error('Failed to start follow subscription');
116-
const followSubId = 'result' in followResp ? (followResp.result as string) : '';
148+
const followResp = await pollForMessage(setup.receivedMessages, isResponseFor(1));
149+
if (!followResp || !('result' in followResp)) throw new Error('Failed to start follow subscription');
150+
const followSubId = followResp.result as string;
117151

118-
const initEvent = await pollForMessage(
119-
setup.receivedMessages,
120-
p => (p as any).method === 'chainHead_v1_followEvent' && (p as any).params?.result?.event === 'initialized',
121-
);
152+
const initEvent = await pollForMessage(setup.receivedMessages, isFollowEventOf('initialized'));
122153
if (!initEvent) throw new Error('Did not receive initialized event');
123154

124-
const initialBlockHash = (initEvent as any).params.result.finalizedBlockHashes[0] as string;
155+
const initialBlockHash = (initEvent.params!.result.finalizedBlockHashes as string[])[0]!;
125156

126157
return { ...setup, followSubId, initialBlockHash };
127158
}
@@ -136,10 +167,9 @@ describe('E2E: Chain Interaction against real Polkadot node', { retry: 2, timeou
136167
try {
137168
sdkConnection.send({ jsonrpc: '2.0', id: 1, method: 'chainSpec_v1_genesisHash', params: [] });
138169

139-
const response = await pollForMessage(receivedMessages, p => p.id === 1 && p.result !== undefined);
170+
const response = await pollForMessage(receivedMessages, isResponseFor(1));
140171
expect(response).toBeDefined();
141-
142-
expect((response! as any).result).toBe(POLKADOT_GENESIS_HASH);
172+
expect(response).toHaveProperty('result', POLKADOT_GENESIS_HASH);
143173
} finally {
144174
await cleanup();
145175
}
@@ -150,12 +180,12 @@ describe('E2E: Chain Interaction against real Polkadot node', { retry: 2, timeou
150180
try {
151181
sdkConnection.send({ jsonrpc: '2.0', id: 1, method: 'chainSpec_v1_chainName', params: [] });
152182

153-
const response = await pollForMessage(receivedMessages, p => p.id === 1 && p.result !== undefined);
183+
const response = await pollForMessage(receivedMessages, isResponseFor(1));
154184
expect(response).toBeDefined();
155-
156-
const parsed = response as any;
157-
expect(typeof parsed.result).toBe('string');
158-
expect(parsed.result.length).toBeGreaterThan(0);
185+
expect(response).toHaveProperty('result');
186+
const result = (response as JsonRpcResponse & { result: string }).result;
187+
expect(typeof result).toBe('string');
188+
expect(result.length).toBeGreaterThan(0);
159189
} finally {
160190
await cleanup();
161191
}
@@ -166,11 +196,10 @@ describe('E2E: Chain Interaction against real Polkadot node', { retry: 2, timeou
166196
try {
167197
sdkConnection.send({ jsonrpc: '2.0', id: 1, method: 'chainSpec_v1_properties', params: [] });
168198

169-
const response = await pollForMessage(receivedMessages, p => p.id === 1 && p.result !== undefined);
199+
const response = await pollForMessage(receivedMessages, isResponseFor(1));
170200
expect(response).toBeDefined();
171-
172-
const parsed = response as any;
173-
const props = parsed.result;
201+
expect(response).toHaveProperty('result');
202+
const props = (response as JsonRpcResponse & { result: { tokenSymbol: string; tokenDecimals: number } }).result;
174203
expect(props).toBeDefined();
175204
expect(props.tokenSymbol).toBe('DOT');
176205
expect(props.tokenDecimals).toBe(10);
@@ -186,24 +215,22 @@ describe('E2E: Chain Interaction against real Polkadot node', { retry: 2, timeou
186215
try {
187216
sdkConnection.send({ jsonrpc: '2.0', id: 1, method: 'chainHead_v1_follow', params: [false] });
188217

189-
const followResp = await pollForMessage(receivedMessages, p => p.id === 1 && p.result !== undefined);
218+
const followResp = await pollForMessage(receivedMessages, isResponseFor(1));
190219
expect(followResp).toBeDefined();
220+
expect(followResp).toHaveProperty('result');
191221

192-
const followSubId = (followResp as any).result;
222+
const followSubId = (followResp as JsonRpcResponse & { result: string }).result;
193223
expect(typeof followSubId).toBe('string');
194224

195-
const initEvent = await pollForMessage(
196-
receivedMessages,
197-
p => (p as any).method === 'chainHead_v1_followEvent' && (p as any).params?.result?.event === 'initialized',
198-
);
199-
225+
const initEvent = await pollForMessage(receivedMessages, isFollowEventOf('initialized'));
200226
expect(initEvent).toBeDefined();
201-
const parsedInit = initEvent as any;
202-
expect(parsedInit.params.result.event).toBe('initialized');
203-
expect(Array.isArray(parsedInit.params.result.finalizedBlockHashes)).toBe(true);
204-
expect(parsedInit.params.result.finalizedBlockHashes.length).toBeGreaterThan(0);
205227

206-
for (const hash of parsedInit.params.result.finalizedBlockHashes) {
228+
const initResult = initEvent!.params!.result as { event: string; finalizedBlockHashes: string[] };
229+
expect(initResult.event).toBe('initialized');
230+
expect(Array.isArray(initResult.finalizedBlockHashes)).toBe(true);
231+
expect(initResult.finalizedBlockHashes.length).toBeGreaterThan(0);
232+
233+
for (const hash of initResult.finalizedBlockHashes) {
207234
expect(hash).toMatch(/^0x[0-9a-fA-F]+$/);
208235
}
209236
} finally {
@@ -221,16 +248,13 @@ describe('E2E: Chain Interaction against real Polkadot node', { retry: 2, timeou
221248
params: [followSubId, initialBlockHash],
222249
});
223250

224-
const response = await pollForMessage(
225-
receivedMessages,
226-
p => p.id === 2 && (p.result !== undefined || p.error !== undefined),
227-
);
228-
251+
const response = await pollForMessage(receivedMessages, isResponseFor(2));
229252
expect(response).toBeDefined();
230-
const parsed = response as any;
231-
expect(parsed.result).toBeDefined();
232-
expect(typeof parsed.result).toBe('string');
233-
expect(parsed.result).toMatch(/^0x/);
253+
expect(response).toHaveProperty('result');
254+
255+
const result = (response as JsonRpcResponse & { result: string }).result;
256+
expect(typeof result).toBe('string');
257+
expect(result).toMatch(/^0x/);
234258
} finally {
235259
await cleanup();
236260
}
@@ -246,41 +270,31 @@ describe('E2E: Chain Interaction against real Polkadot node', { retry: 2, timeou
246270
params: [followSubId, initialBlockHash, [{ key: SYSTEM_NUMBER_KEY, type: 'value' }], null],
247271
});
248272

249-
const storageResp = await pollForMessage(
250-
receivedMessages,
251-
p => p.id === 2 && (p.result !== undefined || p.error !== undefined),
252-
);
253-
273+
const storageResp = await pollForMessage(receivedMessages, isResponseFor(2));
254274
expect(storageResp).toBeDefined();
255-
const parsedResp = storageResp as any;
256-
expect(parsedResp.result).toBeDefined();
257-
expect(parsedResp.result.result).toBe('started');
258-
expect(parsedResp.result.operationId).toBeDefined();
275+
expect(storageResp).toHaveProperty('result');
259276

260-
const operationId = parsedResp.result.operationId;
277+
const result = (storageResp as JsonRpcResponse & { result: { result: string; operationId: string } }).result;
278+
expect(result.result).toBe('started');
279+
expect(result.operationId).toBeDefined();
280+
281+
const operationId = result.operationId;
261282

262283
const storageItemsEvent = await pollForMessage(
263284
receivedMessages,
264-
p =>
265-
(p as any).method === 'chainHead_v1_followEvent' &&
266-
(p as any).params?.result?.event === 'operationStorageItems' &&
267-
(p as any).params?.result?.operationId === operationId,
285+
isFollowEventOf('operationStorageItems', operationId),
268286
);
269-
270287
expect(storageItemsEvent).toBeDefined();
271-
const parsedItems = storageItemsEvent as any;
272-
expect(parsedItems.params.result.items.length).toBeGreaterThan(0);
273-
expect(parsedItems.params.result.items[0].key).toBe(SYSTEM_NUMBER_KEY);
274-
expect(parsedItems.params.result.items[0].value).toMatch(/^0x/);
288+
289+
const items = storageItemsEvent!.params!.result.items as Array<{ key: string; value: string }>;
290+
expect(items.length).toBeGreaterThan(0);
291+
expect(items[0]!.key).toBe(SYSTEM_NUMBER_KEY);
292+
expect(items[0]!.value).toMatch(/^0x/);
275293

276294
const storageDoneEvent = await pollForMessage(
277295
receivedMessages,
278-
p =>
279-
(p as any).method === 'chainHead_v1_followEvent' &&
280-
(p as any).params?.result?.event === 'operationStorageDone' &&
281-
(p as any).params?.result?.operationId === operationId,
296+
isFollowEventOf('operationStorageDone', operationId),
282297
);
283-
284298
expect(storageDoneEvent).toBeDefined();
285299
} finally {
286300
await cleanup();
@@ -297,15 +311,10 @@ describe('E2E: Chain Interaction against real Polkadot node', { retry: 2, timeou
297311
params: [followSubId, [initialBlockHash]],
298312
});
299313

300-
const response = await pollForMessage(
301-
receivedMessages,
302-
p => p.id === 2 && (p.result !== undefined || p.error !== undefined),
303-
);
304-
314+
const response = await pollForMessage(receivedMessages, isResponseFor(2));
305315
expect(response).toBeDefined();
306-
const parsed = response as any;
307-
expect(parsed.error).toBeUndefined();
308-
expect(parsed.result).toBe(null);
316+
expect(response).toHaveProperty('result', null);
317+
expect(response).not.toHaveProperty('error');
309318
} finally {
310319
await cleanup();
311320
}

0 commit comments

Comments
 (0)