Skip to content
Open
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
5 changes: 5 additions & 0 deletions .changeset/sso-secret-derives.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@parity/truapi-host": patch
---

Zeroize SSO session and pairing secrets on drop and redact responder and handshake secrets from debug output.
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ jobs:
run: cargo +"$TRUAPI_NIGHTLY_TOOLCHAIN" clippy -p truapi-provider --no-default-features --features uniffi --all-targets -- -D warnings

- name: cargo (pinned nightly) fmt --check
run: cargo +"$TRUAPI_NIGHTLY_TOOLCHAIN" fmt --check
run: make fmt-check

- name: cargo (pinned nightly) clippy
run: cargo +"$TRUAPI_NIGHTLY_TOOLCHAIN" clippy --workspace --all-targets --all-features -- -D warnings
Expand Down Expand Up @@ -777,6 +777,9 @@ jobs:
- name: Install
run: npm ci --ignore-scripts

- name: Check formatting
run: npm run format:check --prefix js/packages/truapi-host

- name: Build
run: npm run build --prefix js/packages/truapi-host

Expand Down
26 changes: 26 additions & 0 deletions .rustfmt.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Basic
edition = "2024"
hard_tabs = true
max_width = 100
use_small_heuristics = "Max"
# Imports
imports_granularity = "Crate"
reorder_imports = true
# Consistency
newline_style = "Unix"
normalize_comments = true
normalize_doc_attributes = true
# Misc
chain_width = 80
spaces_around_ranges = false
binop_separator = "Back"
reorder_impl_items = false
match_arm_leading_pipes = "Never"
match_arm_blocks = true
match_block_trailing_comma = true
trailing_comma = "Vertical"
trailing_semicolon = true
use_field_init_shorthand = true
# Format comments
comment_width = 100
wrap_comments = true
5 changes: 2 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,8 +334,7 @@ belongs to an existing type rather than adding another free-standing export.
Preserve the local style. Do not add semicolons to `return`, `break` or
`continue` where the file omits them, do not add braces to match arms or
`if`/`else` written without them, and do not move operators between the end of
one line and the start of the next. Format with `cargo +$(cat nightly-toolchain) fmt`, and keep
it to the lines you touched.
one line and the start of the next, except where the repository formatter requires it. Format with `make fmt`; it uses the pinned nightly and includes runtime modules declared inside macros. Keep formatting limited to files you changed.

## Rust style

Expand Down Expand Up @@ -416,7 +415,7 @@ Move the date in that one file, and in `truapi.rustNightly` in

```bash
cargo build --workspace
cargo +$(cat nightly-toolchain) fmt --check
make fmt-check
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test --workspace
```
Expand Down
13 changes: 10 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# Run `make help` for the list of targets.

.DEFAULT_GOAL := help
.PHONY: help setup build codegen test check check-generated clean playground wasm wasm-crypto-test uniffi uniffi-kotlin android-check provider-android-check ios-build ios-run ios-chat-run ios-chat-host-playground-run ios-chat-all android-jni android-publish-local dotli-link dev dev-cli dev-bootstrap debugger dev-link-check e2e-dotli e2e-cli-diagnosis e2e-signing-cli e2e-pairing-cli e2e-chat-cli e2e-pocket-cli e2e-cross-product-storage e2e-cross-product-ringvrf e2e-cross-product-signing e2e-cli-update headless install cli-runner cli-dist matrix explorer xcframework
.PHONY: help setup build codegen test check fmt fmt-check check-generated clean playground wasm wasm-crypto-test uniffi uniffi-kotlin android-check provider-android-check ios-build ios-run ios-chat-run ios-chat-host-playground-run ios-chat-all android-jni android-publish-local dotli-link dev dev-cli dev-bootstrap debugger dev-link-check e2e-dotli e2e-cli-diagnosis e2e-signing-cli e2e-pairing-cli e2e-chat-cli e2e-pocket-cli e2e-cross-product-storage e2e-cross-product-ringvrf e2e-cross-product-signing e2e-cli-update headless install cli-runner cli-dist matrix explorer xcframework

CARGO ?= cargo
# The dated nightly CI runs; see nightly-toolchain.
Expand Down Expand Up @@ -336,14 +336,21 @@ test: check-generated ## Run Rust + TypeScript client tests.
cd $(TRUAPI_PKG) && npm test
cd $(HOST_WASM_PKG) && npm run build && npm test

fmt: ## Format Rust sources.
cargo +$(NIGHTLY_TOOLCHAIN) fmt --all $(RUSTFMT_FLAGS)
git ls-files --cached --others --exclude-standard -z -- 'rust/crates/truapi/src/*.rs' | xargs -0 rustfmt +$(NIGHTLY_TOOLCHAIN) --config skip_children=true $(RUSTFMT_FLAGS)

fmt-check: RUSTFMT_FLAGS := --check
fmt-check: fmt ## Check Rust formatting.

check: check-generated ## Full verification suite (build, fmt, clippy, test, TS tests, playground build + lint).
cargo build --workspace
cargo check --target wasm32-unknown-unknown -p truapi
cargo +$(NIGHTLY_TOOLCHAIN) fmt --check
$(MAKE) fmt-check
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test --workspace --all-features --all-targets
cd $(TRUAPI_PKG) && npm run build && npm test
cd $(HOST_WASM_PKG) && npm install --no-fund --no-audit && npm run build && npm test
cd $(HOST_WASM_PKG) && npm install --no-fund --no-audit && npm run format:check && npm run build && npm test
cd $(PLAYGROUND) && yarn build && yarn lint && yarn test:unit

clean: ## Remove local build/test artifacts without deleting dependencies.
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ for a wallet-initiated disconnect) on `TrUAPIHostRuntime`. Response posting and
session-record cleanup remain on the wallet side.
See the core's [inter-host SSO design](rust/crates/truapi/RUNTIME.md#inter-host-sso)
for typed handlers, canonical resource types, and consent bound to the signing session.
SSO session, responder identity and handshake payload structs zeroize their fields on drop; debug output redacts their secret fields.
Product and SSO signing share canonical payloads and the one-byte `OptionBool`
encoding for `with_signed_transaction`.

Expand Down Expand Up @@ -283,6 +284,10 @@ make wasm # rebuild truapi WASM artifacts under js/packages/truapi-host/dist
CI regenerates the shared bindings before building and testing both npm
packages, so generated client and host callback changes are checked together.

CI and `make check` enforce Rust formatting, including runtime modules declared inside macros, and Prettier formatting for the JS host package. Run `make fmt` and `npm run format --prefix js/packages/truapi-host` to apply the same rules locally. Use `make fmt-check` or `format:check` for the JS package to check without writing. Rust uses the nightly in `nightly-toolchain` and the [Polkadot SDK formatting rules](https://github.com/paritytech/polkadot-sdk/blob/master/.rustfmt.toml) with edition 2024.

Rust runtime tests live beside their components; [native binding tests](rust/crates/truapi/src/native/tests.rs) exercise the exported host API.

The native `truapi-host` utility runs pairing and signing hosts against the real
SSO transport for local end-to-end work. See [Install the CLI](#install-the-cli)
to get it, and the [`truapi-host-cli` guide](rust/crates/truapi-host-cli/README.md)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package io.paritytech.polkadotapp.feature_products_impl.domain.truapi
import io.parity.truapi.HostCoreStorage
import io.parity.truapi.HostStorage
import io.paritytech.polkadotapp.common.data.storage.preferences.encrypted.EncryptedPreferences
import uniffi.truapi.HostRejection
import uniffi.truapi.HostLocalStorageReadException
import uniffi.truapi.HostRejection
import java.text.Normalizer

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import uniffi.truapi.HostRejection
import uniffi.truapi.ProductExecutionKind

class ProductTrUAPIHostBridgeTest {
// The core refuses the open: an unavailable loopback port, or an execution config it rejects.
private val refusingCore = Answer<Any> { throw IllegalStateException("loopback port unavailable") }

private val gameReminder = mock(ProductGameReminder::class.java)
Expand All @@ -46,8 +45,6 @@ class ProductTrUAPIHostBridgeTest {
scope = CoroutineScope(StandardTestDispatcher(testScheduler)),
)

// The callers launch attach into scopes with no handler, so a refusal from the core has to come
// back as the Result the signature promises rather than as a crash.
@Test
fun `a core that refuses to open the execution fails the attach instead of throwing`() = runTest {
val outcome = bridge().attach(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,7 @@ extension SSOTruAPICoordinator: MessageExchangeSignInHostCoordinating {
return
}

let runtime = try runtimeProvider.sharedRuntime()
let disconnectBytes = runtime.prepareDisconnectRequest()
let disconnectBytes = try runtimeProvider.sharedRuntime().prepareDisconnectRequest()

logger.debug("Posting disconnect request to host \(host.name)")
try await rawSender.postMessage(SSORawHostMessage(rawBytes: disconnectBytes), to: host)
Expand Down
4 changes: 2 additions & 2 deletions ios/truapi-host/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -372,8 +372,8 @@ func prepareDisconnectRequest() -> Data
`handleSsoRequest(message:)` takes one SCALE-encoded `RemoteMessage` exactly as decrypted from the statement-store session and routes it through the Rust core. The returned `SsoRequestOutcome` is the generated UniFFI enum (no Swift mirror):

- `.response(message:)` — SCALE-encoded reply; post it back over the same session.
- `.disconnected` — the peer ended the session; tear down the transport and records on the wallet side.
- `.ignored` — the message was not a request; nothing to post.
- `.disconnected`: the peer ended the session; tear down its transport and records.
- `.ignored`: nothing to post.

Confirmation-gated requests suspend on `confirmUserAction` or `confirmPermission`, so `handleSsoRequest` can take arbitrarily long. Always call it from a `Task`, never the main thread.

Expand Down
2 changes: 2 additions & 0 deletions js/packages/truapi-host/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@
],
"scripts": {
"build": "tsc -b && node scripts/build-cjs.mjs",
"format": "prettier --write \"src/**/*.{ts,tsx}\" \"scripts/**/*.{js,mjs,ts}\" \"!src/generated/**\"",
"format:check": "prettier --check \"src/**/*.{ts,tsx}\" \"scripts/**/*.{js,mjs,ts}\" \"!src/generated/**\"",
"typecheck:harness": "tsc -p tsconfig.harness.json",
"build:wasm": "node scripts/build-wasm.mjs",
"test": "bun test"
Expand Down
12 changes: 8 additions & 4 deletions js/packages/truapi-host/scripts/build-wasm.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -186,10 +186,14 @@ async function build(crate, outName, target, subdir, features = [], env = {}) {
} → ${outDir}\n`,
);
try {
await execFileAsync("wasm-pack", args(crate, outName, target, outDir, features), {
cwd: repoRoot,
env: { ...process.env, ...env },
});
await execFileAsync(
"wasm-pack",
args(crate, outName, target, outDir, features),
{
cwd: repoRoot,
env: { ...process.env, ...env },
},
);
} catch (err) {
if (err?.code === "ENOENT") {
console.error(
Expand Down
5 changes: 4 additions & 1 deletion js/packages/truapi-host/scripts/fidelity-report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,8 @@ const rows = await runDiagnosis(client, {
dispose();

mkdirSync(dirname(REPORT_PATH), { recursive: true });
writeFileSync(REPORT_PATH, renderDiagnosisReport("TrUAPI Mock Host Diagnosis", rows));
writeFileSync(
REPORT_PATH,
renderDiagnosisReport("TrUAPI Mock Host Diagnosis", rows),
);
console.error(`wrote ${REPORT_PATH}`);
20 changes: 12 additions & 8 deletions js/packages/truapi-host/src/host-callbacks-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,19 @@ it("preserves one-use permission decisions across the WASM callback", async () =
};
for (const decision of ["AllowOnce", "AllowAlways", "Deny"] as const) {
const reviews: UserConfirmationReview[] = [];
const raw = createWasmRawCallbacks(makeHostCallbacks({
userConfirmation: {
confirmPermission: async (request) => {
reviews.push(request);
return decision;
const raw = createWasmRawCallbacks(
makeHostCallbacks({
userConfirmation: {
confirmPermission: async (request) => {
reviews.push(request);
return decision;
},
},
},
}));
const encoded = await raw.confirmPermission(UserConfirmationReview.enc(review));
}),
);
const encoded = await raw.confirmPermission(
UserConfirmationReview.enc(review),
);
expect({ decision: PermissionDecision.dec(encoded), reviews }).toEqual({
decision,
reviews: [review],
Expand Down
4 changes: 1 addition & 3 deletions js/packages/truapi-host/src/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@ import type {
// SCALE bytes. The web worker pairing-host runtime adapts this typed surface
// into the byte-oriented callback bridge consumed by the WASM core.
export * from "./generated/host-callbacks.js";
export type {
JsonRpcConnection as PlatformJsonRpcConnection,
} from "./generated/host-callbacks.js";
export type { JsonRpcConnection as PlatformJsonRpcConnection } from "./generated/host-callbacks.js";

/** Encode a typed core-storage slot for hosts that need an opaque backing key. */
export function encodeCoreStorageKey(key: CoreStorageKey): Uint8Array {
Expand Down
6 changes: 5 additions & 1 deletion js/packages/truapi-host/src/testing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
// carry the mock with it. The implementation lives under `web/` because it
// mocks the web host's callback seam; only the entry point is split.

export { createMockHost, mockRuntimeConfig, MOCK_GENESIS } from "./web/create-mock-host.js";
export {
createMockHost,
mockRuntimeConfig,
MOCK_GENESIS,
} from "./web/create-mock-host.js";
export type {
ChainStatus,
ChatMessageRecord,
Expand Down
5 changes: 4 additions & 1 deletion js/packages/truapi-host/src/testing/browser-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ const accounts = params
const name = entry.slice(0, separator);
const hex = entry.slice(separator + 1);
const bytes = hex.match(/../g) ?? [];
return { name, entropy: Uint8Array.from(bytes.map((b) => parseInt(b, 16))) };
return {
name,
entropy: Uint8Array.from(bytes.map((b) => parseInt(b, 16))),
};
});
const login = params.get("login");
const productId = params.get("productId") ?? undefined;
Expand Down
10 changes: 7 additions & 3 deletions js/packages/truapi-host/src/testing/create-mock-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ function hex(bytes: Uint8Array): `0x${string}` {
return `0x${digits.join("")}`;
}

const suite = wasmIsBuilt("testing/truapi_server.js") ? describe : describe.skip;
const suite = wasmIsBuilt("testing/truapi_server.js")
? describe
: describe.skip;

suite("createMockClient", () => {
it("round-trips a product call to the mock host", async () => {
Expand Down Expand Up @@ -108,7 +110,8 @@ suite("createMockClient", () => {

// The core caches a decided authorization, so a product asking twice is
// answered from that record and the second call never reaches the host.
const repeated = await client.permissions.requestDevicePermission("Camera");
const repeated =
await client.permissions.requestDevicePermission("Camera");
expect(repeated._unsafeUnwrap().granted).toBe(false);
expect(
host.getPermissionLog(),
Expand All @@ -120,7 +123,8 @@ suite("createMockClient", () => {
// the host afresh. Without this a suite can only ever observe the answer
// its first request happened to settle on.
host.grantPermission("Camera");
const granted = await client.permissions.requestDevicePermission("Camera");
const granted =
await client.permissions.requestDevicePermission("Camera");
expect(granted._unsafeUnwrap().granted).toBe(true);
expect(host.getPermissionLog()).toHaveLength(2);
} finally {
Expand Down
17 changes: 12 additions & 5 deletions js/packages/truapi-host/src/testing/create-mock-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@
// rather than a direct function call. What it does NOT exercise is the iframe
// boundary and the browser's origin checks; the fixture covers those.

import { createClient, createMessagePortProvider, createTransport } from "@parity/truapi";
import {
createClient,
createMessagePortProvider,
createTransport,
} from "@parity/truapi";
import type { TrUApiClient } from "@parity/truapi";

import {
Expand All @@ -25,7 +29,11 @@ import {
type MockHostConfig,
} from "../web/create-mock-host.js";
import type { ProductRuntimeConfig } from "../runtime.js";
import { resolveAccount, type DevAccount, type DevAccountName } from "./dev-accounts.js";
import {
resolveAccount,
type DevAccount,
type DevAccountName,
} from "./dev-accounts.js";

/** Options for {@link createMockClient}. */
export interface MockClientOptions {
Expand Down Expand Up @@ -80,9 +88,8 @@ export async function createMockClient(
};
await glue.default();

const { createWasmRawCallbacks } = await import(
"../generated/host-callbacks-adapter.js"
);
const { createWasmRawCallbacks } =
await import("../generated/host-callbacks-adapter.js");

const host = createMockHost(options.mock);
const { productId, ...hostConfig } = mockRuntimeConfig(
Expand Down
9 changes: 5 additions & 4 deletions js/packages/truapi-host/src/testing/dev-accounts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ const suite = wasmIsBuilt("testing/truapi_server_bg.wasm")

describe("dev account specs", () => {
it("rejects an unknown name and a wrong-sized key", () => {
expect(() => resolveAccount("eve" as "alice")).toThrow(/unknown dev account/);
expect(() => resolveAccount("eve" as "alice")).toThrow(
/unknown dev account/,
);
expect(() =>
resolveAccount({ name: "short", entropy: new Uint8Array(16) }),
).toThrow(/32 bytes/);
Expand All @@ -44,9 +46,8 @@ describe("dev account specs", () => {
suite("dev accounts against the real signing host", () => {
async function signingRuntime() {
const { initSync, WasmSigningHostRuntime } = await import(glueUrl.href);
const { createWasmRawCallbacks } = await import(
"../generated/host-callbacks-adapter.js"
);
const { createWasmRawCallbacks } =
await import("../generated/host-callbacks-adapter.js");
initSync({ module: readFileSync(wasmUrl) });
const mock = createMockHost();
const { productId, ...hostConfig } = mockRuntimeConfig();
Expand Down
Loading
Loading