diff --git a/docs/backlog/24-access-streaming-v2.md b/docs/backlog/24-access-streaming-v2.md index 33c4e79..4d7f807 100644 --- a/docs/backlog/24-access-streaming-v2.md +++ b/docs/backlog/24-access-streaming-v2.md @@ -139,10 +139,11 @@ P3 first vertical slice delivered (`agent/audio-v2-p3`): the browser/device validation matrix, startup telemetry export, and the backend read-through gateway decision. -Product SDK adaptation note (updated 2026-08-26): +Product SDK adaptation note (updated 2026-08-30): - Product SDK 0.23.0 and deploy tooling 0.13.1 remain - prototype/reference/unaudited. + prototype/reference/unaudited in Dotify's pinned baseline. npm now publishes + Product SDK 0.25.0, which is deferred to a dedicated compatibility PR. - Root PAPI 3.0.0 is not adopted yet because the current Product SDK graph uses PAPI 2.2.x while `@polkadot-apps` still uses PAPI 1.23.x. - Host APIs are progressive enhancement for Product containers; standalone web diff --git a/docs/backlog/README.md b/docs/backlog/README.md index fae56a2..051e051 100644 --- a/docs/backlog/README.md +++ b/docs/backlog/README.md @@ -115,12 +115,13 @@ decisions. Product-signed key/session verification now exists through explicit Product account connection. Live host evidence remains required before Product identity can be treated as broadly proven across devices. -The Product SDK evidence snapshot used for this replanning is now +The Product SDK evidence snapshot used for this replanning currently pins `@parity/product-sdk` 0.23.0 and -`@polkadot-community-foundation/polkadot-app-deploy` 0.13.1, -last aligned with npm on 2026-08-26. The Product SDK set remains prototype / +`@polkadot-community-foundation/polkadot-app-deploy` 0.13.1. npm was rechecked +on 2026-08-30 and now publishes Product SDK 0.25.0; that migration is deferred +to a dedicated compatibility PR. The Product SDK set remains prototype / reference / unaudited code. Root `polkadot-api` 3.0.0 exists but is not yet -adopted because the current Product SDK packages use PAPI 2.2.x and +adopted because the current pinned Product SDK packages use PAPI 2.2.x and `@polkadot-apps` packages use PAPI 1.23.x, so a single PAPI 3 root breaks Dotify's Bulletin/wallet type seams. Product SDK contracts target `pallet-revive` / PolkaVM CDM flows, not Dotify's diff --git a/docs/backlog/improvement-plan.md b/docs/backlog/improvement-plan.md index bfb7653..2de1734 100644 --- a/docs/backlog/improvement-plan.md +++ b/docs/backlog/improvement-plan.md @@ -68,7 +68,9 @@ sound. Only the baseline rows marked delivered may be presented as live. Product SDK snapshot used for this plan: - `@parity/product-sdk` 0.23.0, host 0.16.0, statement-store 0.6.5, and - descriptors 0.10.0 (aligned with npm on 2026-08-26); + descriptors 0.10.0 (pinned baseline; npm rechecked on 2026-08-30 and now + publishes Product SDK 0.25.0 / host 0.18.0 / statement-store 0.6.7 / + descriptors 0.11.0, deferred to a dedicated compatibility PR); - `@polkadot-community-foundation/polkadot-app-deploy` 0.13.1; - deferred PAPI drift: root `polkadot-api` 3.0.0 is published, but the current Product SDK graph uses PAPI 2.2.x and `@polkadot-apps` uses PAPI 1.23.x, so diff --git a/docs/backlog/polkadot-product-readiness-and-killer-dapp-roadmap.md b/docs/backlog/polkadot-product-readiness-and-killer-dapp-roadmap.md index 591b33e..541c2a6 100644 --- a/docs/backlog/polkadot-product-readiness-and-killer-dapp-roadmap.md +++ b/docs/backlog/polkadot-product-readiness-and-killer-dapp-roadmap.md @@ -3,12 +3,14 @@ Status: active execution note; the Product DevNet baseline is delivered on `dev` and remains tracked through Product compatibility issue #85. -Last Product SDK package check: 2026-08-26. Dotify now pins the current -published Product SDK set: `@parity/product-sdk` 0.23.0, host 0.16.0, -statement-store 0.6.5, descriptors 0.10.0, and -`@polkadot-community-foundation/polkadot-app-deploy` 0.13.1. npm also -publishes `polkadot-api` 3.0.0, but Dotify keeps root PAPI on 1.23.3 because -the current Product SDK packages use PAPI 2.2.x and `@polkadot-apps` +Last Product SDK package check: 2026-08-30. Dotify currently pins +`@parity/product-sdk` 0.23.0, host 0.16.0, statement-store 0.6.5, +descriptors 0.10.0, and +`@polkadot-community-foundation/polkadot-app-deploy` 0.13.1. npm now publishes +Product SDK 0.25.0, host 0.18.0, statement-store 0.6.7, and descriptors 0.11.0; +that upgrade is deferred to a dedicated compatibility PR. npm also publishes +`polkadot-api` 3.0.0, but Dotify keeps root PAPI on 1.23.3 because the current +Product SDK packages use PAPI 2.2.x and `@polkadot-apps` chain-client/keys/signer use PAPI 1.23.x. Root PAPI 3 remains blocked until the official SDK graph converges. @@ -160,8 +162,8 @@ Goal: prove the Product host path with small spikes before committing the app. - Delivered: connect the app-scoped Product account only on explicit action and separate identity capability from EVM signing capability. - Delivered: publishable Bulletin/DotNS build and dual-origin Fly boundary. -- Delivered: align the Product SDK set with the 2026-08-26 npm latest while - keeping standalone/Product builds green. +- Delivered: align the Product SDK set with the 2026-08-26 npm latest at that + time while keeping standalone/Product builds green. - Remaining: prototype host transaction signing and resource allocation. - Compare Dotify's Hardhat/EVM runtime with Product SDK PolkaVM/CDM contracts. - Delivered on the room-beacon branch: Statement Store presence with strict @@ -205,6 +207,11 @@ Goal: deepen the delivered Product mode one adapter at a time. now has an explicit unsupported `product-cash` rail, so future CASH work can add receipt/bridge settlement without pretending CASH is already executable through `msg.value`. +- Delivered on the next follow-up branch: Product CDM writes now verify both + the selected host signer public key and its derived `pallet-revive` H160 + address against the Product account Dotify connected for key/session requests. + This prevents a Product-host transaction from marking access for a different + runtime account than the one the UI and backend authenticated. - Settled: the chain question. Product DevNet is a preset over the Paseo system parachains (Asset Hub 1000, People 1004, Bulletin 1010) at EVM chain 420420417, not a separate network. Dotify's contracts are already there, @@ -212,8 +219,10 @@ Goal: deepen the delivered Product mode one adapter at a time. and Hub TestNet endpoints. No contract redeploy is needed to port to DevNet. The SDK's `paseo` preset is Paseo Next (1500/1502), a different network, so `devnet` is the only environment Dotify can serve a catalog from. -- Next: `pallet-revive` account mapping plus real host-signed transaction smoke - tests before Product writes can replace the EVM wallet path. +- Next: real host-signed transaction smoke tests before Product writes can + replace the EVM wallet path. The remaining evidence is native value + forwarding, host approval UX, and post-payment `musicAccCanAccess` reads from + inside the Product container. - Next: revisit root `polkadot-api` 3 after Product SDK and `@polkadot-apps` packages publish on a compatible PAPI major line. - Next: confirm the Product-supported CASH settlement model before exposing any diff --git a/docs/explanation/product-devnet-architecture.md b/docs/explanation/product-devnet-architecture.md index a75edec..9d1522b 100644 --- a/docs/explanation/product-devnet-architecture.md +++ b/docs/explanation/product-devnet-architecture.md @@ -380,12 +380,14 @@ selection as reads: `viem` uses the connected EVM/passkey wallet, and default remains `viem`, since routing a payment or a publication through an unproven signer is not a reasonable default. -The remaining gate for Product contract _writes_ is now narrow: `pallet-revive` -account mapping for the signing account, and real host-signed transaction smoke -evidence from inside the container, including native value forwarding for -Classic unlock. The chain question is settled, the manifest and types exist, -and reads/writes share one port. Until that write evidence exists, -`VITE_DOTIFY_RUNTIME_ADAPTER` defaults to `viem`. +The remaining gate for Product contract _writes_ is now narrow. Dotify verifies +the selected host signer public key and derived `pallet-revive` H160 address +against the Product account that the UI and backend use for key/session +requests before any Product CDM write can be submitted. The remaining evidence +must come from inside the container: native value forwarding for Classic unlock, +host approval UX, and post-payment `musicAccCanAccess` reads. The chain question +is settled, the manifest and types exist, and reads/writes share one port. Until +that write evidence exists, `VITE_DOTIFY_RUNTIME_ADAPTER` defaults to `viem`. The backend authentication protocol now has an explicit signature scheme field. Standalone clients use the default `eip191` scheme. Product-host clients can @@ -466,11 +468,13 @@ The current baseline is: | Public gateway | `https://dotify-test01.dev-dot.li` | | Asset Hub EVM chain ID | `420420417` | -Checked against npm on 2026-08-26: the Product SDK set above is current. npm -also publishes `polkadot-api` `3.0.0`, but Dotify keeps root PAPI on `1.23.3` -for now. The current official Product SDK packages bring their own PAPI -`2.2.x` tree, while `@polkadot-apps` chain-client/keys/signer still depend on -PAPI `1.23.x`. A direct root PAPI 3 trial removed the public +Checked against npm on 2026-08-30: npm now publishes Product SDK `0.25.0`, +host `0.18.0`, statement-store `0.6.7`, and descriptors `0.11.0`. Dotify keeps +the baseline above for this branch and defers that migration to a dedicated SDK +compatibility PR. npm also publishes `polkadot-api` `3.0.0`, but Dotify keeps +root PAPI on `1.23.3` for now. The current pinned Product SDK packages bring +their own PAPI `2.2.x` tree, while `@polkadot-apps` chain-client/keys/signer +still depend on PAPI `1.23.x`. A direct root PAPI 3 trial removed the public `PolkadotSigner` export and broke `ChainDefinition` / `TypedApi` compatibility at the Bulletin and wallet seams. Root PAPI 3 therefore remains a blocked compatibility migration until the upstream SDK graph converges. diff --git a/docs/index.html b/docs/index.html index b8d8c53..303106b 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1547,8 +1547,9 @@
` link.
-7. A wallet-free browser joins that link from outside the Product host.
-8. The outside listener reaches `In sync` and hears the host stream; staying on
+8. A wallet-free browser joins that link from outside the Product host.
+9. The outside listener reaches `In sync` and hears the host stream; staying on
`Connecting...` means host capture or WebRTC negotiation is still failing,
not room creation.
-9. A Netlify-origin host and Product-origin guest also connect.
-10. Briefly interrupting the mobile network preserves and resumes the same room
+10. A Netlify-origin host and Product-origin guest also connect.
+11. Briefly interrupting the mobile network preserves and resumes the same room
within 120 seconds; it must disappear from public discovery while the host
is offline and return with the same code after reconnecting.
-11. Explicitly leaving ends the room immediately. Force-closing the host leaves
+12. Explicitly leaving ends the room immediately. Force-closing the host leaves
the room private until the 120-second resume window expires.
Inspect the browser console and Fly logs for CORS, catalog, Socket.IO, and
@@ -601,14 +622,17 @@ active.
signature is untagged, and a Substrate host may sign the payload verbatim or
inside a `` envelope. The API accepts both envelopes and both a bare
64-byte and a MultiSignature-tagged 65-byte sr25519 signature, so a correct
- host signature verifies regardless of which shape it uses. Step 6.4 above
- records which shape the live host actually produced - that observation is the
- evidence, and until it is captured the accepted set stays deliberately wide.
+ host signature verifies regardless of which shape it uses. The validation
+ step for `product-sr25519-v1` records which shape the live host actually
+ produced - that observation is the evidence, and until it is captured the
+ accepted set stays deliberately wide.
- Contract writes still require passkey/EVM signing in the shipped UI. The
Product CDM/PAPI runtime adapter now has its generated manifest, contract
types, and a live resolver, so the only thing still missing before it can be
- selected is `pallet-revive` account mapping plus real host-signed transaction
- evidence.
+ selected is real host-signed transaction evidence. Dotify now validates that
+ the selected Product host signer public key maps to the same `pallet-revive`
+ H160 account used by the connected Product identity before a CDM write can be
+ submitted.
- Rooms still depend on one in-memory Fly signaling machine.
- Product-host cloud storage does not hold Dotify audio or content keys.
- Product personhood is not yet an access decision source.
@@ -617,5 +641,5 @@ active.
- Product contract mode (`VITE_DOTIFY_RUNTIME_ADAPTER=product-cdm`) covers
catalog reads and runtime write submissions inside the Product host. The
tracked deployment still keeps the default `viem` adapter until
- `pallet-revive` account mapping, native value forwarding, and host-signed
- transaction evidence are captured.
+ native value forwarding, host approval UX, and post-payment access evidence
+ are captured.
diff --git a/web/src/features/runtime/productCdmContracts.ts b/web/src/features/runtime/productCdmContracts.ts
index fc513da..9c280fb 100644
--- a/web/src/features/runtime/productCdmContracts.ts
+++ b/web/src/features/runtime/productCdmContracts.ts
@@ -71,7 +71,7 @@ export type ProductChainEnvironment = 'devnet';
export type ProductCdmContractsOptions = {
environment: ProductChainEnvironment;
- /** Signer manager from @parity/product-sdk-signer, when transactions are in scope. */
+ /** Signer manager from @parity/product-sdk/wallet, when transactions are in scope. */
signerManager?: unknown;
};
diff --git a/web/src/features/runtime/runtimeWriterProvider.test.ts b/web/src/features/runtime/runtimeWriterProvider.test.ts
index 4543949..7d18d96 100644
--- a/web/src/features/runtime/runtimeWriterProvider.test.ts
+++ b/web/src/features/runtime/runtimeWriterProvider.test.ts
@@ -7,6 +7,8 @@ const runtime = '0xcccccccccccccccccccccccccccccccccccccccc' as const;
const hash = '0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd' as const;
const productPublicKey = `0x${'11'.repeat(32)}` as const;
const differentProductPublicKey = `0x${'22'.repeat(32)}` as const;
+const productH160Address = '0x9999999999999999999999999999999999999999' as const;
+const differentProductH160Address = '0x8888888888888888888888888888888888888888' as const;
const viemWriter = {
createRuntime: vi.fn(async () => txHash),
@@ -41,14 +43,15 @@ afterEach(() => {
vi.unstubAllEnvs();
vi.resetModules();
vi.doUnmock('./productCdmContracts');
- vi.doUnmock('@parity/product-sdk-signer');
+ vi.doUnmock('@parity/product-sdk/wallet');
+ vi.doUnmock('@parity/product-sdk/address');
});
async function loadProvider() {
return (await import('./runtimeWriterProvider')).createRuntimeWriter;
}
-function productAccount(publicKey = productPublicKey) {
+function productAccount(publicKey = productPublicKey, h160Address = h160ForPublicKey(publicKey)) {
return {
publicKey: Uint8Array.from(
publicKey
@@ -57,7 +60,7 @@ function productAccount(publicKey = productPublicKey) {
.map(byte => Number.parseInt(byte, 16))
),
address: '5ProductAccount',
- h160Address: '0x9999999999999999999999999999999999999999'
+ h160Address
};
}
@@ -73,8 +76,10 @@ function mockProductSigner(selectedAccount = productAccount()) {
return signerManager;
});
const HostProvider = vi.fn(() => ({ type: 'host' }));
- vi.doMock('@parity/product-sdk-signer', () => ({ SignerManager, HostProvider }));
- return { signerManager, signerManagerOptions, SignerManager, HostProvider };
+ const deriveH160 = vi.fn((publicKey: Uint8Array) => h160ForPublicKey(hexFromBytes(publicKey)));
+ vi.doMock('@parity/product-sdk/wallet', () => ({ SignerManager, HostProvider }));
+ vi.doMock('@parity/product-sdk/address', () => ({ deriveH160 }));
+ return { signerManager, signerManagerOptions, SignerManager, HostProvider, deriveH160 };
}
function accessIntent(amountPlanck: bigint) {
@@ -125,7 +130,7 @@ describe('createRuntimeWriter', () => {
it('routes Product writes through CDM setup without falling back to viem', async () => {
vi.stubEnv('VITE_DOTIFY_RUNTIME_ADAPTER', 'product-cdm');
- const { signerManager, signerManagerOptions, SignerManager, HostProvider } = mockProductSigner();
+ const { signerManager, signerManagerOptions, SignerManager, HostProvider, deriveH160 } = mockProductSigner();
const verifyDeployment = vi.fn(async () => undefined);
const resolver = { getRuntimeContract: vi.fn() };
vi.doMock('./productCdmContracts', () => ({
@@ -143,7 +148,7 @@ describe('createRuntimeWriter', () => {
ethRpcUrl: 'https://rpc.example',
getViemWalletClient,
config: { kind: 'product-cdm', productEnvironment: 'devnet' },
- productAccount: { productId: 'dotify-test01.dot', publicKey: productPublicKey }
+ productAccount: { productId: 'dotify-test01.dot', evmAddress: productH160Address, publicKey: productPublicKey }
});
await expect(writer.createRuntime(factory)).resolves.toBe(txHash);
@@ -165,6 +170,7 @@ describe('createRuntimeWriter', () => {
}
});
expect(signerManager.connect).toHaveBeenCalledWith('host');
+ expect(deriveH160).toHaveBeenCalledWith(productAccount().publicKey);
expect(createProductCdmContracts).toHaveBeenCalledTimes(1);
expect(createProductCdmContracts).toHaveBeenCalledWith({ environment: 'devnet', signerManager });
expect(verifyDeployment).toHaveBeenCalledTimes(1);
@@ -195,4 +201,52 @@ describe('createRuntimeWriter', () => {
expect(signerManager.destroy).toHaveBeenCalledTimes(1);
expect(createProductCdmContracts).not.toHaveBeenCalled();
});
+
+ it('rejects Product writes when the host signer maps to a different pallet-revive H160 address', async () => {
+ vi.stubEnv('VITE_DOTIFY_RUNTIME_ADAPTER', 'product-cdm');
+ const { signerManager } = mockProductSigner(productAccount(productPublicKey, productH160Address));
+ const createProductCdmContracts = vi.fn();
+ vi.doMock('./productCdmContracts', () => ({ createProductCdmContracts }));
+ vi.resetModules();
+ const createRuntimeWriter = await loadProvider();
+
+ const writer = createRuntimeWriter({
+ ethRpcUrl: 'https://rpc.example',
+ getViemWalletClient: vi.fn(async () => ({}) as never),
+ config: { kind: 'product-cdm', productEnvironment: 'devnet' },
+ productAccount: { productId: 'dotify-test01.dot', evmAddress: differentProductH160Address, publicKey: productPublicKey }
+ });
+
+ await expect(writer.payForAccess(accessIntent(1n))).rejects.toThrow(/maps to/);
+ expect(signerManager.destroy).toHaveBeenCalledTimes(1);
+ expect(createProductCdmContracts).not.toHaveBeenCalled();
+ });
+
+ it('rejects Product writes when the signer-reported H160 disagrees with the derived public-key mapping', async () => {
+ vi.stubEnv('VITE_DOTIFY_RUNTIME_ADAPTER', 'product-cdm');
+ const { signerManager } = mockProductSigner(productAccount(productPublicKey, differentProductH160Address));
+ const createProductCdmContracts = vi.fn();
+ vi.doMock('./productCdmContracts', () => ({ createProductCdmContracts }));
+ vi.resetModules();
+ const createRuntimeWriter = await loadProvider();
+
+ const writer = createRuntimeWriter({
+ ethRpcUrl: 'https://rpc.example',
+ getViemWalletClient: vi.fn(async () => ({}) as never),
+ config: { kind: 'product-cdm', productEnvironment: 'devnet' },
+ productAccount: { productId: 'dotify-test01.dot', evmAddress: productH160Address, publicKey: productPublicKey }
+ });
+
+ await expect(writer.payForAccess(accessIntent(1n))).rejects.toThrow(/account mapping mismatch/);
+ expect(signerManager.destroy).toHaveBeenCalledTimes(1);
+ expect(createProductCdmContracts).not.toHaveBeenCalled();
+ });
});
+
+function hexFromBytes(bytes: Uint8Array): `0x${string}` {
+ return `0x${Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('')}` as `0x${string}`;
+}
+
+function h160ForPublicKey(publicKey: `0x${string}`): `0x${string}` {
+ return publicKey === productPublicKey ? productH160Address : differentProductH160Address;
+}
diff --git a/web/src/features/runtime/runtimeWriterProvider.ts b/web/src/features/runtime/runtimeWriterProvider.ts
index 64b7be1..53f1993 100644
--- a/web/src/features/runtime/runtimeWriterProvider.ts
+++ b/web/src/features/runtime/runtimeWriterProvider.ts
@@ -16,6 +16,8 @@ type ViemWalletClient = Awaited>;
export type ProductRuntimeSignerAccount = {
productId: string;
derivationIndex?: number;
+ /** H160 address Dotify uses for runtime access checks and pallet-revive calls. */
+ evmAddress?: `0x${string}`;
/** Product account public key already approved by Dotify's wallet flow. */
publicKey?: `0x${string}`;
};
@@ -48,16 +50,24 @@ type ProductSignerModule = {
HostProvider: new (options?: unknown) => unknown;
};
+type ProductAddressModule = {
+ deriveH160: (publicKey: Uint8Array) => `0x${string}`;
+};
+
async function createViemWriter(deps: RuntimeWriterDeps): Promise {
const walletClient = await deps.getViemWalletClient();
return createViemRuntimeWriter({ ethRpcUrl: deps.ethRpcUrl, walletClient });
}
async function createProductSignerManager(account?: ProductRuntimeSignerAccount): Promise {
- const { SignerManager, HostProvider } = (await import('@parity/product-sdk-signer')) as unknown as ProductSignerModule;
+ const [{ SignerManager, HostProvider }, { deriveH160 }] = await Promise.all([
+ import('@parity/product-sdk/wallet') as Promise,
+ import('@parity/product-sdk/address') as Promise
+ ]);
const productId = account?.productId?.trim() || productIdFromEnv();
const derivationIndex = account?.derivationIndex ?? 0;
const expectedPublicKey = normalizeHex(account?.publicKey);
+ const expectedH160Address = normalizeHex(account?.evmAddress);
const manager = new SignerManager({
dappName: productId,
@@ -89,6 +99,23 @@ async function createProductSignerManager(account?: ProductRuntimeSignerAccount)
}
const selectedPublicKey = hexFromBytes(selectedAccount.publicKey);
+ const derivedH160Address = normalizeHex(deriveH160(selectedAccount.publicKey));
+ const selectedH160Address = normalizeHex(selectedAccount.h160Address);
+
+ if (selectedH160Address && selectedH160Address !== derivedH160Address) {
+ manager.destroy();
+ throw new Error(
+ `Product CDM signer account mapping mismatch: the host signer reports H160 ${selectedH160Address}, but deriving pallet-revive H160 from its Product public key gives ${derivedH160Address}. Refusing to submit a runtime transaction.`
+ );
+ }
+
+ if (expectedH160Address && derivedH160Address !== expectedH160Address) {
+ manager.destroy();
+ throw new Error(
+ `Product CDM signer account mismatch: the connected Dotify account maps to ${expectedH160Address}, but the Product host signer maps to ${derivedH160Address}. Reconnect the Product account before submitting a runtime transaction.`
+ );
+ }
+
if (expectedPublicKey && selectedPublicKey.toLowerCase() !== expectedPublicKey) {
manager.destroy();
throw new Error(
diff --git a/web/src/hooks/useCatalog.ts b/web/src/hooks/useCatalog.ts
index d960126..56b95cf 100644
--- a/web/src/hooks/useCatalog.ts
+++ b/web/src/hooks/useCatalog.ts
@@ -297,6 +297,7 @@ export function useCatalog(deps: UseCatalogDeps) {
const signer = connectedWallet.keyRequestSigner;
return {
productId: productHostConfig.productId,
+ evmAddress: connectedWallet.evmAddress,
publicKey: signer && 'productPublicKey' in signer ? signer.productPublicKey : undefined
};
}, [connectedWallet, productHostConfig.productId]);