Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions product-sdk/packages/terminal/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@
"@noble/ciphers": "^2.1.0",
"@noble/curves": "^2.0.1",
"@noble/hashes": "^2.2.0",
"@novasamatech/host-papp": "^0.8.6",
"@novasamatech/statement-store": "^0.8.6",
"@novasamatech/storage-adapter": "^0.8.6",
"@novasamatech/host-papp": "^0.8.7-2",
"@novasamatech/statement-store": "^0.8.7-2",
"@novasamatech/storage-adapter": "^0.8.7-2",
"@parity/product-sdk-logger": "workspace:*",
"@polkadot-api/substrate-bindings": "^0.20.2",
"@polkadot-api/utils": "^0.4.0",
Expand All @@ -50,7 +50,7 @@
"scale-ts": "^1.6.1"
},
"devDependencies": {
"@novasamatech/substrate-slot-sr25519-wasm": "^0.8.6",
"@novasamatech/substrate-slot-sr25519-wasm": "^0.8.7-2",
"@types/qrcode": "^1.5.5",
"tsup": "catalog:",
"typescript": "catalog:",
Expand Down
46 changes: 28 additions & 18 deletions product-sdk/packages/terminal/src/testing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ import { sanitizeKey } from "./node-storage.js";
// `identityAccountId`, `identityChatPublicKey`, and `ssoEncPubKey` — all
// three are now required — and appended `rootEntropySource: Bytes(32)`,
// the layer-1 entropy the host consumes via `deriveProductEntropyFromSource`.
//
// host-papp 0.8.7-1 (PR #212) appended `deviceEncPubKey: Bytes(65)` — the
// paired phone's long-lived ECDH key, lifted from `HandshakeResponseV2.
// deviceEncPubKey`, used by the host's device-sync channel to address
// the paired device. The storage key was renamed `SsoSessionsV2 → SsoSessionsV3`
// in the same release; the old graceful-degrade for V2 blobs is gone.
const storedUserSessionCodec = Struct({
id: str,
localAccount: LocalSessionAccountCodec,
Expand All @@ -60,6 +66,7 @@ const storedUserSessionCodec = Struct({
identityChatPublicKey: Bytes(65),
ssoEncPubKey: Bytes(65),
rootEntropySource: Bytes(32),
deviceEncPubKey: Bytes(65),
}) satisfies Codec<StoredUserSession>;
const sessionsCodec = Vector(storedUserSessionCodec);

Expand Down Expand Up @@ -160,9 +167,9 @@ export interface TestSession {
* tracked via on-chain attestation state. See above for how expiry-path
* tests still work in practice.
* - **Corrupted-session cases** don't need a helper — write garbage to
* `<storageDir>/<appId>_SsoSessionsV2.json` with `fs.writeFile` directly.
* `<storageDir>/<appId>_SsoSessionsV3.json` with `fs.writeFile` directly.
* - **Repeated calls replace the session list.** Each call writes a fresh
* single-entry `SsoSessionsV2` file, so calling twice on the same
* single-entry `SsoSessionsV3` file, so calling twice on the same
* `storageDir`+`appId` leaves only the second session on disk. Use a
* fresh `mkdtempSync` per test to keep cases isolated.
*
Expand Down Expand Up @@ -209,11 +216,13 @@ export async function createTestSession(options: CreateTestSessionOptions): Prom

// host-papp 0.8.6 (RFC-0007) requires all of identityAccountId,
// identityChatPublicKey, ssoEncPubKey, and rootEntropySource on the
// persisted session. For a synthesized test session we collapse the
// identity-side keys onto the remote account (the wallet doubles as
// identity), reuse the peer's P-256 encryption pubkey for both chat
// and SSO transports, and use the remote entropy as the RFC-0007
// root entropy source so the value is reproducible.
// persisted session. host-papp 0.8.7-1 added `deviceEncPubKey` (the
// peer device's long-lived ECDH key). For a synthesized test session
// we collapse the identity-side keys onto the remote account (the
// wallet doubles as identity), reuse the peer's P-256 encryption
// pubkey for chat, SSO, and the new device-sync transport, and use
// the remote entropy as the RFC-0007 root entropy source so the value
// is reproducible.
const session = {
id: sessionId,
localAccount: createLocalSessionAccount(createAccountId(localPublicKey), undefined),
Expand All @@ -227,10 +236,11 @@ export async function createTestSession(options: CreateTestSessionOptions): Prom
identityChatPublicKey: remoteEncrPublicKey,
ssoEncPubKey: remoteEncrPublicKey,
rootEntropySource: remoteEntropy,
deviceEncPubKey: remoteEncrPublicKey,
};

await writeFile(
join(options.storageDir, `${sanitizeKey(options.appId, "SsoSessionsV2")}.json`),
join(options.storageDir, `${sanitizeKey(options.appId, "SsoSessionsV3")}.json`),
toHex(sessionsCodec.enc([session])),
"utf-8",
);
Expand Down Expand Up @@ -295,15 +305,15 @@ if (import.meta.vitest) {
});

describe("createTestSession", () => {
test("writes both SsoSessionsV2 and UserSecretsV2 files by default", async () => {
test("writes both SsoSessionsV3 and UserSecretsV2 files by default", async () => {
const result = await createTestSession({
appId: "my-app",
storageDir,
localMnemonic: LOCAL_MNEMONIC,
remoteMnemonic: REMOTE_MNEMONIC,
});

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

const secrets = await readFile(
Expand All @@ -323,7 +333,7 @@ if (import.meta.vitest) {
});

await expect(
readFile(join(storageDir, "my-app_SsoSessionsV2.json"), "utf-8"),
readFile(join(storageDir, "my-app_SsoSessionsV3.json"), "utf-8"),
).resolves.toMatch(/^0x/);

await expect(
Expand All @@ -334,7 +344,7 @@ if (import.meta.vitest) {
).rejects.toThrow(/ENOENT/);
});

test("SsoSessionsV2 file decodes with the host-papp session codec shape", async () => {
test("SsoSessionsV3 file decodes with the host-papp session codec shape", async () => {
const result = await createTestSession({
appId: "my-app",
storageDir,
Expand All @@ -343,7 +353,7 @@ if (import.meta.vitest) {
sessionId: "stable-test-id",
});

const hex = await readFile(join(storageDir, "my-app_SsoSessionsV2.json"), "utf-8");
const hex = await readFile(join(storageDir, "my-app_SsoSessionsV3.json"), "utf-8");
const decoded = sessionsCodec.dec(fromHex(hex));

expect(decoded).toHaveLength(1);
Expand Down Expand Up @@ -401,10 +411,10 @@ if (import.meta.vitest) {
await createTestSession({ appId: "app-b", storageDir, sessionId: "id" });

await expect(
readFile(join(storageDir, "app-a_SsoSessionsV2.json"), "utf-8"),
readFile(join(storageDir, "app-a_SsoSessionsV3.json"), "utf-8"),
).resolves.toMatch(/^0x/);
await expect(
readFile(join(storageDir, "app-b_SsoSessionsV2.json"), "utf-8"),
readFile(join(storageDir, "app-b_SsoSessionsV3.json"), "utf-8"),
).resolves.toMatch(/^0x/);
});

Expand All @@ -415,15 +425,15 @@ if (import.meta.vitest) {
sessionId: "id",
});
await expect(
readFile(join(storageDir, "app_with_spaces_SsoSessionsV2.json"), "utf-8"),
readFile(join(storageDir, "app_with_spaces_SsoSessionsV3.json"), "utf-8"),
).resolves.toMatch(/^0x/);
});

test("creates storageDir when it does not yet exist", async () => {
const nested = join(storageDir, "does", "not", "exist");
await createTestSession({ appId: "my-app", storageDir: nested });
await expect(
readFile(join(nested, "my-app_SsoSessionsV2.json"), "utf-8"),
readFile(join(nested, "my-app_SsoSessionsV3.json"), "utf-8"),
).resolves.toMatch(/^0x/);
});

Expand Down Expand Up @@ -477,7 +487,7 @@ if (import.meta.vitest) {
sessionId: "second",
});

const hex = await readFile(join(storageDir, "my-app_SsoSessionsV2.json"), "utf-8");
const hex = await readFile(join(storageDir, "my-app_SsoSessionsV3.json"), "utf-8");
const decoded = sessionsCodec.dec(fromHex(hex));
expect(decoded).toHaveLength(1);
expect(decoded[0].id).toBe("second");
Expand Down
45 changes: 45 additions & 0 deletions product-sdk/pending-changesets/bump-host-papp-0.8.7-2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
"@parity/product-sdk-terminal": minor
"@parity/product-sdk-host": patch
"@parity/product-sdk-signer": patch
"@parity/product-sdk-statement-store": patch
"@parity/product-sdk": minor
---

**Bump `@novasamatech/host-api` family from `^0.8.6` to `^0.8.7-2`.** Picks up the upstream `deviceEncPubKey` addition on the V2 session schema (PR #212), the statement-store allowance-slot-prover fix (PR #214 — `createSr25519Prover` → `createSlotAccountProver`), and the `ExpiryTooLow` retry fix in `submitWithRetry`.

One consumer-visible behavioral change worth flagging up front:

> **CLI consumers using `@parity/product-sdk-terminal`** — host-papp `0.8.7-1` renamed the on-disk session storage key (`SsoSessionsV2` → `SsoSessionsV3`) and added a required `deviceEncPubKey: Bytes(65)` field on the persisted session. Sessions persisted from a previous CLI run will be invisible after upgrading; users will need to re-pair their phone the first time they launch the upgraded CLI. The `UserSecretsV2_<sessionId>.json` file format is unchanged.

### What's new

**Upstream catalog bump.** `@novasamatech/host-api`, `@novasamatech/host-api-wrapper`, `@novasamatech/host-papp`, `@novasamatech/statement-store`, `@novasamatech/storage-adapter`, and `@novasamatech/substrate-slot-sr25519-wasm` move from `^0.8.6` to `^0.8.7-2`. Headlines from upstream (between `release: 0.8.6 (#208)` and `chore(release): publish 0.8.7-2`):

- **`deviceEncPubKey` on the V2 session schema** (upstream PR #212). The persisted session codec gains a required `deviceEncPubKey: Bytes(65)` — the paired phone's long-lived ECDH key, lifted from `HandshakeResponseV2.deviceEncPubKey`, used by the host's device-sync channel. The storage key was renamed `SsoSessionsV2 → SsoSessionsV3` in the same release; the old graceful-degrade for V2 blobs is gone.
- **Statement-store allowance-slot-prover fix** (upstream PR #214). `AllowanceService.getStatementStoreProver` now uses `createSlotAccountProver` instead of `createSr25519Prover` — fixes a signature-scheme mismatch when proving slot-account-derived secrets. No public API change on our side (our `getStatementStoreProver` wrapper passes through unchanged), but the proofs the returned prover emits are now of the correct scheme.
- **`ExpiryTooLow` retry handling in `submitWithRetry`** (upstream `73cb870`). Internal to host-papp/statement-store retry logic; no consumer-side change.

### `@parity/product-sdk-terminal`

Internal codec mirror used by `createTestSession` updated to match host-papp 0.8.7-2's reshaped session schema:

- Appended `deviceEncPubKey: Bytes(65)` to the mirrored codec; the synthesized field reuses the remote peer's P-256 encryption pubkey (same value already used for `identityChatPublicKey` and `ssoEncPubKey`).
- Storage-key rename: `SsoSessionsV2.json` → `SsoSessionsV3.json`. The in-source unit tests and TSDoc references all updated.

No public-API change; `createTestSession`'s signature is unchanged. The interop test continues to round-trip the synthesized session through the real `SsoSessionManager` and `UserSecretRepository` to catch upstream drift early — both interop suites pass against host-papp 0.8.7-2.

### `@parity/product-sdk-host`, `@parity/product-sdk-signer`, `@parity/product-sdk-statement-store`

Patch-bumped to signal "tested against host-api(-wrapper) 0.8.7-2" via the published peer-dep / catalog resolution. No source change; runtime behavior is unchanged.

### Migration

**`@parity/product-sdk-terminal` — existing sessions need to be re-paired.** No source change required, but any sessions persisted to disk by a previous CLI run will be invisible after upgrading. host-papp 0.8.7-2 reads from `<storageDir>/<appId>_SsoSessionsV3.json`; the previous `SsoSessionsV2.json` path is no longer consulted, and the old graceful-degrade for stale blobs is gone.

What this means in practice:

- A user upgrading the CLI will see the same UX they'd see on a fresh install — `waitForSessions` returns no sessions until they complete a QR pairing.
- The old `SsoSessionsV2.json` file is not deleted, just ignored. Optional cleanup: surface a one-liner to the user ("we updated the session format, please re-pair") and `fs.unlink` the legacy path.
- The `UserSecretsV2_<sessionId>.json` file format is unchanged; legacy secrets files become orphaned (the new session has a different `sessionId`) but don't cause errors.
- Synthesized test sessions emitted by `createTestSession` automatically write to the new path — no test code change needed unless your tests asserted on the old filenames.
Loading
Loading