Skip to content

Commit 29bb2f2

Browse files
authored
chore(deps): bump @novasamatech catalog deps to ^0.8.6 (#181)
* chore(deps): bump @novasamatech catalog deps to ^0.8.6 * lint
1 parent d35227d commit 29bb2f2

7 files changed

Lines changed: 150 additions & 216 deletions

File tree

product-sdk/packages/terminal/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@
3535
"@noble/ciphers": "^2.1.0",
3636
"@noble/curves": "^2.0.1",
3737
"@noble/hashes": "^2.2.0",
38-
"@novasamatech/host-papp": "^0.8.5",
39-
"@novasamatech/statement-store": "^0.8.5",
40-
"@novasamatech/storage-adapter": "^0.8.5",
38+
"@novasamatech/host-papp": "^0.8.6",
39+
"@novasamatech/statement-store": "^0.8.6",
40+
"@novasamatech/storage-adapter": "^0.8.6",
4141
"@parity/product-sdk-logger": "workspace:*",
4242
"@polkadot-api/substrate-bindings": "^0.20.2",
4343
"@polkadot-api/utils": "^0.4.0",

product-sdk/packages/terminal/src/testing.interop.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ describe("createTestSession interop with host-papp", () => {
115115
expect(result.isOk()).toBe(true);
116116
const secrets = result._unsafeUnwrap();
117117
expect(secrets).not.toBeNull();
118-
expect(secrets!.entropy).toBeInstanceOf(Uint8Array);
118+
expect(secrets!.identityChatPrivateKey).toBeInstanceOf(Uint8Array);
119119
expect(secrets!.ssSecret).toBeInstanceOf(Uint8Array);
120120
expect(secrets!.encrSecret).toBeInstanceOf(Uint8Array);
121121

product-sdk/packages/terminal/src/testing.ts

Lines changed: 63 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -38,34 +38,40 @@ import {
3838
import { mkdir, writeFile } from "node:fs/promises";
3939
import { join } from "node:path";
4040
import { nanoid } from "nanoid";
41-
import { Bytes, type Codec, Option, Struct, Vector, str } from "scale-ts";
41+
import { Bytes, type Codec, Struct, Vector, str } from "scale-ts";
4242
import type { StoredUserSession } from "@novasamatech/host-papp";
4343

4444
import { sanitizeKey } from "./node-storage.js";
4545

4646
// Mirror of host-papp's internal stored-session codec (not exported — only the
4747
// `StoredUserSession` type is). `satisfies` guards drift at build time, the
48-
// interop test at runtime; field order and the `Bytes(65)` chat key must match.
48+
// interop test at runtime; field order and the fixed-width fields must match.
4949
//
50-
// host-papp 0.8.5 appended `ssoEncPubKey` (Mobile SSO spec v0.2.2):
51-
// the peer's 65-byte uncompressed P-256 encryption pubkey. `None` for
52-
// pre-v0.2.2 peers; we synthesize as `undefined` below.
50+
// host-papp 0.8.6 (RFC-0007, PR #205) dropped the `Option` wrapper on
51+
// `identityAccountId`, `identityChatPublicKey`, and `ssoEncPubKey` — all
52+
// three are now required — and appended `rootEntropySource: Bytes(32)`,
53+
// the layer-1 entropy the host consumes via `deriveProductEntropyFromSource`.
5354
const storedUserSessionCodec = Struct({
5455
id: str,
5556
localAccount: LocalSessionAccountCodec,
5657
remoteAccount: RemoteSessionAccountCodec,
5758
rootAccountId: AccountIdCodec,
58-
identityAccountId: Option(AccountIdCodec),
59-
identityChatPublicKey: Option(Bytes(65)),
60-
ssoEncPubKey: Option(Bytes(65)),
59+
identityAccountId: AccountIdCodec,
60+
identityChatPublicKey: Bytes(65),
61+
ssoEncPubKey: Bytes(65),
62+
rootEntropySource: Bytes(32),
6163
}) satisfies Codec<StoredUserSession>;
6264
const sessionsCodec = Vector(storedUserSessionCodec);
6365

6466
// Mirrors the internal StoredUserSecretsCodec in host-papp's userSecretRepository.
67+
//
68+
// host-papp 0.8.6 dropped `entropy` (root entropy moved onto the session as
69+
// `rootEntropySource`) and added the V2 `identityChatPrivateKey` field.
70+
// The storage key was also renamed `UserSecrets` → `UserSecretsV2`.
6571
const storedUserSecretsCodec = Struct({
6672
ssSecret: Bytes(),
6773
encrSecret: Bytes(),
68-
entropy: Bytes(),
74+
identityChatPrivateKey: Bytes(32),
6975
});
7076

7177
// Mirrors host-papp's userSecretRepository AES-GCM wrapper: the encryption
@@ -105,7 +111,7 @@ export interface CreateTestSessionOptions {
105111
/** Stable session id. Default: random nanoid(12). */
106112
sessionId?: string;
107113
/**
108-
* Whether to write the encrypted `UserSecrets_<id>` file. Default: `true`.
114+
* Whether to write the encrypted `UserSecretsV2_<id>` file. Default: `true`.
109115
* Set to `false` to exercise recovery paths where a session exists on disk
110116
* but its secrets are missing.
111117
*/
@@ -154,9 +160,9 @@ export interface TestSession {
154160
* tracked via on-chain attestation state. See above for how expiry-path
155161
* tests still work in practice.
156162
* - **Corrupted-session cases** don't need a helper — write garbage to
157-
* `<storageDir>/<appId>_SsoSessions.json` with `fs.writeFile` directly.
163+
* `<storageDir>/<appId>_SsoSessionsV2.json` with `fs.writeFile` directly.
158164
* - **Repeated calls replace the session list.** Each call writes a fresh
159-
* single-entry `SsoSessions` file, so calling twice on the same
165+
* single-entry `SsoSessionsV2` file, so calling twice on the same
160166
* `storageDir`+`appId` leaves only the second session on disk. Use a
161167
* fresh `mkdtempSync` per test to keep cases isolated.
162168
*
@@ -201,6 +207,13 @@ export async function createTestSession(options: CreateTestSessionOptions): Prom
201207

202208
const sessionId = options.sessionId ?? nanoid(12);
203209

210+
// host-papp 0.8.6 (RFC-0007) requires all of identityAccountId,
211+
// identityChatPublicKey, ssoEncPubKey, and rootEntropySource on the
212+
// persisted session. For a synthesized test session we collapse the
213+
// identity-side keys onto the remote account (the wallet doubles as
214+
// identity), reuse the peer's P-256 encryption pubkey for both chat
215+
// and SSO transports, and use the remote entropy as the RFC-0007
216+
// root entropy source so the value is reproducible.
204217
const session = {
205218
id: sessionId,
206219
localAccount: createLocalSessionAccount(createAccountId(localPublicKey), undefined),
@@ -209,31 +222,34 @@ export async function createTestSession(options: CreateTestSessionOptions): Prom
209222
sharedSecret,
210223
undefined,
211224
),
212-
// synthesized session: the remote (wallet) account doubles as root.
213225
rootAccountId: createAccountId(remotePublicKey),
214-
identityAccountId: undefined,
215-
identityChatPublicKey: undefined,
216-
// pre-v0.2.2 peer: no SSO encryption pubkey on the wire.
217-
ssoEncPubKey: undefined,
226+
identityAccountId: createAccountId(remotePublicKey),
227+
identityChatPublicKey: remoteEncrPublicKey,
228+
ssoEncPubKey: remoteEncrPublicKey,
229+
rootEntropySource: remoteEntropy,
218230
};
219231

220232
await writeFile(
221-
join(options.storageDir, `${sanitizeKey(options.appId, "SsoSessions")}.json`),
233+
join(options.storageDir, `${sanitizeKey(options.appId, "SsoSessionsV2")}.json`),
222234
toHex(sessionsCodec.enc([session])),
223235
"utf-8",
224236
);
225237

226238
const includeSecrets = options.includeSecrets ?? true;
227239
if (includeSecrets) {
240+
// host-papp 0.8.6 dropped `entropy` (now lives on the session as
241+
// `rootEntropySource`) and requires a 32-byte `identityChatPrivateKey`
242+
// (V2 — P-256 raw scalar). Reuse the local P-256 encryption secret
243+
// for both — same domain (P-256, 32-byte scalar).
228244
const encoded = storedUserSecretsCodec.enc({
229245
ssSecret: localSecret,
230246
encrSecret: localEncrSecret,
231-
entropy: localEntropy,
247+
identityChatPrivateKey: localEncrSecret,
232248
});
233249
await writeFile(
234250
join(
235251
options.storageDir,
236-
`${sanitizeKey(options.appId, `UserSecrets_${sessionId}`)}.json`,
252+
`${sanitizeKey(options.appId, `UserSecretsV2_${sessionId}`)}.json`,
237253
),
238254
encryptSecrets(options.appId, encoded),
239255
"utf-8",
@@ -279,25 +295,25 @@ if (import.meta.vitest) {
279295
});
280296

281297
describe("createTestSession", () => {
282-
test("writes both SsoSessions and UserSecrets files by default", async () => {
298+
test("writes both SsoSessionsV2 and UserSecretsV2 files by default", async () => {
283299
const result = await createTestSession({
284300
appId: "my-app",
285301
storageDir,
286302
localMnemonic: LOCAL_MNEMONIC,
287303
remoteMnemonic: REMOTE_MNEMONIC,
288304
});
289305

290-
const sessions = await readFile(join(storageDir, "my-app_SsoSessions.json"), "utf-8");
306+
const sessions = await readFile(join(storageDir, "my-app_SsoSessionsV2.json"), "utf-8");
291307
expect(sessions).toMatch(/^0x[0-9a-f]+$/);
292308

293309
const secrets = await readFile(
294-
join(storageDir, `my-app_UserSecrets_${result.sessionId}.json`),
310+
join(storageDir, `my-app_UserSecretsV2_${result.sessionId}.json`),
295311
"utf-8",
296312
);
297313
expect(secrets).toMatch(/^0x[0-9a-f]+$/);
298314
});
299315

300-
test("omits UserSecrets file when includeSecrets is false", async () => {
316+
test("omits UserSecretsV2 file when includeSecrets is false", async () => {
301317
const result = await createTestSession({
302318
appId: "my-app",
303319
storageDir,
@@ -307,15 +323,18 @@ if (import.meta.vitest) {
307323
});
308324

309325
await expect(
310-
readFile(join(storageDir, "my-app_SsoSessions.json"), "utf-8"),
326+
readFile(join(storageDir, "my-app_SsoSessionsV2.json"), "utf-8"),
311327
).resolves.toMatch(/^0x/);
312328

313329
await expect(
314-
readFile(join(storageDir, `my-app_UserSecrets_${result.sessionId}.json`), "utf-8"),
330+
readFile(
331+
join(storageDir, `my-app_UserSecretsV2_${result.sessionId}.json`),
332+
"utf-8",
333+
),
315334
).rejects.toThrow(/ENOENT/);
316335
});
317336

318-
test("SsoSessions file decodes with the host-papp session codec shape", async () => {
337+
test("SsoSessionsV2 file decodes with the host-papp session codec shape", async () => {
319338
const result = await createTestSession({
320339
appId: "my-app",
321340
storageDir,
@@ -324,7 +343,7 @@ if (import.meta.vitest) {
324343
sessionId: "stable-test-id",
325344
});
326345

327-
const hex = await readFile(join(storageDir, "my-app_SsoSessions.json"), "utf-8");
346+
const hex = await readFile(join(storageDir, "my-app_SsoSessionsV2.json"), "utf-8");
328347
const decoded = sessionsCodec.dec(fromHex(hex));
329348

330349
expect(decoded).toHaveLength(1);
@@ -351,7 +370,7 @@ if (import.meta.vitest) {
351370
expect(result.localAccountId).toHaveLength(32);
352371
});
353372

354-
test("UserSecrets file decrypts and decodes with the host-papp secret codec shape", async () => {
373+
test("UserSecretsV2 file decrypts and decodes with the host-papp secret codec shape", async () => {
355374
const appId = "my-app";
356375
const result = await createTestSession({
357376
appId,
@@ -361,30 +380,31 @@ if (import.meta.vitest) {
361380
});
362381

363382
const hex = await readFile(
364-
join(storageDir, `${appId}_UserSecrets_${result.sessionId}.json`),
383+
join(storageDir, `${appId}_UserSecretsV2_${result.sessionId}.json`),
365384
"utf-8",
366385
);
367386
const key = blake2b(new TextEncoder().encode(appId), { dkLen: 16 });
368387
const nonce = blake2b(new TextEncoder().encode("nonce"), { dkLen: 32 });
369388
const decrypted = gcm(key, nonce).decrypt(fromHex(hex));
370389
const decoded = storedUserSecretsCodec.dec(decrypted);
371390

372-
expect(decoded.entropy).toEqual(mnemonicToEntropy(LOCAL_MNEMONIC));
373391
// Sr25519 secret is 64 bytes (32-byte secret + 32-byte nonce).
374392
expect(decoded.ssSecret).toHaveLength(64);
375393
// P256 secret is 32 bytes.
376394
expect(decoded.encrSecret).toHaveLength(32);
395+
// V2 chat private key (P-256 raw scalar) is 32 bytes.
396+
expect(decoded.identityChatPrivateKey).toHaveLength(32);
377397
});
378398

379399
test("different appIds produce files under different prefixes", async () => {
380400
await createTestSession({ appId: "app-a", storageDir, sessionId: "id" });
381401
await createTestSession({ appId: "app-b", storageDir, sessionId: "id" });
382402

383403
await expect(
384-
readFile(join(storageDir, "app-a_SsoSessions.json"), "utf-8"),
404+
readFile(join(storageDir, "app-a_SsoSessionsV2.json"), "utf-8"),
385405
).resolves.toMatch(/^0x/);
386406
await expect(
387-
readFile(join(storageDir, "app-b_SsoSessions.json"), "utf-8"),
407+
readFile(join(storageDir, "app-b_SsoSessionsV2.json"), "utf-8"),
388408
).resolves.toMatch(/^0x/);
389409
});
390410

@@ -395,15 +415,15 @@ if (import.meta.vitest) {
395415
sessionId: "id",
396416
});
397417
await expect(
398-
readFile(join(storageDir, "app_with_spaces_SsoSessions.json"), "utf-8"),
418+
readFile(join(storageDir, "app_with_spaces_SsoSessionsV2.json"), "utf-8"),
399419
).resolves.toMatch(/^0x/);
400420
});
401421

402422
test("creates storageDir when it does not yet exist", async () => {
403423
const nested = join(storageDir, "does", "not", "exist");
404424
await createTestSession({ appId: "my-app", storageDir: nested });
405425
await expect(
406-
readFile(join(nested, "my-app_SsoSessions.json"), "utf-8"),
426+
readFile(join(nested, "my-app_SsoSessionsV2.json"), "utf-8"),
407427
).resolves.toMatch(/^0x/);
408428
});
409429

@@ -423,7 +443,7 @@ if (import.meta.vitest) {
423443
});
424444
expect(result.sessionId).toBe("pinned-id");
425445
await expect(
426-
readFile(join(storageDir, "my-app_UserSecrets_pinned-id.json"), "utf-8"),
446+
readFile(join(storageDir, "my-app_UserSecretsV2_pinned-id.json"), "utf-8"),
427447
).resolves.toMatch(/^0x/);
428448
});
429449

@@ -457,17 +477,20 @@ if (import.meta.vitest) {
457477
sessionId: "second",
458478
});
459479

460-
const hex = await readFile(join(storageDir, "my-app_SsoSessions.json"), "utf-8");
480+
const hex = await readFile(join(storageDir, "my-app_SsoSessionsV2.json"), "utf-8");
461481
const decoded = sessionsCodec.dec(fromHex(hex));
462482
expect(decoded).toHaveLength(1);
463483
expect(decoded[0].id).toBe("second");
464-
// The first session's UserSecrets file is left behind (not cleaned).
484+
// The first session's UserSecretsV2 file is left behind (not cleaned).
465485
// This matches a real logout flow as well, so we don't try to hide it.
466486
await expect(
467-
readFile(join(storageDir, `my-app_UserSecrets_${first.sessionId}.json`), "utf-8"),
487+
readFile(join(storageDir, `my-app_UserSecretsV2_${first.sessionId}.json`), "utf-8"),
468488
).resolves.toMatch(/^0x/);
469489
await expect(
470-
readFile(join(storageDir, `my-app_UserSecrets_${second.sessionId}.json`), "utf-8"),
490+
readFile(
491+
join(storageDir, `my-app_UserSecretsV2_${second.sessionId}.json`),
492+
"utf-8",
493+
),
471494
).resolves.toMatch(/^0x/);
472495
});
473496

product-sdk/pending-changesets/bump-novasama-host-api-0.8.5.md

Lines changed: 0 additions & 16 deletions
This file was deleted.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
"@parity/product-sdk-host": patch
3+
"@parity/product-sdk-signer": patch
4+
"@parity/product-sdk-statement-store": patch
5+
"@parity/product-sdk-terminal": patch
6+
---
7+
8+
Bump `@novasamatech/host-api`, `@novasamatech/host-api-wrapper`, `@novasamatech/host-papp`, `@novasamatech/statement-store`, and `@novasamatech/storage-adapter` from `^0.8.5` to `^0.8.6`.
9+
10+
0.8.6 lands RFC-0007 (PR #205 upstream — derive product entropy from `rootEntropySource`) and a `polkadot-api` bump to `2.1.6` (double-notification fix). The RFC-0007 work changes the on-disk session and secrets schemas:
11+
12+
- **Session** (`SsoSessions``SsoSessionsV2`): dropped the `Option` wrapper on `identityAccountId`, `identityChatPublicKey`, and `ssoEncPubKey` (all now required); appended `rootEntropySource: Bytes(32)` for the host's `host_derive_entropy` handler.
13+
- **Secrets** (`UserSecrets``UserSecretsV2`): dropped `entropy` (now lives on the session as `rootEntropySource`); added the V2 `identityChatPrivateKey: Bytes(32)`.
14+
- **Graceful-degrade removed.** Old-shape blobs no longer fall back to empty — they now throw at decode. A CLI on 0.8.5 disk state will need to re-pair after the consumer upgrades.
15+
16+
`host-api` and `host-api-wrapper` had no source changes in 0.8.6 (lockstep version tag only) — `host`, `signer`, and `statement-store` are patch-bumped to signal "tested against 0.8.6" via published peer-dep / catalog resolution; their runtime behavior is unchanged.
17+
18+
In `@parity/product-sdk-terminal`, the internal codec mirror for `createTestSession` was updated to match the 0.8.6 session and secrets shapes — including the storage-key rename to `*V2` — so synthesized test sessions round-trip cleanly through the real 0.8.6 `SsoSessionManager` / `UserSecretRepository`. No public-API change in any of the four packages.

0 commit comments

Comments
 (0)