Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
5b9f22e
refactor(host): adopt Rust TrUAPI runtime
pgherveou Jul 15, 2026
9b29b7b
test(ui): wait for bridge lifecycle events
pgherveou Jul 15, 2026
2864c01
test(ui): allow cold topbar import
pgherveou Jul 15, 2026
95b810c
fix(ui): preserve notification requests
pgherveou Jul 15, 2026
e8915f0
docs(shared): remove stale auth reference
pgherveou Jul 15, 2026
44b880c
refactor(protocol): unify auth storage keys
pgherveou Jul 15, 2026
6e8dc55
refactor(protocol): defer chainSend flush error responses to a follow-up
pgherveou Jul 15, 2026
d2c57a1
refactor(resolver): defer dependency pruning and coverage tests to a …
pgherveou Jul 15, 2026
984f073
refactor(debug): defer panel fixes unrelated to the port to a follow-up
pgherveou Jul 15, 2026
98690c0
refactor(ui): defer permission-prompt rate limiting to a follow-up
pgherveou Jul 15, 2026
6c01473
refactor(ui): defer allowance-key at-rest encryption to a follow-up
pgherveou Jul 15, 2026
c6a1d94
refactor(bridge): defer the per-frame wire debug tap to a follow-up
pgherveou Jul 15, 2026
cf99551
refactor(topbar): defer login-failure copy and popover hardening to a…
pgherveou Jul 15, 2026
963ac83
build(ui): depend on published @parity/truapi 0.4.0 and @parity/truap…
pgherveou Jul 15, 2026
0bf87e5
build: link local TrUAPI packages
pgherveou Jul 15, 2026
95e8bac
refactor(config): keep base formatting for unchanged gateway helpers
pgherveou Jul 15, 2026
d1640a1
fix(ui): translate legacy Nova follow ids
pgherveou Jul 16, 2026
9ec38d0
fix(ui): isolate opened navigation tabs
pgherveou Jul 17, 2026
6dea057
fix(ui): avoid duplicate permission rows
pgherveou Jul 17, 2026
c35f944
fix(ui): reject login when core closes
pgherveou Jul 17, 2026
26a93be
test(ui): cover confirmation variants
pgherveou Jul 17, 2026
8f0c9cd
fix(ui): pin legacy bridge origin
pgherveou Jul 17, 2026
4493ea3
fix(ui): verify cached preimages
pgherveou Jul 17, 2026
29d89b3
fix(ui): handle ring proof reviews
pgherveou Jul 17, 2026
b93d9a0
chore: merge main into Rust core port
pgherveou Jul 17, 2026
f7d372f
fix(ui): support published confirmation types
pgherveou Jul 17, 2026
ed783fc
fix(ui): retain legacy alias shape
pgherveou Jul 17, 2026
37a6f54
build(ui): update TruAPI to 0.4.1
pgherveou Jul 17, 2026
9648cf6
test(e2e): support local host playground
pgherveou Jul 20, 2026
7c27048
update with temporary packages
pgherveou Jul 20, 2026
be0092a
merge
pgherveou Jul 20, 2026
b570793
fix bridge
pgherveou Jul 20, 2026
b407af4
update package deps
pgherveou Jul 20, 2026
0fb0672
fix modal
pgherveou Jul 20, 2026
9667cbf
fix(metrics): keep event callbacks intact
pgherveou Jul 20, 2026
2840b45
fix(ui): normalize local legacy accounts
pgherveou Jul 20, 2026
7921ce4
fix(ui): address core migration regressions
pgherveou Jul 20, 2026
3d26792
update lock files
pgherveou Jul 21, 2026
66ace10
remove nova-removal.test.ts
pgherveou Jul 21, 2026
4730658
refactor tests
pgherveou Jul 21, 2026
90d4974
fix(deps): update vulnerable transitive deps
pgherveou Jul 21, 2026
5de0ee5
fix(ui): broker smoldot core connections
pgherveou Jul 22, 2026
0258973
fix(deps): update fast-uri
pgherveou Jul 22, 2026
aa229af
refactor(protocol): centralize chain ownership
pgherveou Jul 22, 2026
72bda6c
fix(shared): extension-qualify subpath exports
pgherveou Jul 23, 2026
99549e2
refactor(protocol): drop unused error-code plumbing
pgherveou Jul 23, 2026
ae4112b
fix(host): classify iframe chunk-load fatals as module-fetch errors
pgherveou Jul 23, 2026
bcdbb6f
Merge branch 'main' into codex/centralize-chain-provider-ownership
Jul 28, 2026
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
24 changes: 24 additions & 0 deletions apps/host/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,29 @@ export default [
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
"no-restricted-imports": [
"error",
{
paths: [
{
name: "@dotli/protocol/broker",
message:
"Host code must open chains through @dotli/protocol/client.",
},
{
name: "@dotli/resolver/chains",
message:
"Smoldot upstream ownership belongs to the protocol runtime.",
},
{
name: "@dotli/resolver/rpc-chain",
message:
"RPC upstream ownership belongs to the protocol runtime.",
},
],
},
],
},
},
];
9 changes: 6 additions & 3 deletions apps/host/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ export interface ErrorDescription {
export function describeError(err: unknown, isP2p: boolean): ErrorDescription {
const msg = err instanceof Error ? err.message : String(err);

// Chunk-load failures want a reload prompt regardless of which side of the
// protocol boundary they surface on. The iframe reports them as `fatal`
// (its vite:preloadError relay), so this must run before the fatal branch.
if (msg.includes("Failed to fetch dynamically imported module")) {
return { message: HOST_ERRORS.MODULE_FETCH_FAILED, recovery: "reload" };
}
if (err instanceof ProtocolFatalError) {
return { message: HOST_ERRORS.FATAL_PANIC, recovery: "switch-backend" };
}
Expand All @@ -56,9 +62,6 @@ export function describeError(err: unknown, isP2p: boolean): ErrorDescription {
recovery: "switch-backend",
};
}
if (msg.includes("Failed to fetch dynamically imported module")) {
return { message: HOST_ERRORS.MODULE_FETCH_FAILED, recovery: "reload" };
}
if (
msg.includes("non-IPFS contenthash") ||
msg.includes("Failed to decode contenthash")
Expand Down
3 changes: 2 additions & 1 deletion apps/host/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1290,7 +1290,8 @@ async function main(): Promise<void> {
});

try {
const { statusToPhase } = await import("@dotli/resolver/resolve");
const { statusToPhase } =
await import("@dotli/resolver/access-raw-storage");
const onResolveProgress = (msg: string): void => {
// Progress events arrive as opaque strings across the iframe
// boundary. The resolver package owns the authoritative
Expand Down
192 changes: 161 additions & 31 deletions apps/protocol/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ import {
// these either. Smoldot for shared-worker mode lives inside
// `./protocol-shared-worker.ts`, which is already a separate bundle.
import {
createRpcChainProvider,
isRpcChainSupported,
createRpcUpstreamProvider,
isRpcUpstreamSupported,
} from "@dotli/resolver/rpc-chain";
import { log } from "@dotli/shared/log";
import { serializeError } from "@dotli/shared/errors";
Expand All @@ -93,6 +93,10 @@ import {
type ProtocolRequestEnvelope,
type ProtocolRequestMap,
} from "@dotli/protocol/messages";
import {
ChainConnectionError,
toProtocolErrorPayload,
} from "@dotli/protocol/errors";
import type { SWRelayRequest, SWOutbound } from "./protocol-shared-worker";

initSentry("host");
Expand Down Expand Up @@ -546,6 +550,75 @@ function signalError(message: string): void {

async function initSharedWorkerMode(network: Network): Promise<void> {
const swStartTime = performance.now();
const localPeopleConnectionIds = new Set<string>();
let localPeopleEnginePromise: Promise<ProtocolEngine> | null = null;

function getLocalPeopleEngine(): Promise<ProtocolEngine> {
localPeopleEnginePromise ??= Promise.all([
import("@dotli/resolver/chains"),
import("@dotli/resolver/smoldot"),
]).then(([chains, smoldot]) => {
// WebRTC is unavailable in SharedWorkerGlobalScope. Statement Store
// peers can be discovered through WebRTC even when chain sync itself
// succeeds over WSS, so People must keep smoldot's browser networking
// frontend in this protocol iframe. The broker and physical provider
// still remain inside the protocol runtime; only cross-tab sharing is
// unavailable for this chain.
const peopleGenesis =
getActiveServicesConfig().people.genesis.toLowerCase();
const engine = createEngine({
createUpstreamProvider: (genesisHash) =>
genesisHash.toLowerCase() === peopleGenesis
? chains.createSmoldotUpstreamProvider(genesisHash)
: null,
isChainSupported: (genesisHash) =>
genesisHash.toLowerCase() === peopleGenesis,
onInit: () => {
smoldot.getSmoldot();
},
onCleanup: () => {
smoldot.terminateSmoldot();
},
});
smoldot.onSmoldotFatal((message) => {
log.error(
"[dot.li protocol] Local People smoldot panic detected, signaling fatal",
);
if (window.parent !== window) {
window.parent.postMessage(
{
namespace: "dotli:protocol",
kind: "fatal",
message,
},
"*",
);
}
});
return engine;
});
return localPeopleEnginePromise;
}

function isLocalPeopleRequest(request: ProtocolRequestEnvelope): boolean {
if (request.method === "chainConnect") {
const payload = request.payload as ProtocolRequestMap["chainConnect"];
return (
typeof payload.genesisHash === "string" &&
payload.genesisHash.toLowerCase() ===
getActiveServicesConfig().people.genesis.toLowerCase()
);
}
if (request.method === "chainSend") {
const payload = request.payload as ProtocolRequestMap["chainSend"];
return localPeopleConnectionIds.has(payload.connectionId);
}
if (request.method === "chainDisconnect") {
const payload = request.payload as ProtocolRequestMap["chainDisconnect"];
return localPeopleConnectionIds.has(payload.connectionId);
}
return false;
}

// Vite statically rewrites `new SharedWorker(new URL("./worker.ts",
// import.meta.url), ...)` to point at the bundled chunk. The `new URL`
Expand Down Expand Up @@ -626,12 +699,49 @@ async function initSharedWorkerMode(network: Network): Promise<void> {
return;
}

const msg: SWRelayRequest = {
if (isLocalPeopleRequest(data)) {
const payload = data.payload as { connectionId?: unknown };
const connectionId =
typeof payload.connectionId === "string" ? payload.connectionId : null;
if (data.method === "chainConnect" && connectionId !== null) {
localPeopleConnectionIds.add(connectionId);
}
void getLocalPeopleEngine()
.then((engine) =>
engine.handleRequest(data, event.origin, (response) => {
postToSource(event.source, event.origin, response);
}),
)
.then(() => {
if (data.method === "chainDisconnect" && connectionId !== null) {
localPeopleConnectionIds.delete(connectionId);
}
})
.catch((error: unknown) => {
if (
connectionId !== null &&
(data.method === "chainConnect" ||
data.method === "chainDisconnect")
) {
localPeopleConnectionIds.delete(connectionId);
}
log.error("[dot.li protocol] Local People request failed:", error);
postToSource(event.source, event.origin, {
namespace: "dotli:protocol",
kind: "response",
id: data.id,
ok: false,
...toProtocolErrorPayload(error),
});
});
return;
}

port.postMessage({
type: "relay-request",
envelope: data,
origin: event.origin,
};
port.postMessage(msg);
} satisfies SWRelayRequest);
});

// Relay SharedWorker responses back up to the parent.
Expand All @@ -655,6 +765,9 @@ async function initSharedWorkerMode(network: Network): Promise<void> {
/* port already closed on unload, safe */
}
port.close();
void localPeopleEnginePromise?.then((engine) => {
engine.cleanup();
});
});
}

Expand All @@ -666,12 +779,15 @@ async function initDirectMode(): Promise<void> {

// Dynamic imports so users in `rpc` or `shared-worker` submode don't pay
// the smoldot / chain-specs bundle cost (D-1).
const [{ createChainProvider, isChainSupported }, resolve, smoldotMod] =
await Promise.all([
import("@dotli/resolver/chains"),
import("@dotli/resolver/resolve"),
import("@dotli/resolver/smoldot"),
]);
const [
{ createSmoldotUpstreamProvider, isChainSupported },
resolve,
smoldotMod,
] = await Promise.all([
import("@dotli/resolver/chains"),
import("@dotli/resolver/resolve"),
import("@dotli/resolver/smoldot"),
]);
const {
getRelayChain,
getSmoldot,
Expand Down Expand Up @@ -703,7 +819,7 @@ async function initDirectMode(): Promise<void> {
});

const engine = createEngine({
createChainProvider,
createUpstreamProvider: createSmoldotUpstreamProvider,
isChainSupported,
onBrokerReady: (broker) => {
// Route the resolver's Asset Hub reads AND the People warm-keep through
Expand Down Expand Up @@ -770,8 +886,8 @@ function initRpcMode(): void {
);

const engine = createEngine({
createChainProvider: createRpcChainProvider,
isChainSupported: isRpcChainSupported,
createUpstreamProvider: createRpcUpstreamProvider,
isChainSupported: isRpcUpstreamSupported,
// No onInit / onCleanup: the WS provider lifecycle is owned by the
// broker's `ensureUpstream` / `disconnectAll`.
// No resolver: gateway-mode resolution doesn't go through this iframe.
Expand Down Expand Up @@ -815,7 +931,7 @@ function bindEngineToMessages(engine: ProtocolEngine): void {
kind: "response",
id: data.id,
ok: false,
error: serializeError(error),
...toProtocolErrorPayload(error),
});
});
});
Expand Down Expand Up @@ -1037,7 +1153,7 @@ interface ProtocolEngine {

interface EngineOptions {
/** Factory for a `JsonRpcProvider` keyed by genesis hash. */
createChainProvider: (genesisHash: string) => JsonRpcProvider | null;
createUpstreamProvider: (genesisHash: string) => JsonRpcProvider | null;
/** Whether the given genesis hash is handled by this engine. */
isChainSupported: (genesisHash: string) => boolean;
/** Called once at engine creation, e.g. to kick off smoldot pre-sync. */
Expand Down Expand Up @@ -1077,7 +1193,7 @@ function createEngine(options: EngineOptions): ProtocolEngine {
const MAX_CONNS = 10;
const connections = new Map<string, StringJsonRpcConnection>();
const originConns = new Map<string, Set<string>>();
const broker = createChainBrokerManager(options.createChainProvider);
const broker = createChainBrokerManager(options.createUpstreamProvider);
options.onBrokerReady?.(broker);
options.onInit?.();

Expand Down Expand Up @@ -1225,22 +1341,36 @@ function createEngine(options: EngineOptions): ProtocolEngine {
);
}
if (!options.isChainSupported(payload.genesisHash)) {
throw new Error(`Unsupported chain: ${payload.genesisHash}`);
throw new ChainConnectionError(
"UNSUPPORTED_CHAIN",
`Unsupported chain: ${payload.genesisHash}`,
);
}
let connection: StringJsonRpcConnection | null;
try {
connection = broker.connectRemote(
payload.genesisHash,
payload.connectionId,
(message) => {
respond({
namespace: "dotli:protocol",
kind: "chain-message",
connectionId: payload.connectionId,
message,
});
},
);
} catch (error: unknown) {
throw new ChainConnectionError(
"UPSTREAM_CONNECTION_FAILED",
serializeError(error),
);
}
const connection = broker.connectRemote(
payload.genesisHash,
payload.connectionId,
(message) => {
respond({
namespace: "dotli:protocol",
kind: "chain-message",
connectionId: payload.connectionId,
message,
});
},
);
if (connection === null) {
throw new Error("Failed to create chain broker");
throw new ChainConnectionError(
"UPSTREAM_CONNECTION_FAILED",
"Failed to create chain broker",
);
}
connections.set(payload.connectionId, connection);
oc.add(payload.connectionId);
Expand Down
Loading
Loading