From ceb3469833e067ccfd79edbdd01b83c699d4c1b6 Mon Sep 17 00:00:00 2001 From: Michael Taylor Date: Sat, 4 Jul 2026 17:36:56 -0700 Subject: [PATCH 1/2] =?UTF-8?q?refactor(dig-browser):=20SoC=20re-arch=20?= =?UTF-8?q?=E2=80=94=20pure=20external-RPC=20consumer,=20drop=20cache=20UI?= =?UTF-8?q?,=20custom-node=20setting=20(#44)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per the #41 separation-of-concerns ruling (SYSTEM.md "Roles — serving vs consuming"), make DIG Browser a PURE RPC CONSUMER of an external dig-node. It keeps client-side verify+decrypt (the trustless tier that powers Shields) but owns no node functionality. Item 1 — remove the in-process node (consumer read path): - dig/node/dig_source_resolution.mjs: drop the `in-process` terminal; the §5.3 ladder now terminates at the public gateway rpc.dig.net. Tests updated (16/16). - windows-dig-browser-ux.patch: the loader (dig_url_loader_factory.cc) no longer calls the runtime node RPC (dig_rpc/CallDigRpc); its terminal is an off-machine POST to rpc.dig.net (PostToRemote + a gateway traffic annotation). - dig_native_runtime.{h,cc}: drop the dig_rpc/CallDigRpc FFI; the runtime DLL is loaded only for the in-browser WALLET (CallWalletRpc + StartNativeRuntime). NOTE: fully dropping the node from the runtime is a companion dig-node change (dig_runtime_start → wallet-only); until then rpc.dig.net is the guaranteed fallback and the dig-installer ships a standalone local dig-node (#40). Item 2 — single-source read-crypto (PENDING, build-gated): the native net::dig C++ crypto stays for now; the SoC target is to route verify+decrypt through the shared digstore dig_client wasm. That needs a V8/wasm context + a Chromium build to wire and verify — designed + flagged in SPEC §10 and the loader header. Item 3 — remove chrome://settings/dig cache config + DigCacheHandler Mojo + the "My Node" cache card: - windows-dig-settings-section.patch rewritten: no Mojo/cache controls. - dig/node/dig_node.html: cache card + control.cache.* calls removed (+ regression test). - dig/node/dig_node_controller.mjs: cache.* dropped from CONTROL_METHODS (9 methods). Item 4 — custom-node setting: chrome://settings/dig now hosts a persisted custom node endpoint pref (dig.custom_node_url) bound via cr-input; the §5.3 override (custom endpoint wins entirely) is implemented in dig_source_resolution.mjs and mirrored in the C++ loader (NormalizeCustomEndpoint + read via UserPrefs). Docs: SPEC.md §10 transition status; README + USER_JOURNEY updated to the pure-consumer model; obsolete dig/plans/chrome-settings-dig-section.md removed. Build-gated: this is a Chromium fork; the native C++/patch changes cannot be compiled here. Do NOT merge until built + verified on C:\d (see the PR body). --- README.md | 64 ++- SPEC.md | 36 +- USER_JOURNEY.md | 55 +- dig/node/dig_deploy_flow.mjs | 18 +- dig/node/dig_node.html | 52 +- dig/node/dig_node.test.mjs | 11 +- dig/node/dig_node_controller.mjs | 9 +- dig/node/dig_node_controller.test.mjs | 12 +- dig/node/dig_source_resolution.mjs | 130 +++-- dig/node/dig_source_resolution.test.mjs | 101 +++- dig/plans/chrome-settings-dig-section.md | 29 - .../windows/windows-dig-browser-ux.patch | 326 ++++++++--- .../windows-dig-settings-section.patch | 532 ++++-------------- .../windows/windows-dig-wallet-bridge.patch | 6 +- 14 files changed, 659 insertions(+), 722 deletions(-) delete mode 100644 dig/plans/chrome-settings-dig-section.md diff --git a/README.md b/README.md index 54979e7c..7df605cf 100644 --- a/README.md +++ b/README.md @@ -43,16 +43,20 @@ chia:///index.html?salt= # private store ### How `chia://` → `rpc.dig.net` works (the native read path) -The native handler (`net/url_request/dig_protocol_handler.cc`) mirrors the -reference extension exactly, but in C++ with BoringSSL instead of the WASM -crypto module: +The native handler mirrors the reference extension exactly, currently in C++ +with BoringSSL (`net/url_request/dig_crypto.*`). **Planned (#44 item 2, build- +gated):** route verify+decrypt through the shared `digstore` `dig_client` **wasm** +— the byte-identical artifact the extension/hub/SDK use — so there is exactly ONE +trustless read-crypto implementation (client-side decrypt stays; only the +duplicate C++ copy is retired). The pipeline: 1. **Parse the URN** (`dig_urn.cc`) — full form or shorthand, plus `?salt=`. 2. **Retrieval key** — `retrieval_key = SHA-256(canonical rootless URN)` where the canonical rootless URN is `urn:dig:chia:/` (`dig_crypto.cc`, SYSTEM.md "Retrieval key"). The URN itself is never sent. -3. **Fetch** — `POST` JSON-RPC 2.0 `dig.getContent` to the configured DIG RPC - endpoint (default `https://rpc.dig.net/`), streaming 3 MiB windows and +3. **Fetch** — `POST` JSON-RPC 2.0 `dig.getContent` to an **external** dig-node + resolved by the §5.3 ladder (custom endpoint > `dig.local` > `localhost:8080` + > `rpc.dig.net`; see "source resolution" below), streaming windows and reassembling `{ ciphertext, chunk_lens, inclusion_proof, total_length, complete, next_offset }` (SYSTEM.md "JSON-RPC 2.0 read methods"). 4. **Verify** — recompute the leaf `SHA-256(ciphertext)` and fold the Merkle @@ -73,11 +77,14 @@ error page — content is never shown unless it verifies *and* decrypts. ### Where `chia://` content is read from (source resolution) -The DIG Browser is a **consumer** in the DIG serve/consume split (`SYSTEM.md` → -"Roles — serving vs consuming"). For every `chia://` read it picks a source **in -order**: +The DIG Browser is a **pure RPC consumer** in the DIG serve/consume split +(`SYSTEM.md` → "Roles — serving vs consuming"): it runs **no in-process node**. +For every `chia://` read it resolves an **external** dig-node **in order** (§5.3): -1. **A local standalone dig-node**, if one is reachable — preferred, because it +1. **A custom endpoint**, if you set one in `chrome://settings/dig` — it + **overrides** the whole ladder and is used exclusively (your deliberate + choice, no silent fallback). +2. **A local standalone dig-node**, if one is reachable — preferred, because it is local/offline-capable and contributes to the network. It is addressed `http://dig.local` **first** (the `dig-installer` maps that name to the node's privileged `:80` loopback listener), then `http://localhost:8080` (the @@ -85,17 +92,21 @@ order**: liveness probe (confirming `status:"ok"` + `mode:"local-node"`) and **memoizes the verdict for ~5s**, so a page's many subresources never each re-probe a down node and a single failed probe never stalls a load. -2. **The browser's own in-process dig-node** otherwise — which itself reaches - `rpc.dig.net` when it has no cached capsule. - -Either way the served bytes are **always** verified against the on-chain root -and decrypted on your device — the source is never trusted (fail-closed). So the -browser is **fully functional standalone** (no local node needed), and when a -local dig-node *is* present it consumes from it and they share one `.dig` cache. - -- **Disable** consuming from a local node (in-process only) with the +3. **The public gateway `https://rpc.dig.net`** otherwise — the final fallback, + so a standalone browser with no local node still resolves every request. + +Whichever source serves the bytes, they are **always** verified against the +on-chain root and decrypted on your device — the source is never trusted +(fail-closed). So the browser is **fully functional standalone** (no local node +needed); when a local dig-node *is* present it consumes from it. + +- **Set a custom node** (or clear it, back to automatic) in **Settings → DIG + Network** (`chrome://settings/dig`); it persists in the `dig.custom_node_url` + pref and takes precedence over the auto-ladder. +- **Skip the local node** (go straight to `rpc.dig.net`) with the `--disable-local-dig-node` command-line switch. -- The pure resolution policy (ordering, port, host, probe path, TTL) lives in +- The pure resolution policy (override precedence, ordering, port, host, probe + path, TTL) lives in [`dig/node/dig_source_resolution.mjs`](dig/node/dig_source_resolution.mjs) with a Node test harness; the native loader mirrors it in C++. @@ -103,16 +114,17 @@ local dig-node *is* present it consumes from it and they share one `.dig` cache. The DIG Browser is also your node's **controller**. When a local standalone **dig-node** is running, open **`dig://node`** ("My Node") to manage it: see its -status, the **stores it hosts** (pin / unpin), the **cache** (view / clear / set -cap), **§21 sync** status, and the **upstream** it fetches from. It drives the -node's `control.*` admin RPCs over loopback only. +status, the **stores it hosts** (pin / unpin), **§21 sync** status, and the +**upstream** it fetches from. It drives the node's `control.*` admin RPCs over +loopback only. (The browser is **not** the node's cache-config UI — cache +management lives on the node itself, #44 SoC.) - **Hidden when you have no local node.** Consumption never needs one, so with no node present the page shows a **calm, dismissible nudge** instead of an empty - controller: it says the browser already works fully on its own (in-process node - + `rpc.dig.net`, nothing to install to browse), then invites installing a - **standalone dig-node** to run a full node, contribute to the network, share - your `.dig` cache, and unlock this My Node controller. The **Install dig-node** + controller: it says the browser already works fully on its own (it reads from + the public gateway `rpc.dig.net`, nothing to install to browse), then invites + installing a **standalone dig-node** to run a full node, contribute to the + network, and unlock this My Node controller. The **Install dig-node** link points at the [`dig-installer` releases](https://github.com/DIG-Network/dig-installer/releases) (the same target the `dig-chrome-extension` uses). Never alarmist; dismissing it is remembered (localStorage), and "Check again" re-engages. diff --git a/SPEC.md b/SPEC.md index 8cab5e70..e36c5abf 100644 --- a/SPEC.md +++ b/SPEC.md @@ -155,19 +155,27 @@ configuration lives with the node. ## 10. Transition (#41/#44) -The fork is mid-migration to the pure-RPC-consumer contract above (super-repo #44, per the #41 -separation-of-concerns ruling). The remaining code deltas from the previous "browser runs an -in-process node" architecture are: - -- **Remove the in-process node** (the `dig-runtime` cdylib loaded at PostBrowserStart) and make the - terminal fallback of the §4 ladder `rpc.dig.net` instead of an in-process node. This ships gated on - the dig-installer reliably delivering a local dig-node (#40) AND a buildable release (#26). -- **Single-source the read-crypto** — drop the native C++ `net::dig` DigURLLoader read-crypto copy in - favor of the shared wasm (§5). +The fork is completing its migration to the pure-RPC-consumer contract above (super-repo #44, per the +#41 separation-of-concerns ruling). Status of the four code deltas from the previous "browser runs an +in-process node" architecture: + +- **Remove the in-process node** — LANDED on the consumer read path: `dig/node/dig_source_resolution.mjs` + and the native `dig_url_loader_factory.cc` no longer have an `in-process` terminal; the §4 ladder now + terminates at the public gateway `rpc.dig.net`, and the loader never calls the runtime's node RPC + (`dig_rpc`). The `dig-runtime` DLL is still loaded at PostBrowserStart for the in-browser WALLET only + (§6). FULLY removing the node process requires a companion `dig-node` change so + `dig_runtime_start()` starts only the wallet (not `dig_rpc`/P2P/cache); until then the ecosystem + relies on the dig-installer delivering a standalone local dig-node (#40) with `rpc.dig.net` as the + guaranteed fallback. Ships once verified on a buildable release (#26). +- **Single-source the read-crypto** — PENDING (build-gated). The native C++ `net::dig` read-crypto + (`net/url_request/dig_crypto.*`, `dig_urn.*`) is still the loader's verify+decrypt. The target (§5) + is to route verify+decrypt through the shared `digstore` `dig_client` wasm so there is exactly ONE + trustless read-crypto implementation. That move needs a V8/wasm execution context and a Chromium + build to wire and verify (design + steps tracked on super-issue #44); client-side decrypt STAYS. - **Remove the `chrome://settings/dig` cache section** + the `DigCacheHandler` Mojo + the "My Node" - cache card (§1). -- **Add the custom-node setting** (§4.3). + cache card (§1) — DONE. +- **Add the custom-node setting** (§4.3) — DONE (the `chrome://settings/dig` section now hosts the + persisted custom node endpoint pref, `dig.custom_node_url`). -Until the in-process node is removed, `dig/node/dig_source_resolution.mjs` still lists an `in-process` -terminal step; the target terminal step is `rpc.dig.net`. This section is removed once the migration -lands. +This section is removed once the read-crypto single-sourcing (item 2) lands and the full node removal +is build-verified. diff --git a/USER_JOURNEY.md b/USER_JOURNEY.md index 7aeabb83..6bdbaf4a 100644 --- a/USER_JOURNEY.md +++ b/USER_JOURNEY.md @@ -83,14 +83,17 @@ content, a web URL, or DuckDuckGo. Wallet → `dig://wallet`. Publish → DIGHUb ## 3. Opening content — `chia://` navigation + verification When the user opens a `chia://` address (from the omnibox, a link, or a typed -address), the native dig handler (`windows-add-dig-protocol.patch`) resolves the -resource end-to-end: fetch from `rpc.dig.net` (or the local cache), verify -against the on-chain root, decrypt on-device, then commit. The loader is -**fail-closed** — a `chia://` page only ever renders if it passed verification, -so a committed `chia://` page is verified by construction. - -Locally-cached pages load instantly and offline (the local cache is capped and -managed in settings — see §6). +address), the native dig handler resolves the resource end-to-end: fetch from an +**external** dig-node over the §5.3 ladder (custom endpoint > `dig.local` > +`localhost:8080` > `rpc.dig.net`), verify against the on-chain root, decrypt +on-device, then commit. The browser runs **no in-process node and keeps no +content cache** (#44 SoC). The loader is **fail-closed** — a `chia://` page only +ever renders if it passed verification, so a committed `chia://` page is verified +by construction. + +When a local dig-node is present, its cache makes pages load instantly and +offline — that cache lives with the node, not the browser (set a custom node, or +run a local one, in Settings — see §6). --- @@ -115,7 +118,7 @@ per-site control panel: telemetry), each shown "On". The copy is framed as a readout of browser-wide defaults, not toggles this panel can flip. -**Hand-off:** "Manage DIG settings & cache" → `dig://settings`; "Privacy & +**Hand-off:** "DIG settings" → `dig://settings`; "Privacy & security settings" → `dig://privacy`. --- @@ -151,15 +154,21 @@ in-process wallet. `chrome://settings` (which `dig://settings` rewrites to, at `/dig`) gains a dedicated **DIG Network** section (`windows-dig-settings-section.patch`) with its own left-nav -entry. It exposes the native local-cache controls via a Mojo handler -(`DigCacheHandler`): +entry. It exposes the browser's own pure-RPC-consumer setting — the **custom node +endpoint** (#44 SoC) — as a plain persisted pref (`dig.custom_node_url`), **no +Mojo, no cache controls**: - A plain-language intro (the DIG Network is a decentralized web where every page - is proven on-chain, secured by the **$DIG** token; visited pages are cached - on-device to load fast and offline). -- A **local cache limit** slider (1–50 GB) with a live usage readout — "Most - space DIG Browser may use to store sites from the DIG Network on this device." -- A **Clear cache** button (content re-warms from `rpc.dig.net` on next visit). + is proven on-chain, secured by the **$DIG** token; DIG Browser reads DIG + content from a DIG node and verifies it on your device). +- A **Custom DIG node** input — the address of the node DIG Browser reads content + from. When set it is used exclusively, overriding the automatic order (your + local node, then the public gateway `rpc.dig.net`); it accepts an `http(s)://` + URL or a bare host. +- A **Use automatic** button that clears the override (back to the auto-ladder). + +There is intentionally **no local-cache UI**: the browser owns no node and no +content cache; cache management lives on the node itself. --- @@ -167,10 +176,11 @@ entry. It exposes the native local-cache controls via a Mojo handler When a local **dig-node** is running, `dig://node` ("My Node", `dig/node/dig_node.html`) is its controller: status, hosted stores (pin/unpin), -cache, §21 sync, upstream — over the node's loopback `control.*` RPC, gated by a +§21 sync, upstream — over the node's loopback `control.*` RPC, gated by a control token the browser injects on this device (see the README "Run & manage -your node"). With no node present the page just explains that and links to -install one — consumption never needs a node. +your node"). It is **not** the node's cache-config UI (cache lives with the node, +#44 SoC). With no node present the page just explains that and links to install +one — consumption never needs a node. My Node also has a **Publish** panel — the browser-as-local-hub centrepiece. It puts a folder on the DIG Network **from this device**, signed by the in-process @@ -205,7 +215,7 @@ Hosted-only features (handles, discovery) are labeled "On DIGHUb ↗" cards. | DIG identity panel | `dig://shields` | `dig/shields/dig_shields.html` | Verification + privacy posture readout (capsule) | | DIG Wallet | `dig://wallet` | in-process DIG runtime | The built-in Chia wallet | | My Node + Publish | `dig://node` | `dig/node/dig_node.html` | Run/manage the local node + local launch & deploy | -| Settings → DIG | `dig://settings` (→ `/dig`) | `windows-dig-settings-section.patch` | Local-cache controls | +| Settings → DIG | `dig://settings` (→ `/dig`) | `windows-dig-settings-section.patch` | Custom node endpoint (`dig.custom_node_url`) | | Injected provider | `window.chia` | `dig/provider/dig_provider.js` | CHIP-0002 wallet bridge for pages | ## Ecosystem hand-offs @@ -221,7 +231,8 @@ Hosted-only features (handles, discovery) are labeled "On DIGHUb ↗" cards. footer link (the brand logo goes to `dig://home`, not the marketing site). - **TibetSwap / dexie / 9mm** — acquire **$DIG** to publish. TibetSwap is surfaced on the home Apps directory + footer. -- **rpc.dig.net** — the read path the native `chia://` handler fetches from - (then verifies on-chain + decrypts on-device); the local cache sits in front. +- **rpc.dig.net** — the public-gateway read path the native `chia://` handler + falls back to (then verifies on-chain + decrypts on-device); a local dig-node + (custom endpoint / `dig.local` / `localhost`) is preferred ahead of it. - **CHIP-0035 / on-chain root** — the source of truth the verified badge and the capsule (`storeId:rootHash`) are anchored to. diff --git a/dig/node/dig_deploy_flow.mjs b/dig/node/dig_deploy_flow.mjs index 507b8bc9..57304f27 100644 --- a/dig/node/dig_deploy_flow.mjs +++ b/dig/node/dig_deploy_flow.mjs @@ -1,8 +1,9 @@ // DIG Browser "My Node" local PUBLISH / DEPLOY flow — the browser-as-local-hub -// centrepiece (#95 Pass D). This is the LOCAL, in-process equivalent of the -// digstore CLI `deploy` / the hub StagingView: launch a brand-new site (MINT a -// store) or publish an update (ADVANCE a store) entirely on this device, signed -// by the in-process DIG wallet — no hub spend service. +// centrepiece (#95 Pass D). This is the on-device equivalent of the digstore CLI +// `deploy` / the hub StagingView: launch a brand-new site (MINT a store) or +// publish an update (ADVANCE a store) via a LOCAL dig-node, signed by the +// in-process DIG wallet — no hub spend service. (The browser runs no content +// node of its own — #44 SoC; publishing drives a local standalone dig-node.) // // SERVE/CONSUME split (SYSTEM.md → "Roles — serving vs consuming"): publishing // is the SUPPLY side, so it runs through the node the browser controls. The flow @@ -10,10 +11,11 @@ // transport — additive policy only): // // 1. STAGE / COMPILE a folder → capsule `.module` ── `dig.stage` -// Served by the dig-node `handle_rpc` dispatch (dig-node crates/dig-node), -// which backs BOTH the in-process FFI (`dig::CallDigRpc`) AND a standalone -// dig-node's loopback JSON-RPC at POST "/". The My Node page already POSTs -// `control.*` to that loopback base, so it reaches `dig.stage` the same way. +// Served by the dig-node `handle_rpc` dispatch (dig-node crates/dig-node) +// over a LOCAL standalone dig-node's loopback JSON-RPC at POST "/". The My +// Node page already POSTs `control.*` to that loopback base, so it reaches +// `dig.stage` the same way. Publishing needs a local dig-node — the browser +// runs none of its own (#44 SoC: pure RPC consumer). // Request : { dir, store_id?, salt?, metadata? } // Result : { capsule, store_id, root, module_path, size, content_address, // files, ephemeral } diff --git a/dig/node/dig_node.html b/dig/node/dig_node.html index 715a41b4..ad0e8d7b 100644 --- a/dig/node/dig_node.html +++ b/dig/node/dig_node.html @@ -361,20 +361,9 @@

Hosted stores

- -
-

Cache

-

The on-disk store cache this node keeps (shared with the browser's - in-process node when both run).

-
-
- - - -
-
+
@@ -444,7 +433,6 @@

On DIGHUb

var CONTROL_METHODS = { status:'control.status', configGet:'control.config.get', configSetUpstream:'control.config.setUpstream', - cacheGet:'control.cache.get', cacheSetCap:'control.cache.setCap', cacheClear:'control.cache.clear', hostedStoresList:'control.hostedStores.list', hostedStoresPin:'control.hostedStores.pin', hostedStoresUnpin:'control.hostedStores.unpin', hostedStoresStatus:'control.hostedStores.status', syncStatus:'control.sync.status', syncTrigger:'control.sync.trigger' @@ -798,7 +786,7 @@

On DIGHUb

} function esc(s){ return String(s==null?'':s).replace(/[&<>]/g, function(c){ return {'&':'&','<':'<','>':'>'}[c]; }); } - function loadAll(){ loadStatus(); loadStores(); loadCache(); loadSync(); loadConfig(); } + function loadAll(){ loadStatus(); loadStores(); loadSync(); loadConfig(); } function loadStatus(){ call(CONTROL_METHODS.status).then(function(c){ @@ -871,20 +859,6 @@

On DIGHUb

if (prev) sel.value = prev; } - function loadCache(){ - call(CONTROL_METHODS.cacheGet).then(function(c){ - if (handleAuth(c)) return; - if (c.kind !== 'ok') { $('cacheStats').innerHTML = '
'+esc(c.message)+'
'; return; } - var r = c.result || {}; - $('cacheStats').innerHTML = - statCard('Used', fmtBytes(r.used_bytes)) + - statCard('Cap', fmtBytes(r.cap_bytes)) + - statCard('Shared', r.shared ? 'Yes' : 'No') + - statCard('Location', ''+esc(r.dir||'—')+''); - if (r.cap_bytes) $('capInput').value = Math.round(r.cap_bytes/1048576); - }); - } - function loadSync(){ call(CONTROL_METHODS.syncStatus).then(function(c){ if (handleAuth(c)) return; @@ -943,24 +917,6 @@

On DIGHUb

}); }); - $('capApply').addEventListener('click', function(){ - var mib = parseInt($('capInput').value, 10); - if (!(mib > 0)) return; - call(CONTROL_METHODS.cacheSetCap, {cap_bytes: mib*1048576}).then(function(c){ - if (handleAuth(c)) return; - if (c.kind === 'ok') { note('ok','Cache cap updated.'); loadCache(); } - else note(c.kind==='not-supported'?'warn':'bad', c.message); - }); - }); - - $('cacheClear').addEventListener('click', function(){ - call(CONTROL_METHODS.cacheClear).then(function(c){ - if (handleAuth(c)) return; - if (c.kind === 'ok') { note('ok','Cache cleared.'); loadCache(); loadStores(); } - else note(c.kind==='not-supported'?'warn':'bad', c.message); - }); - }); - $('upstreamApply').addEventListener('click', function(){ var v = $('upstreamInput').value.trim(); if (!v) return; call(CONTROL_METHODS.configSetUpstream, {upstream:v}).then(function(c){ diff --git a/dig/node/dig_node.test.mjs b/dig/node/dig_node.test.mjs index 15a0da23..92477576 100644 --- a/dig/node/dig_node.test.mjs +++ b/dig/node/dig_node.test.mjs @@ -139,13 +139,22 @@ test("page: every primary control/input carries a stable testid", () => { "node-token-input", "node-token-apply", "node-stats", "node-status-raw-toggle", "node-pin-input", "node-pin", "node-stores-refresh", "node-stores-list", - "node-cache-stats", "node-cap-input", "node-cap-apply", "node-cache-clear", "node-sync-stats", "node-upstream-input", "node-upstream-apply", "node-op-note", ]) { assert.match(html, new RegExp(`data-testid="${t}"`), `data-testid=${t}`); } }); +test("page: the node CACHE-config card is removed (#44 SoC — browser is not the cache UI)", () => { + // The browser does not surface node cache configuration. None of the cache + // controls, their testids, or the cache.* control calls remain in the page. + for (const t of ["node-cache-stats", "node-cap-input", "node-cap-apply", "node-cache-clear"]) { + assert.ok(!html.includes(`data-testid="${t}"`), `cache testid ${t} removed`); + } + assert.ok(!/control\.cache\./.test(html), "no control.cache.* calls in the page"); + assert.ok(!/aria-label="Local cache"/.test(html), "no 'Local cache' card"); +}); + test("page: ARIA landmarks + live regions exist", () => { assert.match(html, /\baria-label=/, "labelled sections"); assert.match(html, /\baria-live="polite"/, "live status region"); diff --git a/dig/node/dig_node_controller.mjs b/dig/node/dig_node_controller.mjs index 5b253869..15b3deb2 100644 --- a/dig/node/dig_node_controller.mjs +++ b/dig/node/dig_node_controller.mjs @@ -4,8 +4,10 @@ // // When a LOCAL standalone dig-node is present, the browser's "My Node" surface // (dig://node) drives the node's control.* admin RPCs — status, hosted stores -// (list/pin/unpin), cache (view/clear/setCap), §21 sync (status/trigger), and -// config (get/upstream). These control methods live BESIDE the node's read RPC, +// (list/pin/unpin), §21 sync (status/trigger), and config (get/upstream). The +// browser is NOT the node's cache-config UI (#44 SoC): cache management lives on +// the node's own control surface, so the browser surfaces no cache.* control. +// These control methods live BESIDE the node's read RPC, // are loopback-only, and are gated by a local control token: // - the node writes a 64-hex token to /control-token, // - every control.* call must carry it in the X-Dig-Control-Token header @@ -54,9 +56,6 @@ export const CONTROL_METHODS = Object.freeze({ status: "control.status", configGet: "control.config.get", configSetUpstream: "control.config.setUpstream", - cacheGet: "control.cache.get", - cacheSetCap: "control.cache.setCap", - cacheClear: "control.cache.clear", hostedStoresList: "control.hostedStores.list", hostedStoresPin: "control.hostedStores.pin", hostedStoresUnpin: "control.hostedStores.unpin", diff --git a/dig/node/dig_node_controller.test.mjs b/dig/node/dig_node_controller.test.mjs index 1bbff4f5..4ea08a52 100644 --- a/dig/node/dig_node_controller.test.mjs +++ b/dig/node/dig_node_controller.test.mjs @@ -47,14 +47,14 @@ test("control auth scheme matches the dig-node contract (header/param/file)", () assert.equal(CONTROL_TOKEN_FILE, "control-token"); }); -test("canonical control.* method names match the node surface (12 methods)", () => { +test("canonical control.* method names match the node surface (9 methods)", () => { + // No cache.* methods: the browser is not the node's cache-config UI (#44 SoC + // — cache management lives on the node's own control surface, not in the + // browser). assert.deepEqual(CONTROL_METHODS, { status: "control.status", configGet: "control.config.get", configSetUpstream: "control.config.setUpstream", - cacheGet: "control.cache.get", - cacheSetCap: "control.cache.setCap", - cacheClear: "control.cache.clear", hostedStoresList: "control.hostedStores.list", hostedStoresPin: "control.hostedStores.pin", hostedStoresUnpin: "control.hostedStores.unpin", @@ -62,6 +62,10 @@ test("canonical control.* method names match the node surface (12 methods)", () syncStatus: "control.sync.status", syncTrigger: "control.sync.trigger", }); + // the browser no longer surfaces node cache configuration at all. + assert.ok(!("cacheGet" in CONTROL_METHODS), "no cache.* control in the browser"); + assert.ok(!("cacheSetCap" in CONTROL_METHODS)); + assert.ok(!("cacheClear" in CONTROL_METHODS)); // every value is a control.*-prefixed string. for (const m of Object.values(CONTROL_METHODS)) { assert.ok(m.startsWith("control."), `${m} is a control.* method`); diff --git a/dig/node/dig_source_resolution.mjs b/dig/node/dig_source_resolution.mjs index 85f58427..8ad87dc5 100644 --- a/dig/node/dig_source_resolution.mjs +++ b/dig/node/dig_source_resolution.mjs @@ -1,26 +1,32 @@ // DIG Browser source-resolution policy (the CONSUMER side of the serve/consume // split, SYSTEM.md → "Roles — serving vs consuming"). // -// The browser's chia:// read path must, IN ORDER: -// 1. try a LOCAL standalone dig-node's read RPC if reachable — preferred -// because it is local/offline-capable and contributes to the network. It -// is addressed http://dig.local FIRST (the dig-installer maps it to the -// 127.0.0.2:80 loopback listener), then http://localhost: (default -// 8080 — the dig-node's always-on localhost listener); -// 2. else fall back to the browser's OWN in-process dig-node (FFI), which -// itself reaches rpc.dig.net when it has no cached capsule. +// Post-#44 the browser is a PURE RPC CONSUMER: it runs NO in-process node. Its +// chia:// read path resolves an EXTERNAL dig-node's read RPC, IN ORDER (§5.3): +// 1. an explicit user-configured CUSTOM endpoint if set — it overrides the +// whole ladder (the user deliberately chose it, so it is used exclusively); +// 2. else a LOCAL standalone dig-node if reachable — preferred because it is +// local/offline-capable and contributes to the network. Addressed +// http://dig.local FIRST (the dig-installer maps it to the 127.0.0.2:80 +// loopback listener), then http://localhost: (default 8080 — the +// dig-node's always-on localhost listener); +// 3. else the PUBLIC GATEWAY https://rpc.dig.net — the final fallback, so a +// standalone browser with no local node still resolves every request. // // The source is NEVER trusted: whichever node serves the bytes, the browser // ALWAYS verifies the Merkle inclusion proof against the on-chain root and -// decrypts client-side, fail-closed (that happens in the loader, not here). +// decrypts client-side, fail-closed (that happens in the loader via the shared +// dig_client read-crypto, not here). // // This module is the SINGLE SOURCE OF TRUTH for the *pure* resolution policy — -// the candidate ordering, the setting gate, and the short-TTL reachability -// memo. The native loader (chrome/browser/dig/dig_url_loader_factory.cc, added -// by windows-dig-browser-ux.patch) mirrors this exact logic in C++ (it cannot +// the candidate ordering, the setting gate, the custom-endpoint override, and +// the short-TTL reachability memo. The native loader +// (chrome/browser/dig/dig_url_loader_factory.cc, added by +// windows-dig-browser-ux.patch) mirrors this exact logic in C++ (it cannot // import JS). Keeping the policy here lets it be unit-tested with no Chromium // build; the C++ side carries a pointer back to this file. Any change to the -// ordering / port / host / probe-path / TTL must be made in BOTH places. +// ordering / port / host / probe-path / TTL / override precedence must be made +// in BOTH places. // // Run: node dig/node/dig_source_resolution.test.mjs (Node >= 18) @@ -30,6 +36,13 @@ export const DIG_LOCAL_HOST = "dig.local"; export const DEFAULT_LOCAL_PORT = 8080; +// The public gateway — the FINAL fallback of the ladder (the safety net when no +// local node is reachable). NEVER the primary endpoint (§5.3): a local node is +// always preferred. Browsers reach it over the plain-HTTPS public read tier (a +// browser cannot present a DIG-identity client cert, so it does not use the +// node-class mTLS path). +export const RPC_DIG_NET = "https://rpc.dig.net"; + // The dig-node serves the JSON-RPC read methods at POST "/" and a cheap liveness // probe at GET "/health" (server.rs). We probe /health, never a content method, // so a failed probe is fast and side-effect free. @@ -42,18 +55,20 @@ export const RPC_PATH = "/"; // a down node (which would stall every load). Milliseconds. export const PROBE_TTL_MS = 5000; -// Where the browser-served pages reach the source resolver verdict from. Stable +// Where the browser-served pages read the source resolver verdict from. Stable // names so an agent / the controller UI can read the active source posture. export const SOURCE_LOCAL_NODE = "local-node"; // a standalone dig-node served it -export const SOURCE_IN_PROCESS = "in-process"; // the browser's own node served it +export const SOURCE_PUBLIC_GATEWAY = "public-gateway"; // rpc.dig.net served it +export const SOURCE_CUSTOM = "custom-endpoint"; // the user's custom node served it /** * Build the ordered list of local standalone dig-node base URLs to try. * * @param {object} [opts] * @param {boolean} [opts.preferLocalNode=true] the user setting; when false the - * browser skips the standalone node entirely and consumes via its in-process - * node only (always fully functional — a consumer needs no local dig-node). + * browser skips the standalone node entirely and consumes straight from the + * public gateway (rpc.dig.net) — always fully functional, a consumer needs no + * local node. * @param {number} [opts.port=DEFAULT_LOCAL_PORT] the localhost listener port. * @returns {string[]} base URLs WITHOUT a trailing slash, in preference order. * Empty when the setting disables the local node. @@ -69,6 +84,42 @@ export function localNodeCandidates(opts = {}) { return [`http://${DIG_LOCAL_HOST}`, `http://localhost:${port}`]; } +/** + * Normalize a user-entered custom node endpoint into a base URL, or null if it + * is empty/malformed. This is the §5.3 "explicitly-configured node" — a + * discoverable, persisted setting that OVERRIDES the auto-ladder entirely. + * + * Accepts an http(s) URL, or a bare host[:port] (assumed http for the friendly + * local case). Rejects non-http(s) schemes. Strips any trailing slash(es) so the + * result composes with HEALTH_PATH / RPC_PATH exactly like the ladder candidates. + * + * @param {string} url the raw setting value. + * @returns {string|null} a base URL WITHOUT a trailing slash, or null. + */ +export function normalizeCustomEndpoint(url) { + if (typeof url !== "string") return null; + let s = url.trim(); + if (!s) return null; + // A bare host (no scheme) is a friendly convenience → assume http. + if (!/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(s)) { + s = `http://${s}`; + } + let parsed; + try { + parsed = new URL(s); + } catch (_) { + return null; + } + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { + return null; + } + if (!parsed.host) return null; + // Reconstruct base (scheme + host[:port]) + any path, minus trailing slashes. + let base = `${parsed.protocol}//${parsed.host}${parsed.pathname}`; + base = base.replace(/\/+$/, ""); + return base; +} + /** * Is a dig-node /health response body a live dig-node? The probe must confirm * it is actually a dig-node (not some other service squatting the port), so we @@ -127,30 +178,41 @@ export class ReachabilityMemo { } /** - * Decide which source to read a chia:// request from, honoring the setting and - * the reachability memo. This is the ordering decision ONLY — it returns the - * chosen plan; the caller performs the actual fetch + (always) the client-side - * verify/decrypt. + * Decide which source to read a chia:// request from, honoring the custom + * endpoint override, the setting, and the reachability memo. This is the + * ordering decision ONLY — it returns the chosen plan; the caller performs the + * actual fetch + (always) the client-side verify/decrypt. * - * Resolution order: - * 1. each local-node candidate that the memo says is reachable (fresh) — in - * order. Candidates with no fresh verdict are returned as "probe" steps so - * the caller probes /health, records the verdict, and continues. - * 2. the in-process node (always last, always available — never skipped). + * Resolution order (§5.3): + * 1. If `customEndpoint` is set → it is the SOLE step (kind 'override'). The + * auto-ladder is not consulted and the public gateway is NOT appended — + * the explicit choice wins entirely. + * 2. Else each local-node candidate that the memo says is reachable (fresh) — + * in order. Candidates with no fresh verdict are returned as 'probe' steps + * so the caller probes /health, records the verdict, and continues. + * 3. The public gateway (rpc.dig.net) is the terminal 'public-gateway' step — + * always appended (never skipped), so the plan always ends at a reachable + * source. * * @param {object} args + * @param {string} [args.customEndpoint] the normalized custom endpoint base URL + * (from {@link normalizeCustomEndpoint}), or falsy when unset. * @param {string[]} args.candidates from {@link localNodeCandidates}. * @param {ReachabilityMemo} args.memo * @param {number} args.now injected clock (ms). - * @returns {{plan: Array<{kind:'local'|'probe'|'in-process', baseUrl?:string}>}} + * @returns {{plan: Array<{kind:'override'|'local'|'probe'|'public-gateway', + * baseUrl:string}>}} * An ordered plan. The caller walks it: a 'probe' step means GET /health on - * baseUrl, and on success treat it as 'local'; 'in-process' is the terminal - * fallback (the browser's own node → rpc.dig.net). The plan ALWAYS ends with - * an 'in-process' step, so a standalone browser with no local node still - * resolves every request. + * baseUrl, and on success treat it as 'local'; 'override' and 'public-gateway' + * are POSTed to directly. The plan ends with 'public-gateway' (unless an + * override replaced the whole ladder). */ export function resolveSourcePlan(args) { - const { candidates, memo, now } = args; + const { customEndpoint, candidates, memo, now } = args; + // An explicit custom endpoint overrides the entire ladder (§5.3). + if (customEndpoint) { + return { plan: [{ kind: "override", baseUrl: customEndpoint }] }; + } const plan = []; for (const baseUrl of candidates || []) { const verdict = memo ? memo.get(baseUrl, now) : null; @@ -164,7 +226,7 @@ export function resolveSourcePlan(args) { plan.push({ kind: "probe", baseUrl }); } } - // The in-process node is the terminal, always-present fallback. - plan.push({ kind: "in-process" }); + // The public gateway is the terminal, always-present fallback. + plan.push({ kind: "public-gateway", baseUrl: RPC_DIG_NET }); return { plan }; } diff --git a/dig/node/dig_source_resolution.test.mjs b/dig/node/dig_source_resolution.test.mjs index 8614a9f3..0652788a 100644 --- a/dig/node/dig_source_resolution.test.mjs +++ b/dig/node/dig_source_resolution.test.mjs @@ -1,10 +1,16 @@ // Test harness for the source-resolution policy (dig/node/dig_source_resolution.mjs). // // A full Chromium build is infeasible in CI, so the *pure* resolution policy the -// native chia:// loader mirrors (candidate ordering, the setting gate, the -// short-TTL reachability memo, and the resolve plan) lives in a single JS module -// that this harness exercises directly. The C++ loader carries a pointer back to -// that module; these tests guard the contract both sides share. +// native chia:// loader mirrors (candidate ordering, the setting gate, the custom +// endpoint override, the short-TTL reachability memo, and the resolve plan) lives +// in a single JS module that this harness exercises directly. The C++ loader +// carries a pointer back to that module; these tests guard the contract both +// sides share. +// +// Post-#44 (separation-of-concerns re-arch): the browser is a PURE RPC CONSUMER. +// It owns no in-process node, so the ladder terminates at the PUBLIC GATEWAY +// (rpc.dig.net), not at an in-process node. §5.3 order: +// explicit custom endpoint > dig.local > localhost: > rpc.dig.net. // // Run: node dig/node/dig_source_resolution.test.mjs (Node >= 18) @@ -15,10 +21,13 @@ import { DEFAULT_LOCAL_PORT, HEALTH_PATH, RPC_PATH, + RPC_DIG_NET, PROBE_TTL_MS, SOURCE_LOCAL_NODE, - SOURCE_IN_PROCESS, + SOURCE_PUBLIC_GATEWAY, + SOURCE_CUSTOM, localNodeCandidates, + normalizeCustomEndpoint, isHealthyDigNode, ReachabilityMemo, resolveSourcePlan, @@ -38,13 +47,17 @@ test("a custom port is honored on the localhost candidate (dig.local stays portl test("the setting can DISABLE the local node entirely (consumer needs none)", () => { // preferLocalNode:false → no standalone-node candidates; the browser consumes - // via its in-process node only, still fully functional. + // straight from the public gateway (rpc.dig.net), still fully functional. assert.deepEqual(localNodeCandidates({ preferLocalNode: false }), []); // default (omitted) prefers the local node. assert.equal(localNodeCandidates().length, 2); assert.equal(localNodeCandidates({ preferLocalNode: true }).length, 2); }); +test("the terminal fallback is the PUBLIC GATEWAY (rpc.dig.net), not an in-process node", () => { + assert.equal(RPC_DIG_NET, "https://rpc.dig.net"); +}); + test("the probe paths are /health (liveness) and / (rpc), never a content method", () => { assert.equal(HEALTH_PATH, "/health"); assert.equal(RPC_PATH, "/"); @@ -57,13 +70,35 @@ test("isHealthyDigNode requires status:ok AND mode:local-node", () => { // a different service squatting the port is rejected. assert.equal(isHealthyDigNode({ status: "ok", mode: "something-else" }), false); assert.equal(isHealthyDigNode({ status: "degraded", mode: "local-node" }), false); - // malformed / empty → false (fail safe: fall through to the in-process node). + // malformed / empty → false (fail safe: fall through to the public gateway). assert.equal(isHealthyDigNode("not json"), false); assert.equal(isHealthyDigNode(null), false); assert.equal(isHealthyDigNode(undefined), false); assert.equal(isHealthyDigNode(42), false); }); +test("normalizeCustomEndpoint accepts http(s) URLs and strips the trailing slash", () => { + assert.equal(normalizeCustomEndpoint("http://dig.local"), "http://dig.local"); + assert.equal(normalizeCustomEndpoint("https://node.example.com"), "https://node.example.com"); + assert.equal(normalizeCustomEndpoint("http://localhost:9999/"), "http://localhost:9999"); + assert.equal(normalizeCustomEndpoint(" https://n.example/ "), "https://n.example"); + // a bare host with no scheme is assumed http (a friendly convenience). + assert.equal(normalizeCustomEndpoint("localhost:8080"), "http://localhost:8080"); + assert.equal(normalizeCustomEndpoint("my-node.lan"), "http://my-node.lan"); +}); + +test("normalizeCustomEndpoint rejects empty / malformed / non-http(s) inputs (→ null)", () => { + assert.equal(normalizeCustomEndpoint(""), null); + assert.equal(normalizeCustomEndpoint(" "), null); + assert.equal(normalizeCustomEndpoint(null), null); + assert.equal(normalizeCustomEndpoint(undefined), null); + assert.equal(normalizeCustomEndpoint(42), null); + // non-http(s) schemes are not valid node endpoints. + assert.equal(normalizeCustomEndpoint("ftp://x"), null); + assert.equal(normalizeCustomEndpoint("javascript:alert(1)"), null); + assert.equal(normalizeCustomEndpoint("file:///etc/passwd"), null); +}); + test("ReachabilityMemo caches a verdict for the TTL then goes stale", () => { const memo = new ReachabilityMemo(PROBE_TTL_MS); const url = "http://dig.local"; @@ -81,18 +116,19 @@ test("ReachabilityMemo caches a verdict for the TTL then goes stale", () => { assert.equal(memo.get(url, 5000 + PROBE_TTL_MS), null); }); -test("resolveSourcePlan: unknown candidates become probe steps, in-process is terminal", () => { +test("resolveSourcePlan: unknown candidates become probe steps, public gateway is terminal", () => { const candidates = localNodeCandidates(); const memo = new ReachabilityMemo(); const { plan } = resolveSourcePlan({ candidates, memo, now: 0 }); assert.deepEqual(plan, [ { kind: "probe", baseUrl: "http://dig.local" }, { kind: "probe", baseUrl: "http://localhost:8080" }, - { kind: "in-process" }, + { kind: "public-gateway", baseUrl: RPC_DIG_NET }, ]); - // The plan ALWAYS ends with in-process (standalone browser with no local node - // still resolves every request). - assert.equal(plan[plan.length - 1].kind, "in-process"); + // The plan ALWAYS ends with the public gateway (a standalone browser with no + // local node still resolves every request). + assert.equal(plan[plan.length - 1].kind, "public-gateway"); + assert.equal(plan[plan.length - 1].baseUrl, "https://rpc.dig.net"); }); test("resolveSourcePlan: a fresh-reachable candidate is used directly (no re-probe)", () => { @@ -109,21 +145,52 @@ test("resolveSourcePlan: a fresh-unreachable candidate is skipped without a prob const memo = new ReachabilityMemo(); memo.put("http://dig.local", false, 100); // known down, still fresh const { plan } = resolveSourcePlan({ candidates, memo, now: 100 }); - // dig.local is skipped; localhost has no verdict so it is a probe step. + // dig.local is skipped; localhost has no verdict so it is a probe step; the + // public gateway is the terminal. assert.deepEqual(plan, [ { kind: "probe", baseUrl: "http://localhost:8080" }, - { kind: "in-process" }, + { kind: "public-gateway", baseUrl: RPC_DIG_NET }, ]); }); -test("resolveSourcePlan: disabled setting → only the in-process node", () => { +test("resolveSourcePlan: disabled local node → straight to the public gateway", () => { const candidates = localNodeCandidates({ preferLocalNode: false }); const memo = new ReachabilityMemo(); const { plan } = resolveSourcePlan({ candidates, memo, now: 0 }); - assert.deepEqual(plan, [{ kind: "in-process" }]); + assert.deepEqual(plan, [{ kind: "public-gateway", baseUrl: RPC_DIG_NET }]); +}); + +test("resolveSourcePlan: an explicit custom endpoint OVERRIDES the ladder ENTIRELY (§5.3)", () => { + // When the user sets a custom node, it is the SOLE source — the auto-ladder + // (dig.local/localhost/rpc.dig.net) is not consulted, honoring the explicit + // choice (privacy/routing). No silent fallback to the public gateway. + const candidates = localNodeCandidates(); + const memo = new ReachabilityMemo(); + const { plan } = resolveSourcePlan({ + customEndpoint: "https://my-node.example.com", + candidates, + memo, + now: 0, + }); + assert.deepEqual(plan, [ + { kind: "override", baseUrl: "https://my-node.example.com" }, + ]); + // Crucially: the public gateway is NOT appended — the override wins entirely. + assert.equal(plan.length, 1); + assert.equal(plan.some((s) => s.kind === "public-gateway"), false); +}); + +test("resolveSourcePlan: an empty/invalid custom endpoint falls back to the auto-ladder", () => { + const candidates = localNodeCandidates(); + const memo = new ReachabilityMemo(); + // A falsy customEndpoint (setting cleared) → the normal ladder applies. + const { plan } = resolveSourcePlan({ customEndpoint: "", candidates, memo, now: 0 }); + assert.equal(plan[plan.length - 1].kind, "public-gateway"); + assert.equal(plan[0].kind, "probe"); }); test("stable source posture names for the controller/agent surface", () => { assert.equal(SOURCE_LOCAL_NODE, "local-node"); - assert.equal(SOURCE_IN_PROCESS, "in-process"); + assert.equal(SOURCE_PUBLIC_GATEWAY, "public-gateway"); + assert.equal(SOURCE_CUSTOM, "custom-endpoint"); }); diff --git a/dig/plans/chrome-settings-dig-section.md b/dig/plans/chrome-settings-dig-section.md deleted file mode 100644 index 1fe7f951..00000000 --- a/dig/plans/chrome-settings-dig-section.md +++ /dev/null @@ -1,29 +0,0 @@ -# #30 — chrome://settings DIG cache section (Mojo) — execution blueprint - -Backend DONE: dig-node `cache.getConfig`/`cache.setCapBytes`/`cache.clear` RPC (committed digstore 26710f9) + dig_runtime.dll rebuilt (target/release). Browser handler reaches them via `dig::CallDigRpc` FFI. - -Already created in C:\d (head start): -- `ui/webui/resources/cr_components/dig_cache/dig_cache.mojom` (DigCacheHandler: GetConfig/SetCapBytes/Clear — direct handler, no factory) -- `chrome/browser/resources/settings/dig_page/dig_page.html` - -## Remaining files (verified patterns from the 149 tree — clone customize_color_scheme_mode) - -1. **`ui/webui/resources/cr_components/dig_cache/BUILD.gn`** — `mojom("mojom")` (sources=[dig_cache.mojom], public_deps=[//mojo/public/mojom/base], webui_module_path="chrome://resources/cr_components/dig_cache/") + `build_webui("build")` shipping ONLY the mojom-webui.ts (ts_files=[], static_files=[], mojo_files=["$target_gen_dir/dig_cache.mojom-webui.ts"], mojo_files_deps=[":mojom_ts__generator"], generate_grdp=true, grd_prefix="cr_components_dig_cache", grd_resource_path_prefix=rebase_path(".","//ui/webui/resources")). Clone customize_color_scheme_mode/BUILD.gn:1-62. -2. **grdp aggregation** — register the generated `cr_components_dig_cache_resources.grdp` into the shared cr_components resources grd (find where customize_color_scheme_mode's grdp is ``-included — likely ui/webui/resources/BUILD.gn or a cr_components_resources.grd). UNRESOLVED — locate by grepping for `cr_components_customize_color_scheme_mode_resources.grdp`. -3. **`ui/webui/BUILD.gn`** static_library("webui") public_deps += `//ui/webui/resources/cr_components/dig_cache:mojom` (lines ~49-56, beside customize_color_scheme_mode:mojom). The load-bearing C++ binding edit. -4. **`chrome/browser/ui/webui/cr_components/dig_cache/dig_cache_handler.{h,cc}`** — ctor(PendingReceiver) + mojo::Receiver member; methods post `dig::CallDigRpc(json)` to base::MayBlock thread (cache.getConfig/setCapBytes/clear), parse JSON, run callback. Add sources to `chrome/browser/ui/BUILD.gn` static_library("ui") ~line 1352. -5. **`settings_ui.h/.cc`** — include dig_cache.mojom.h + handler.h; add `void BindInterface(PendingReceiver)` + `unique_ptr dig_cache_handler_` member; impl constructs the handler. (Direct, NOT factory — simpler than customize's factory; .Add() maps to BindInterface(PendingReceiver).) -6. **`chrome/browser/chrome_browser_interface_binders_webui_parts_desktop.cc`** ~661-671 — `.Add()` in the ForWebUI() chain + `#include ".../dig_cache.mojom.h"` (~line 139). -7. **`dig_page/dig_page.ts`** — Polymer element (plan skeleton); import `DigCacheHandler` from `chrome://resources/cr_components/dig_cache/dig_cache.mojom-webui.js` (ABSOLUTE path); `DigCacheHandler.getRemote()`. -8. **settings BUILD.gn** (`chrome/browser/resources/settings/BUILD.gn`) — web_component_files += "dig_page/dig_page.ts"; AND add a ts_dep on the dig_cache mojom .ts so the import resolves at compile (UNRESOLVED — how the settings bundle depends on a cr_component mojom; check how settings imports an existing cr_components mojom-webui.js, e.g. customize_color_scheme_mode, + mirror the ts_deps/extra dep). -9. **routing edits**: route.ts (r.DIG=r.BASIC.createSection('/dig','dig',...)), router.ts (SettingsRoutes: DIG), settings_menu/settings_menu.html (), settings_main/settings_main.html (
), page_visibility.ts (dig key), ensure_lazy_loaded.ts ('settings-dig-page'), lazy_load.ts (import './dig_page/dig_page.js'), icons.html (settings:dig icon). -10. **i18n**: settings_localized_strings_provider.cc AddDigStrings() {digPageTitle→IDS_SETTINGS_DIG, digCacheLimitLabel/Sublabel, digClearCacheButton} + call in dispatcher ~4265; chrome/app/settings_strings.grdp defs for each IDS_. - -## Build + verify -gn gen out\Default ; ninja -C out\Default chrome (settings WebUI bundle + mojom gen + C++). Expect 2-4 iterations (grdp aggregation, settings ts-dep, mojom path). Stage dig_runtime.dll. Verify chrome://settings/dig. - -## Sub-risks — RESOLVED (both wirings located) -- (2) grdp aggregation → `ui/webui/resources/BUILD.gn` includes the cr_components grdps (beside customize_color_scheme_mode); add dig_cache's there. -- (8) settings TS dep → `chrome/browser/resources/settings/BUILD.gn:462-467` ts_deps lists each cr_component's `:build_ts`; add `"//ui/webui/resources/cr_components/dig_cache:build_ts",` so the dig_cache.mojom-webui import resolves at settings TS-compile. - -All wiring points now identified — the build is fully specified + de-risked. ~16 files + a heavy settings-WebUI rebuild (gn gen + ninja chrome, ~15-20 min) + likely 1-2 iterations. Then stage dig_runtime.dll, verify chrome://settings/dig. diff --git a/patches/ungoogled-chromium/windows/windows-dig-browser-ux.patch b/patches/ungoogled-chromium/windows/windows-dig-browser-ux.patch index fa947ebb..e20ef98e 100644 --- a/patches/ungoogled-chromium/windows/windows-dig-browser-ux.patch +++ b/patches/ungoogled-chromium/windows/windows-dig-browser-ux.patch @@ -1,14 +1,16 @@ -# DIG Browser live-path + UX: route dig:// through the local dig-node, render as DIG. +# DIG Browser live-path + UX: route chia:// through an EXTERNAL dig-node, render as DIG. # -# This patch captures the browser changes that make dig:// load via the in-process -# DigURLLoaderFactory (the live navigation + subresource path) and that give the -# browser its DIG identity: +# This patch captures the browser changes that make chia:// load via the +# browser-process DigURLLoaderFactory (the live navigation + subresource path) and +# that give the browser its DIG identity. The browser is a PURE RPC CONSUMER +# (#44 separation-of-concerns): it runs NO in-process content node. # # * chrome/browser/dig/ — the DigURLLoaderFactory source_set: a browser-process -# URLLoaderFactory that serves dig:// by POSTing dig.getContent to the local -# dig-node sidecar (default http://127.0.0.1:9778/, DIG_RPC_ENDPOINT override), -# verifying the Merkle proof against the chain-anchored root (rootless URNs are -# resolved via the dig-resolver sidecar) and decrypting client-side; +# URLLoaderFactory that serves chia:// content by POSTing dig.getContent to an +# EXTERNAL dig-node over the §5.3 ladder (custom endpoint > dig.local > +# localhost:8080 > rpc.dig.net), verifying the Merkle proof against the +# chain-anchored root and decrypting client-side (fail-closed). It runs no +# in-process node and keeps no content cache; # * chrome_content_browser_client.cc — register that factory for BOTH DIG # schemes (dig:// internal pages + chia:// content) for navigations + subresources; # * theme_service.cc — seed the DIG palette (dark + brand violet #5800D6) on a @@ -25,8 +27,12 @@ # fetch/wasm-capable so those pages can fetch() subresources and instantiate wasm; # * chrome/browser/BUILD.gn — depend on //chrome/browser/dig; # * chrome_browser_main.cc — at PostBrowserStart, LoadLibrary("dig_runtime.dll") -# and call dig_runtime_start(): the DIG node runs NATIVE in-process (no -# dig-node.exe sidecar). build.py stages the cargo-built DLL beside dig.exe. +# and call dig_runtime_start() to bring up the in-browser WALLET host only +# (#44 SoC: no in-process content node — chia:// content resolves via an +# external dig-node, see dig_url_loader_factory.cc). build.py stages the +# cargo-built DLL beside dig.exe. (Fully removing the node from the runtime is +# a companion dig-node change: dig_runtime_start() should start ONLY the +# wallet, not dig_rpc / P2P / cache.) # * chrome_render_frame_observer.cc — at DidClearWindowObject, inject the # built-in Chia wallet provider (window.chia, CHIP-0002) into every http/ # https/dig page; the provider proxies to the in-process wallet over loopback @@ -307,10 +313,10 @@ +CLOSE --- a/chrome/browser/ui/browser_ui_prefs.cc +++ b/chrome/browser/ui/browser_ui_prefs.cc -@@ -116,9 +116,12 @@ +@@ -116,9 +116,20 @@ registry->RegisterIntegerPref(prefs::kSessionRestoreInfoBarTimesShown, 0); #endif // BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN) - + - registry->RegisterBooleanPref(prefs::kHomePageIsNewTabPage, true, + // DIG Browser: show the home button by default and make "home" the DIG + // Network site (the home button is the DIG-coin button in the top-left that @@ -320,6 +326,14 @@ - registry->RegisterBooleanPref(prefs::kShowHomeButton, false, + registry->RegisterBooleanPref(prefs::kShowHomeButton, true, pref_registration_flags); ++ // DIG Browser (#44): the user's optional custom DIG node endpoint. Empty = ++ // the automatic §5.3 ladder (dig.local > localhost > rpc.dig.net); non-empty ++ // = the exclusive override. Set in chrome://settings/dig (dig_page.ts), read ++ // by the chia:// loader (dig_url_loader_factory.cc). Keep the key ++ // "dig.custom_node_url" in sync across those sites and ++ // dig/node/dig_source_resolution.mjs. ++ registry->RegisterStringPref("dig.custom_node_url", std::string(), ++ pref_registration_flags); registry->RegisterBooleanPref(prefs::kSplitViewDragAndDropEnabled, true, pref_registration_flags); @@ -142,8 +145,9 @@ @@ -402,13 +416,16 @@ #include "base/task/current_thread.h" #include "base/task/single_thread_task_runner.h" #include "base/task/thread_pool.h" -@@ -1652,6 +1653,11 @@ +@@ -1652,6 +1653,14 @@ chrome_extra_part->PostBrowserStart(); } -+ // DIG: bring up the native in-process DIG runtime — the browser process IS the -+ // DIG node (dig_runtime.dll loaded in-process, called directly via FFI). No -+ // loopback server, no dig-node.exe sidecar. ++ // DIG (#44 SoC): bring up the in-browser WALLET host from dig_runtime.dll ++ // (loaded in-process, called via FFI for the window.chia wallet only). The ++ // browser runs NO in-process content node — chia:// content resolves via an ++ // EXTERNAL dig-node over the §5.3 ladder (dig_url_loader_factory.cc). Fully ++ // dropping the node from the runtime is a companion dig-node change ++ // (dig_runtime_start() → wallet-only). + dig::StartNativeRuntime(); + browser_process_->browser_policy_connector()->OnBrowserStarted(); @@ -762,7 +779,7 @@ +} // namespace dig --- /dev/null +++ b/chrome/browser/dig/BUILD.gn -@@ -0,0 +1,28 @@ +@@ -0,0 +1,30 @@ +# Copyright (c) 2026 The DIG Network Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. @@ -784,6 +801,8 @@ + "//content/public/browser", + "//mojo/public/cpp/bindings", + "//mojo/public/cpp/system", ++ "//components/prefs", ++ "//components/user_prefs", + "//components/version_info", + "//net", + "//services/network/public/cpp", @@ -793,7 +812,7 @@ +} --- /dev/null +++ b/chrome/browser/dig/dig_native_runtime.h -@@ -0,0 +1,38 @@ +@@ -0,0 +1,36 @@ +// Copyright (c) 2026 The DIG Network Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. @@ -803,23 +822,21 @@ + +#include + -+// The DIG browser process IS the DIG node. dig_runtime.dll (a cargo cdylib -+// shipped next to the browser executable) is loaded in-process and its -+// dig_rpc(request_json) -> response_json entrypoint is called DIRECTLY — there -+// is no loopback server and no dig-node.exe sidecar. ++// The in-browser WALLET host (#44 SoC). dig_runtime.dll (a cargo cdylib shipped ++// next to the browser executable) is loaded in-process to back the built-in ++// window.chia wallet (dig_wallet_rpc + the dig://wallet UI host). The browser ++// runs NO in-process CONTENT node — chia:// content resolves via an EXTERNAL ++// dig-node over the §5.3 ladder (dig_url_loader_factory.cc), so dig_rpc (the ++// node read RPC) is NOT called from the browser. (Fully dropping the node from ++// the runtime is a companion dig-node change: dig_runtime_start() should start ++// only the wallet.) +namespace dig { + -+// Load dig_runtime.dll (next to the browser executable) and start the native -+// in-process DIG runtime. Call once at browser startup, on the UI thread. -+// Best-effort: a no-op if the DLL is absent (dig:// simply can't serve yet). ++// Load dig_runtime.dll (next to the browser executable) and start the in-browser ++// wallet host. Call once at browser startup, on the UI thread. Best-effort: a ++// no-op if the DLL is absent (the built-in wallet simply isn't available). +void StartNativeRuntime(); + -+// Execute one DIG JSON-RPC request in-process (dig_rpc) and return the JSON -+// response, or an empty string on failure. BLOCKING — drives the request to -+// completion on the runtime's threads — so call ONLY from a thread allowed to -+// block (a base::MayBlock task), never the UI/IO thread. Thread-safe. -+std::string CallDigRpc(const std::string& request_json); -+ +// Execute one wallet (CHIP-0002 window.chia) request in-process (dig_wallet_rpc) +// on behalf of `origin` — the calling frame's UNSPOOFABLE committed origin, +// supplied by the browser process so the wallet's per-origin approval gate is @@ -834,7 +851,7 @@ +#endif // CHROME_BROWSER_DIG_DIG_NATIVE_RUNTIME_H_ --- /dev/null +++ b/chrome/browser/dig/dig_native_runtime.cc -@@ -0,0 +1,112 @@ +@@ -0,0 +1,96 @@ +// Copyright (c) 2026 The DIG Network Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. @@ -851,15 +868,16 @@ +namespace dig { +namespace { + -+// C-ABI exported by dig_runtime.dll (see crates/dig-runtime). ++// C-ABI exported by dig_runtime.dll (see crates/dig-runtime). The browser binds ++// only the wallet surface (#44 SoC): dig_runtime_start + dig_wallet_rpc + dig_free. ++// It does NOT bind dig_rpc — chia:// content resolves via an EXTERNAL dig-node, ++// not the in-process runtime (dig_url_loader_factory.cc). +using DigRuntimeStartFn = void (*)(); -+using DigRpcFn = char* (*)(const char*); +using DigWalletRpcFn = char* (*)(const char*, const char*); +using DigFreeFn = void (*)(char*); + +struct DigRuntimeLib { + DigRuntimeStartFn start = nullptr; -+ DigRpcFn rpc = nullptr; + DigWalletRpcFn wallet_rpc = nullptr; + DigFreeFn free_fn = nullptr; + bool attempted = false; @@ -898,8 +916,6 @@ + } + lib.start = reinterpret_cast( + base::GetFunctionPointerFromNativeLibrary(native, "dig_runtime_start")); -+ lib.rpc = reinterpret_cast( -+ base::GetFunctionPointerFromNativeLibrary(native, "dig_rpc")); + lib.wallet_rpc = reinterpret_cast( + base::GetFunctionPointerFromNativeLibrary(native, "dig_wallet_rpc")); + lib.free_fn = reinterpret_cast( @@ -915,21 +931,6 @@ + } +} + -+std::string CallDigRpc(const std::string& request_json) { -+ EnsureLoaded(); -+ DigRuntimeLib& lib = Lib(); -+ if (!lib.rpc || !lib.free_fn) { -+ return std::string(); -+ } -+ char* out = lib.rpc(request_json.c_str()); -+ if (!out) { -+ return std::string(); -+ } -+ std::string result(out); -+ lib.free_fn(out); -+ return result; -+} -+ +std::string CallWalletRpc(const std::string& origin, + const std::string& request_json) { + EnsureLoaded(); @@ -949,7 +950,7 @@ +} // namespace dig --- /dev/null +++ b/chrome/browser/dig/dig_url_loader_factory.h -@@ -0,0 +1,88 @@ +@@ -0,0 +1,108 @@ +// Copyright (c) 2026 The DIG Network Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. @@ -964,15 +965,17 @@ +// RegisterNonNetworkSubresourceURLLoaderFactories in +// chrome_content_browser_client.cc) so dig:// resolves natively, end-to-end. +// -+// It follows the SAME contract as the reference dig-chrome-extension and the -+// ported net::dig::* crypto core (see SYSTEM.md "JSON-RPC 2.0 read methods"): ++// It follows the SAME contract as the reference dig-chrome-extension (see ++// SYSTEM.md "JSON-RPC 2.0 read methods"): +// +// 1. Parse the URN (net::dig::ParseDigUrn). +// 2. retrieval_key = SHA-256(canonical rootless URN). -+// 3. POST JSON-RPC 2.0 `dig.getContent` to the configured DIG RPC endpoint -+// (https://rpc.dig.net/), streaming 3 MiB windows until complete, via a -+// fresh network::SimpleURLLoader per window over the browser-process -+// SharedURLLoaderFactory captured at Create() time. ++// 3. POST JSON-RPC 2.0 `dig.getContent` to an EXTERNAL dig-node over the §5.3 ++// ladder (custom endpoint > dig.local > localhost:8080 > rpc.dig.net; see ++// CallNode below), streaming windows until complete, via a fresh ++// network::SimpleURLLoader per window over the browser-process ++// SharedURLLoaderFactory captured at Create() time. The browser runs NO ++// in-process node (#44 SoC). +// 4. Verify the Merkle inclusion proof against the trusted root. +// 5. Derive the per-URN AES-256 key (HKDF) and decrypt AES-256-GCM-SIV. +// 6. Stream the verified + decrypted bytes to the URLLoaderClient with the @@ -980,10 +983,21 @@ +// +// Verification/decryption are FAIL-CLOSED: a decoy, tampered bytes, or wrong +// key surface a branded error page (served 200/text/html) and never content. ++// ++// #44 ITEM 2 (build-gated single-source-of-crypto): steps 1/4/5 currently use ++// the native net::dig::* C++ crypto (a third copy of the read-crypto). The SoC ++// target is to route verify+decrypt through the SHARED digstore `dig_client` ++// wasm (the byte-identical artifact the extension/hub/SDK use) so there is ONE ++// trustless read-crypto impl. That move needs a V8/wasm context and a Chromium ++// build to wire + verify — see the PR / super-issue #44 for the design and the ++// exact C:\d build steps. Client-side decrypt STAYS; only the duplicate C++ ++// copy is retired. + +#ifndef CHROME_BROWSER_DIG_DIG_URL_LOADER_FACTORY_H_ +#define CHROME_BROWSER_DIG_DIG_URL_LOADER_FACTORY_H_ + ++#include ++ +#include "base/memory/scoped_refptr.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "mojo/public/cpp/bindings/pending_remote.h" @@ -1017,6 +1031,7 @@ + private: + DigURLLoaderFactory( + scoped_refptr network_loader_factory, ++ std::string custom_endpoint, + mojo::PendingReceiver factory_receiver); + + ~DigURLLoaderFactory() override; @@ -1031,8 +1046,14 @@ + const net::MutableNetworkTrafficAnnotationTag& traffic_annotation) + override; + -+ // Browser-process factory used to POST dig.getContent to the RPC endpoint. ++ // Browser-process factory used to POST dig.getContent to the resolved external ++ // DIG node (custom endpoint > dig.local > localhost > rpc.dig.net). + scoped_refptr network_loader_factory_; ++ ++ // The user's custom node endpoint (§5.3 override), or empty for the automatic ++ // ladder. Read once from the dig.custom_node_url pref at Create(); passed to ++ // each DigURLLoader. ++ std::string custom_endpoint_; +}; + +} // namespace dig @@ -1040,7 +1061,7 @@ +#endif // CHROME_BROWSER_DIG_DIG_URL_LOADER_FACTORY_H_ --- /dev/null +++ b/chrome/browser/dig/dig_url_loader_factory.cc -@@ -0,0 +1,1315 @@ +@@ -0,0 +1,1442 @@ +// Copyright (c) 2026 The DIG Network Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. @@ -1052,7 +1073,6 @@ +#include "chrome/browser/dig/dig_node_html.inc" // kDigNodeHtml (My Node) +#include "chrome/browser/dig/dig_shields_html.inc" // kDigShieldsHtml +#include "chrome/browser/dig/dig_welcome_html.inc" // kDigWelcomeHtml -+#include "chrome/browser/dig/dig_native_runtime.h" +#include "components/version_info/version_info.h" + +#include @@ -1088,6 +1108,8 @@ +#include "base/strings/string_split.h" +#include "base/strings/string_util.h" +#include "base/values.h" ++#include "components/prefs/pref_service.h" ++#include "components/user_prefs/user_prefs.h" +#include "content/public/browser/browser_context.h" +#include "content/public/browser/storage_partition.h" +#include "mojo/public/cpp/bindings/receiver.h" @@ -1320,17 +1342,21 @@ + +// --------------------------------------------------------------------------- +// Source resolution (the CONSUMER side of the serve/consume split, SYSTEM.md → -+// "Roles — serving vs consuming"). The chia:// read path tries, IN ORDER: -+// 1. a LOCAL standalone dig-node's read RPC if reachable — preferred ++// "Roles — serving vs consuming"). The browser is a PURE RPC CONSUMER (#44): it ++// runs NO in-process node. The chia:// read path resolves an EXTERNAL dig-node, ++// IN ORDER (§5.3): ++// 1. an explicit user-configured CUSTOM endpoint if set — it OVERRIDES the ++// whole ladder (used exclusively; no fallback), honoring the user's choice; ++// 2. else a LOCAL standalone dig-node if reachable — preferred +// (local/offline/contributes), addressed http://dig.local FIRST then +// http://localhost: (default 8080); -+// 2. else the browser's OWN in-process dig-node (the FFI dig::CallDigRpc), -+// which itself reaches rpc.dig.net when it has no cached capsule. ++// 3. else the PUBLIC GATEWAY https://rpc.dig.net — the final fallback, so a ++// standalone browser with no local node still resolves every request. +// Whichever source serves the bytes, the loader ALWAYS verifies the Merkle +// proof against the on-chain root and decrypts client-side, fail-closed — the -+// source is NEVER trusted. The PURE policy (ordering/port/host/probe-path/TTL) -+// is mirrored from dig/node/dig_source_resolution.mjs (which carries the Node -+// tests for it); change BOTH in lockstep. ++// source is NEVER trusted. The PURE policy (ordering/port/host/probe-path/TTL/ ++// override precedence) is mirrored from dig/node/dig_source_resolution.mjs ++// (which carries the Node tests for it); change BOTH in lockstep. +// --------------------------------------------------------------------------- + +// dig-node's canonical local addresses, in preference order. dig.local has NO @@ -1338,6 +1364,10 @@ +// listener); the localhost listener uses the default 8080. +constexpr char kDigLocalBase[] = "http://dig.local"; +constexpr char kLocalhostBase[] = "http://localhost:8080"; ++// The public gateway — the FINAL fallback (the safety net when no local node is ++// reachable). NEVER the primary endpoint (§5.3). A browser reaches it over the ++// plain-HTTPS public read tier (no DIG-identity client cert). ++constexpr char kPublicGatewayBase[] = "https://rpc.dig.net"; +// The node serves JSON-RPC at POST "/" and a cheap liveness probe at GET +// "/health" (dig-companion server.rs). We probe /health, never a content +// method, so a failed probe is fast and side-effect free. @@ -1371,6 +1401,36 @@ + return {kDigLocalBase, kLocalhostBase}; +} + ++// Normalize a user-entered custom node endpoint (the dig.custom_node_url pref, ++// §5.3) into a base URL WITHOUT a trailing slash, or "" if empty/malformed. ++// MIRRORS normalizeCustomEndpoint() in dig/node/dig_source_resolution.mjs: ++// accept an http(s) URL or a bare host[:port] (assumed http); reject any other ++// scheme; strip trailing slashes so it composes with "/" and "/health" exactly ++// like the ladder candidates. Change BOTH in lockstep. ++std::string NormalizeCustomEndpoint(const std::string& raw) { ++ std::string s(base::TrimWhitespaceASCII(raw, base::TRIM_ALL)); ++ if (s.empty()) { ++ return std::string(); ++ } ++ // A bare host (no "scheme://") is a friendly convenience → assume http. ++ if (s.find("://") == std::string::npos) { ++ s = base::StrCat({"http://", s}); ++ } ++ GURL url(s); ++ if (!url.is_valid() || !url.SchemeIsHTTPOrHTTPS() || !url.has_host()) { ++ return std::string(); ++ } ++ // scheme://host[:port] + path, minus any trailing slash(es). ++ std::string base = base::StrCat( ++ {url.scheme(), "://", url.host(), ++ url.has_port() ? base::StrCat({":", url.port()}) : std::string(), ++ url.path()}); ++ while (!base.empty() && base.back() == '/') { ++ base.pop_back(); ++ } ++ return base; ++} ++ +// --------------------------------------------------------------------------- +// Local control token (the My Node controller surface, dig://node). The local +// standalone dig-node gates its control.* admin RPC with a loopback-only token @@ -1496,6 +1556,33 @@ + policy_exception_justification: "Loopback-only; no off-machine traffic." + })"); + ++// Traffic annotation for the terminal POST to an OFF-MACHINE DIG node — the ++// public gateway https://rpc.dig.net (the ladder's final fallback) or a ++// user-configured custom endpoint. Like the local case it carries no user ++// identity (credentials omitted) and moves only blind ciphertext + Merkle ++// proofs which are re-verified locally, but it DOES leave the machine. ++const net::NetworkTrafficAnnotationTag kDigGatewayTrafficAnnotation = ++ net::DefineNetworkTrafficAnnotation("dig_gateway_rpc", R"( ++ semantics { ++ sender: "DIG Browser chia:// loader" ++ description: ++ "Reads DIG content from an off-machine DIG node — the public gateway " ++ "rpc.dig.net (the fallback when no local node is present) or the " ++ "user-configured custom node. The response (blind ciphertext + a " ++ "Merkle inclusion proof) is verified against the on-chain root and " ++ "decrypted locally; the source is never trusted." ++ trigger: "Navigating to a chia:// address or loading a chia:// subresource " ++ "with no reachable local dig-node (or with a custom node set)." ++ data: "A JSON-RPC dig.getContent/dig.getAnchoredRoot request (store id, " ++ "root, retrieval key, offset/length). No user identity." ++ destination: WEBSITE ++ } ++ policy { ++ cookies_allowed: NO ++ setting: "Set a custom node, or run a local dig-node, in chrome://settings/dig." ++ policy_exception_justification: "Blind ciphertext + proof only; re-verified locally." ++ })"); ++ +// MIME from the resource key extension (mirrors the extension's ctForPath and +// net/url_request/dig_protocol_handler.cc MimeForResourceKey). +std::string MimeForResourceKey(const std::string& resource_key) { @@ -1594,13 +1681,15 @@ + public: + static void CreateAndStart( + scoped_refptr network_loader_factory, ++ std::string custom_endpoint, + const network::ResourceRequest& request, + mojo::PendingReceiver loader, + mojo::PendingRemote client) { + // Owns itself; lives until the client gives up (mojo disconnect) or the + // response (or branded error page) has been fully delivered. + auto* self = new DigURLLoader(std::move(network_loader_factory), -+ std::move(loader), std::move(client)); ++ std::move(custom_endpoint), std::move(loader), ++ std::move(client)); + self->Start(request.url); + } + @@ -1619,9 +1708,11 @@ + private: + DigURLLoader( + scoped_refptr network_loader_factory, ++ std::string custom_endpoint, + mojo::PendingReceiver loader, + mojo::PendingRemote client) -+ : network_loader_factory_(std::move(network_loader_factory)) { ++ : network_loader_factory_(std::move(network_loader_factory)), ++ custom_endpoint_(std::move(custom_endpoint)) { + receiver_.Bind(std::move(loader)); + receiver_.set_disconnect_handler(base::BindOnce( + &DigURLLoader::OnMojoDisconnect, base::Unretained(this))); @@ -1632,14 +1723,23 @@ + + // ---- Source resolution ------------------------------------------------- + // Run one DIG JSON-RPC request (`request_json`) against the BEST available -+ // source and deliver the response body to `on_response`. Resolution order -+ // (SYSTEM.md "Roles — serving vs consuming"): a reachable LOCAL standalone -+ // dig-node first (http://dig.local then http://localhost:8080), else the -+ // in-process node (FFI). The body is the node's JSON-RPC response either way; -+ // the caller always verifies + decrypts it client-side (the source is never -+ // trusted). `on_response` runs on this sequence. ++ // EXTERNAL source and deliver the response body to `on_response`. Resolution ++ // order (SYSTEM.md "Roles — serving vs consuming", §5.3): the custom endpoint ++ // if set (exclusive override), else a reachable LOCAL standalone dig-node ++ // (http://dig.local then http://localhost:8080), else the PUBLIC GATEWAY ++ // (rpc.dig.net). The browser runs NO in-process node (#44 SoC). The body is ++ // the node's JSON-RPC response either way; the caller always verifies + ++ // decrypts it client-side (the source is never trusted). `on_response` runs on ++ // this sequence. + using NodeResponseCallback = base::OnceCallback; + void CallNode(std::string request_json, NodeResponseCallback on_response) { ++ // §5.3: an explicit custom endpoint overrides the whole ladder — it is the ++ // sole source (no probe, no fallback), honoring the user's deliberate choice. ++ if (!custom_endpoint_.empty()) { ++ PostToRemote(custom_endpoint_, std::move(request_json), ++ std::move(on_response)); ++ return; ++ } + std::vector candidates = LocalNodeCandidates(); + // Drop candidates the memo says are freshly unreachable; keep the rest in + // order (a fresh-reachable one is POSTed directly, an unknown one is probed @@ -1664,12 +1764,12 @@ + std::string request_json, + NodeResponseCallback on_response) { + if (index >= plan.size()) { -+ // Terminal fallback: the browser's own in-process node. BLOCKING FFI, so -+ // run it on a MayBlock thread and reply on this sequence. -+ base::ThreadPool::PostTaskAndReplyWithResult( -+ FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_BLOCKING}, -+ base::BindOnce(&dig::CallDigRpc, std::move(request_json)), -+ std::move(on_response)); ++ // Terminal fallback: the PUBLIC GATEWAY (rpc.dig.net). The browser runs no ++ // in-process node (#44 SoC), so this off-machine POST is the final source ++ // (no further fallback). Its response is verified + decrypted client-side ++ // like every other source. ++ PostToRemote(kPublicGatewayBase, std::move(request_json), ++ std::move(on_response)); + return; + } + const std::string base_url = plan[index]; @@ -1760,6 +1860,37 @@ + std::move(on_response)); + } + ++ // POST the JSON-RPC request to an OFF-MACHINE DIG node (`base_url`): the public ++ // gateway (the ladder's terminal fallback) or the user's custom endpoint (the ++ // §5.3 override). No further fallback — an empty body surfaces as an error to ++ // the caller (which renders the branded error page). Uses the off-machine ++ // traffic annotation (this request leaves the device). ++ void PostToRemote(std::string base_url, ++ std::string request_json, ++ NodeResponseCallback on_response) { ++ auto resource_request = std::make_unique(); ++ resource_request->url = GURL(base_url + "/"); ++ resource_request->method = "POST"; ++ resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit; ++ resource_request->headers.SetHeader("Content-Type", "application/json"); ++ auto loader = network::SimpleURLLoader::Create( ++ std::move(resource_request), kDigGatewayTrafficAnnotation); ++ loader->AttachStringForUpload(request_json, "application/json"); ++ network::SimpleURLLoader* raw = loader.get(); ++ raw->DownloadToString( ++ network_loader_factory_.get(), ++ base::BindOnce(&DigURLLoader::OnRemoteResponse, ++ weak_factory_.GetWeakPtr(), std::move(loader), ++ std::move(on_response)), ++ kLocalNodeMaxResponse); ++ } ++ ++ void OnRemoteResponse(std::unique_ptr loader, ++ NodeResponseCallback on_response, ++ std::optional body) { ++ std::move(on_response).Run(body ? std::move(*body) : std::string()); ++ } ++ + // Serve dig://node with the local control token injected. `token` is the + // trimmed /control-token (empty when no node has been set up — the + // page then shows its "needs the control token" state). The placeholder is @@ -2281,11 +2412,17 @@ + mojo::Receiver receiver_{this}; + mojo::Remote client_; + -+ // Captured at construction. Used to POST/GET to a LOCAL standalone dig-node -+ // over loopback HTTP (CallNode → /health probe + dig.getContent POST) when one -+ // is the preferred source; the in-process node path uses the FFI instead. ++ // Captured at construction. Used to POST/GET to any EXTERNAL DIG node over ++ // HTTP: a LOCAL standalone dig-node over loopback (CallNode → /health probe + ++ // dig.getContent POST), the public gateway (rpc.dig.net), or the custom ++ // endpoint. The browser runs no in-process node (#44 SoC). + scoped_refptr network_loader_factory_; + ++ // The user's custom node endpoint (§5.3 override), or empty for the automatic ++ // ladder. Captured from the dig.custom_node_url pref by the factory and passed ++ // in at construction; when set it is the sole source (see CallNode). ++ std::string custom_endpoint_; ++ + net::dig::ParsedUrn parsed_; + std::string chain_; + std::string resource_key_; @@ -2328,9 +2465,18 @@ + browser_context->GetDefaultStoragePartition() + ->GetURLLoaderFactoryForBrowserProcess(); + ++ // Read the user's custom node endpoint (§5.3 override) from the profile pref, ++ // normalized like dig/node/dig_source_resolution.mjs. UserPrefs::Get avoids a ++ // //chrome/browser/profiles dependency. Empty = the automatic ladder. ++ std::string custom_endpoint; ++ if (PrefService* prefs = user_prefs::UserPrefs::Get(browser_context)) { ++ custom_endpoint = NormalizeCustomEndpoint(prefs->GetString("dig.custom_node_url")); ++ } ++ + // The DigURLLoaderFactory deletes itself when there are no more receivers — + // see network::SelfDeletingURLLoaderFactory::OnDisconnect. + new DigURLLoaderFactory(std::move(network_loader_factory), ++ std::move(custom_endpoint), + pending_remote.InitWithNewPipeAndPassReceiver()); + + return pending_remote; @@ -2338,9 +2484,11 @@ + +DigURLLoaderFactory::DigURLLoaderFactory( + scoped_refptr network_loader_factory, ++ std::string custom_endpoint, + mojo::PendingReceiver factory_receiver) + : network::SelfDeletingURLLoaderFactory(std::move(factory_receiver)), -+ network_loader_factory_(std::move(network_loader_factory)) {} ++ network_loader_factory_(std::move(network_loader_factory)), ++ custom_endpoint_(std::move(custom_endpoint)) {} + +DigURLLoaderFactory::~DigURLLoaderFactory() = default; + @@ -2351,8 +2499,8 @@ + const network::ResourceRequest& request, + mojo::PendingRemote client, + const net::MutableNetworkTrafficAnnotationTag& traffic_annotation) { -+ DigURLLoader::CreateAndStart(network_loader_factory_, request, -+ std::move(loader), std::move(client)); ++ DigURLLoader::CreateAndStart(network_loader_factory_, custom_endpoint_, ++ request, std::move(loader), std::move(client)); +} + +} // namespace dig diff --git a/patches/ungoogled-chromium/windows/windows-dig-settings-section.patch b/patches/ungoogled-chromium/windows/windows-dig-settings-section.patch index 98be1e50..947440de 100644 --- a/patches/ungoogled-chromium/windows/windows-dig-settings-section.patch +++ b/patches/ungoogled-chromium/windows/windows-dig-settings-section.patch @@ -1,232 +1,66 @@ -# DIG Browser: chrome://settings DIG section (native local-cache controls). +# DIG Browser: chrome://settings DIG section (custom node endpoint). # -# Adds a dedicated "DIG" section to chrome://settings (its own left-nav entry, -# route /dig, and page) exposing the native local-cache controls: a storage-cap -# slider, live usage readout, and a Clear-cache button. This supersedes the -# standalone dig-cache settings surface by placing the controls alongside the -# other Chromium settings, with their own section, per DIG UX guidelines. +# Adds a dedicated "DIG Network" section to chrome://settings (its own left-nav +# entry, route /dig, and page) exposing the browser's OWN pure-RPC-consumer +# setting: an optional CUSTOM DIG node endpoint that overrides the automatic +# node-resolution ladder (§5.3: custom endpoint > dig.local > localhost > +# rpc.dig.net). This is the browser's own consumer preference — WHICH external +# node it reads content from — NOT node configuration; the DIG Browser owns no +# node and surfaces no node/cache config chrome (#44 separation-of-concerns). # -# Architecture: -# * ui/webui/resources/cr_components/dig_cache/ — a Mojo interface -# (DigCacheHandler: GetConfig / SetCapBytes / Clear). webui_module_path="/" -# so the generated dig_cache.mojom-webui.ts ships + compiles through the -# settings bundle (mirroring autofill_ai_enums.mojom), avoiding a CSP -# connect-src change (the loopback dig-node RPC is unreachable from a -# chrome://settings fetch). -# * chrome/browser/ui/webui/cr_components/dig_cache/dig_cache_handler.* — the -# browser-process handler. Each method posts a MayBlock task that calls the -# in-process DIG runtime (dig::CallDigRpc) with a cache.* JSON-RPC and maps -# the result back over Mojo. Bound on SettingsUI via the WebUI interface -# binder. +# Architecture (deliberately simple — no Mojo): +# * The setting is a single persisted string pref, dig.custom_node_url, +# registered in browser_ui_prefs.cc and read by the native chia:// loader +# (chrome/browser/dig/dig_url_loader_factory.cc). Empty = use the automatic +# ladder; non-empty = the exclusive override. # * chrome/browser/resources/settings/dig_page/ — the Polymer settings element -# (settings-dig-page). The slider is driven by a synthetic PrefObject (the -# value is persisted via the Mojo handler, not the prefs store); a guarded -# observer pushes user edits to SetCapBytes and reflects the server-clamped -# value back. +# (settings-dig-page) binds a cr-input directly to that pref via PrefsMixin, +# normalizing the value (accept http(s):// or a bare host; strip trailing +# slashes) exactly like normalizeCustomEndpoint() in +# dig/node/dig_source_resolution.mjs. No handler, no cache controls. # * Standard settings wiring: route (/dig), router, left-nav menu item, view # in settings-main, page-visibility flag, lazy-load registration, and i18n # strings. +# +# There is intentionally NO local-cache UI and NO DigCacheHandler Mojo: the +# browser is not the node's cache-config surface (the node owns its own cache). ---- /dev/null -+++ b/ui/webui/resources/cr_components/dig_cache/dig_cache.mojom -@@ -0,0 +1,25 @@ -+// Copyright 2026 The DIG Network Authors. All rights reserved. -+// Use of this source code is governed by a BSD-style license that can be -+// found in the LICENSE file. -+ -+module dig_cache.mojom; -+ -+// The local DIG cache configuration, surfaced in the chrome://settings DIG -+// section. Sizes are in bytes. -+struct CacheConfig { -+ uint64 cap_bytes; -+ uint64 used_bytes; -+}; -+ -+// Browser-process handler for the chrome://settings DIG cache section. Pull-on- -+// open: the page reads the config, sets the cap, or clears the cache. The -+// browser fulfils each by calling the in-process DIG runtime (CallDigRpc FFI), -+// which owns the cache. -+interface DigCacheHandler { -+ // Current cap + on-disk usage. -+ GetConfig() => (CacheConfig config); -+ // Set the LRU cap (bytes); returns the effective cap (floored at 64 MiB). -+ SetCapBytes(uint64 cap_bytes) => (uint64 effective_cap_bytes); -+ // Purge the cache (content re-warms from rpc.dig.net on next visit). -+ Clear() => (); -+}; ---- /dev/null -+++ b/ui/webui/resources/cr_components/dig_cache/BUILD.gn -@@ -0,0 +1,17 @@ -+# Copyright 2026 The DIG Network Authors. All rights reserved. -+# Use of this source code is governed by a BSD-style license that can be -+# found in the LICENSE file. -+ -+import("//mojo/public/tools/bindings/mojom.gni") -+ -+# DIG cache settings interface. Generates C++ bindings (consumed by the -+# settings WebUI handler) and a WebUI TS module. The generated -+# dig_cache.mojom-webui.ts is shipped + compiled through the settings bundle -+# (see chrome/browser/resources/settings/BUILD.gn mojo_files), mirroring how -+# autofill_ai_enums.mojom is consumed by settings. No component TS/CSS of its -+# own — the DIG settings element lives in the settings bundle as dig_page.ts. -+mojom("mojom") { -+ sources = [ "dig_cache.mojom" ] -+ -+ webui_module_path = "/" -+} ---- /dev/null -+++ b/chrome/browser/ui/webui/cr_components/dig_cache/dig_cache_handler.h -@@ -0,0 +1,32 @@ -+// Copyright 2026 The DIG Network Authors. All rights reserved. -+// Use of this source code is governed by a BSD-style license that can be -+// found in the LICENSE file. -+ -+#ifndef CHROME_BROWSER_UI_WEBUI_CR_COMPONENTS_DIG_CACHE_DIG_CACHE_HANDLER_H_ -+#define CHROME_BROWSER_UI_WEBUI_CR_COMPONENTS_DIG_CACHE_DIG_CACHE_HANDLER_H_ -+ -+#include "mojo/public/cpp/bindings/pending_receiver.h" -+#include "mojo/public/cpp/bindings/receiver.h" -+#include "ui/webui/resources/cr_components/dig_cache/dig_cache.mojom.h" -+ -+// Browser-process handler for the chrome://settings DIG cache section. Reads + -+// writes the local-cache config by calling the in-process DIG runtime -+// (dig::CallDigRpc — the cache lives in dig-node). -+class DigCacheHandler : public dig_cache::mojom::DigCacheHandler { -+ public: -+ explicit DigCacheHandler( -+ mojo::PendingReceiver receiver); -+ DigCacheHandler(const DigCacheHandler&) = delete; -+ DigCacheHandler& operator=(const DigCacheHandler&) = delete; -+ ~DigCacheHandler() override; -+ -+ // dig_cache::mojom::DigCacheHandler: -+ void GetConfig(GetConfigCallback callback) override; -+ void SetCapBytes(uint64_t cap_bytes, SetCapBytesCallback callback) override; -+ void Clear(ClearCallback callback) override; -+ -+ private: -+ mojo::Receiver receiver_; -+}; -+ -+#endif // CHROME_BROWSER_UI_WEBUI_CR_COMPONENTS_DIG_CACHE_DIG_CACHE_HANDLER_H_ ---- /dev/null -+++ b/chrome/browser/ui/webui/cr_components/dig_cache/dig_cache_handler.cc -@@ -0,0 +1,82 @@ -+// Copyright 2026 The DIG Network Authors. All rights reserved. -+// Use of this source code is governed by a BSD-style license that can be -+// found in the LICENSE file. -+ -+#include "chrome/browser/ui/webui/cr_components/dig_cache/dig_cache_handler.h" -+ -+#include -+#include -+#include -+ -+#include "base/functional/bind.h" -+#include "base/json/json_reader.h" -+#include "base/strings/stringprintf.h" -+#include "base/task/thread_pool.h" -+#include "base/values.h" -+#include "chrome/browser/dig/dig_native_runtime.h" -+ -+namespace { -+ -+// Blocking FFI into the in-process DIG runtime — must run on a MayBlock thread. -+std::string CallDig(std::string request_json) { -+ return dig::CallDigRpc(request_json); -+} -+ -+// Pull a u64 field out of the JSON-RPC "result" dict (response from dig-node). -+uint64_t ResultU64(const std::string& resp, const char* field) { -+ std::optional v = -+ base::JSONReader::Read(resp, base::JSON_PARSE_RFC); -+ if (v && v->is_dict()) { -+ if (const base::DictValue* result = v->GetDict().FindDict("result")) { -+ return static_cast(result->FindDouble(field).value_or(0)); -+ } -+ } -+ return 0; -+} -+ -+} // namespace -+ -+DigCacheHandler::DigCacheHandler( -+ mojo::PendingReceiver receiver) -+ : receiver_(this, std::move(receiver)) {} -+ -+DigCacheHandler::~DigCacheHandler() = default; -+ -+void DigCacheHandler::GetConfig(GetConfigCallback callback) { -+ base::ThreadPool::PostTaskAndReplyWithResult( -+ FROM_HERE, {base::MayBlock()}, -+ base::BindOnce(&CallDig, -+ R"({"jsonrpc":"2.0","id":1,"method":"cache.getConfig"})"), -+ base::BindOnce( -+ [](GetConfigCallback cb, std::string resp) { -+ auto config = dig_cache::mojom::CacheConfig::New(); -+ config->cap_bytes = ResultU64(resp, "cap_bytes"); -+ config->used_bytes = ResultU64(resp, "used_bytes"); -+ std::move(cb).Run(std::move(config)); -+ }, -+ std::move(callback))); -+} -+ -+void DigCacheHandler::SetCapBytes(uint64_t cap_bytes, -+ SetCapBytesCallback callback) { -+ std::string req = base::StringPrintf( -+ R"({"jsonrpc":"2.0","id":1,"method":"cache.setCapBytes",)" -+ R"("params":{"cap_bytes":%llu}})", -+ static_cast(cap_bytes)); -+ base::ThreadPool::PostTaskAndReplyWithResult( -+ FROM_HERE, {base::MayBlock()}, base::BindOnce(&CallDig, std::move(req)), -+ base::BindOnce( -+ [](SetCapBytesCallback cb, std::string resp) { -+ std::move(cb).Run(ResultU64(resp, "cap_bytes")); -+ }, -+ std::move(callback))); -+} -+ -+void DigCacheHandler::Clear(ClearCallback callback) { -+ base::ThreadPool::PostTaskAndReplyWithResult( -+ FROM_HERE, {base::MayBlock()}, -+ base::BindOnce(&CallDig, -+ R"({"jsonrpc":"2.0","id":1,"method":"cache.clear"})"), -+ base::BindOnce([](ClearCallback cb, std::string) { std::move(cb).Run(); }, -+ std::move(callback))); -+} --- /dev/null +++ b/chrome/browser/resources/settings/dig_page/dig_page.ts -@@ -0,0 +1,147 @@ +@@ -0,0 +1,112 @@ +// Copyright 2026 The DIG Network Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * @fileoverview -+ * 'settings-dig-page' is the settings page for the DIG Network protocol — -+ * currently the native local-cache threshold. ++ * 'settings-dig-page' is the settings page for the DIG Network protocol. It ++ * exposes the browser's own pure-RPC-consumer setting: an optional CUSTOM DIG ++ * node endpoint that OVERRIDES the automatic node-resolution ladder (§5.3: ++ * custom endpoint > dig.local > localhost > rpc.dig.net). It is the browser's ++ * OWN consumer preference (which external node to read from) — NOT node ++ * configuration. The browser owns no node and surfaces no node/cache config. + */ +import 'chrome://resources/cr_elements/cr_button/cr_button.js'; -+import '../controls/settings_slider.js'; ++import 'chrome://resources/cr_elements/cr_input/cr_input.js'; +import '../settings_page/settings_section.js'; +import '../settings_shared.css.js'; + +import {PrefsMixin} from '/shared/settings/prefs/prefs_mixin.js'; -+import {WebUiListenerMixin} from 'chrome://resources/cr_elements/web_ui_listener_mixin.js'; +import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js'; + +import {getSearchManager} from '../search_settings.js'; +import type {SettingsPlugin} from '../settings_main/settings_plugin.js'; + -+import {DigCacheHandler} from '../dig_cache.mojom-webui.js'; -+import type {DigCacheHandlerRemote} from '../dig_cache.mojom-webui.js'; +import {getTemplate} from './dig_page.html.js'; + -+const GIB: number = 1024 * 1024 * 1024; ++// The persisted browser pref holding the custom node endpoint (empty = use the ++// automatic ladder). Registered in browser_ui_prefs.cc and read by the native ++// chia:// loader (dig_url_loader_factory.cc). Keep this key in sync with those ++// two sites and dig/node/dig_source_resolution.mjs. ++const CUSTOM_NODE_PREF: string = 'dig.custom_node_url'; + -+const SettingsDigPageElementBase = -+ WebUiListenerMixin(PrefsMixin(PolymerElement)); ++const SettingsDigPageElementBase = PrefsMixin(PolymerElement); + +export class SettingsDigPageElement extends SettingsDigPageElementBase + implements SettingsPlugin { @@ -240,95 +74,56 @@ + + static get properties() { + return { -+ // Synthetic pref backing the cache-cap slider. Not a real Chrome pref — -+ // settings-slider only needs a PrefObject shape (key/type/value) -+ // to drive the knob; the value (in GiB) is persisted via the DIG mojo -+ // handler, not the prefs store. -+ capPref_: { -+ type: Object, -+ value() { -+ return { -+ key: 'digCacheCapGib', -+ type: chrome.settingsPrivate.PrefType.NUMBER, -+ value: 1, -+ }; -+ }, -+ }, -+ -+ usedBytes_: { -+ type: Number, -+ value: 0, -+ }, -+ -+ capTicks_: { -+ type: Array, -+ value() { -+ return [ -+ {value: 1, label: '1 GB'}, -+ {value: 2, label: '2 GB'}, -+ {value: 5, label: '5 GB'}, -+ {value: 10, label: '10 GB'}, -+ {value: 25, label: '25 GB'}, -+ {value: 50, label: '50 GB'}, -+ ]; -+ }, -+ }, -+ -+ usedLabel_: { -+ type: String, -+ computed: 'computeUsedLabel_(usedBytes_, capPref_.value)', -+ }, ++ // Editable text bound to the input; committed to the pref on change after ++ // normalization. Seeded from the pref on ready(). ++ nodeUrl_: {type: String, value: ''}, ++ nodeUrlInvalid_: {type: Boolean, value: false}, + }; + } + -+ static get observers() { -+ return ['onCapPrefChanged_(capPref_.value)']; -+ } -+ -+ declare private capPref_: chrome.settingsPrivate.PrefObject; -+ declare private usedBytes_: number; -+ declare private capTicks_: Array<{value: number, label: string}>; -+ declare private usedLabel_: string; -+ -+ private handler_: DigCacheHandlerRemote = DigCacheHandler.getRemote(); -+ // Suppresses the pref observer while we apply values programmatically (load / -+ // server-clamped echo), so only genuine user slider edits hit the handler. -+ private cacheLoaded_: boolean = false; ++ declare private nodeUrl_: string; ++ declare private nodeUrlInvalid_: boolean; + + override ready() { + super.ready(); -+ this.refreshConfig_(); ++ this.nodeUrl_ = this.getPref(CUSTOM_NODE_PREF).value; + } + -+ private async refreshConfig_() { -+ const {config} = await this.handler_.getConfig(); -+ this.usedBytes_ = Number(config.usedBytes); -+ this.cacheLoaded_ = false; -+ this.set('capPref_.value', Number(config.capBytes) / GIB); -+ this.cacheLoaded_ = true; -+ } -+ -+ private async onCapPrefChanged_() { -+ if (!this.cacheLoaded_) { ++ // Normalize + persist the endpoint on edit. Mirrors normalizeCustomEndpoint() ++ // in dig/node/dig_source_resolution.mjs: accept http(s):// or a bare host ++ // (assumed http), strip trailing slashes; empty clears the override. ++ private onNodeUrlChanged_() { ++ const raw = (this.nodeUrl_ || '').trim(); ++ if (raw === '') { ++ this.nodeUrlInvalid_ = false; ++ this.setPrefValue(CUSTOM_NODE_PREF, ''); + return; + } -+ const {effectiveCapBytes} = await this.handler_.setCapBytes( -+ BigInt(Math.round(this.capPref_.value * GIB))); -+ const effGiB = Number(effectiveCapBytes) / GIB; -+ if (effGiB !== this.capPref_.value) { -+ this.cacheLoaded_ = false; -+ this.set('capPref_.value', effGiB); -+ this.cacheLoaded_ = true; ++ const withScheme = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(raw) ++ ? raw ++ : `http://${raw}`; ++ let url: URL; ++ try { ++ url = new URL(withScheme); ++ } catch { ++ this.nodeUrlInvalid_ = true; ++ return; + } ++ if (url.protocol !== 'http:' && url.protocol !== 'https:') { ++ this.nodeUrlInvalid_ = true; ++ return; ++ } ++ this.nodeUrlInvalid_ = false; ++ const base = ++ `${url.protocol}//${url.host}${url.pathname}`.replace(/\/+$/, ''); ++ this.setPrefValue(CUSTOM_NODE_PREF, base); + } + -+ private async onClearCacheClick_() { -+ await this.handler_.clear(); -+ this.refreshConfig_(); -+ } -+ -+ private computeUsedLabel_(used: number, capGiB: number): string { -+ return `${(used / GIB).toFixed(2)} GB of ${capGiB} GB used`; ++ // Clear the override → back to the automatic ladder. ++ private onResetClick_() { ++ this.nodeUrl_ = ''; ++ this.nodeUrlInvalid_ = false; ++ this.setPrefValue(CUSTOM_NODE_PREF, ''); + } + + // SettingsPlugin implementation @@ -347,7 +142,7 @@ +customElements.define(SettingsDigPageElement.is, SettingsDigPageElement); --- /dev/null +++ b/chrome/browser/resources/settings/dig_page/dig_page.html -@@ -0,0 +1,25 @@ +@@ -0,0 +1,27 @@ + +
@@ -355,124 +150,26 @@ +
+
+
-+
$i18n{digCacheLimitLabel}
-+
$i18n{digCacheLimitSublabel}
++
$i18n{digNodeEndpointLabel}
++
$i18n{digNodeEndpointSublabel}
+
+
-+ -+ +
-+
[[usedLabel_]]
++ ++ +
+
-+ -+ $i18n{digClearCacheButton} ++ ++ $i18n{digNodeEndpointReset} + +
+
---- a/ui/webui/BUILD.gn -+++ b/ui/webui/BUILD.gn -@@ -49,6 +49,7 @@ - if (!is_android && !is_ios) { - public_deps += [ - "//ui/webui/resources/cr_components/customize_color_scheme_mode:mojom", -+ "//ui/webui/resources/cr_components/dig_cache:mojom", - "//ui/webui/resources/cr_components/history_clusters:mojo_bindings", - "//ui/webui/resources/cr_components/most_visited:mojom", - "//ui/webui/resources/cr_components/theme_color_picker:mojom", ---- a/chrome/browser/ui/BUILD.gn -+++ b/chrome/browser/ui/BUILD.gn -@@ -1351,6 +1351,8 @@ - "webui/chrome_web_contents_handler.h", - "webui/cr_components/customize_color_scheme_mode/customize_color_scheme_mode_handler.cc", - "webui/cr_components/customize_color_scheme_mode/customize_color_scheme_mode_handler.h", -+ "webui/cr_components/dig_cache/dig_cache_handler.cc", -+ "webui/cr_components/dig_cache/dig_cache_handler.h", - "webui/cr_components/history/history_util.cc", - "webui/cr_components/history/history_util.h", - "webui/cr_components/history_clusters/history_clusters_util.cc", ---- a/chrome/browser/ui/webui/settings/settings_ui.h -+++ b/chrome/browser/ui/webui/settings/settings_ui.h -@@ -19,6 +19,7 @@ - #include "mojo/public/cpp/bindings/receiver.h" - #include "ui/webui/mojo_web_ui_controller.h" - #include "ui/webui/resources/cr_components/customize_color_scheme_mode/customize_color_scheme_mode.mojom.h" -+#include "ui/webui/resources/cr_components/dig_cache/dig_cache.mojom.h" - #include "ui/webui/resources/cr_components/help_bubble/help_bubble.mojom.h" - - #if !BUILDFLAG(IS_CHROMEOS) -@@ -44,6 +45,7 @@ - #endif // BUILDFLAG(ENABLE_DICE_SUPPORT) - - class CustomizeColorSchemeModeHandler; -+class DigCacheHandler; - namespace settings { - - class SettingsUI; -@@ -117,6 +119,11 @@ - CustomizeColorSchemeModeHandlerFactory> - pending_receiver); - -+ // DIG: the chrome://settings DIG cache section binds this directly (no -+ // factory — it is a simple pull-on-open handler). -+ void BindInterface( -+ mojo::PendingReceiver receiver); -+ - private: - void AddSettingsPageUIHandler( - std::unique_ptr handler); -@@ -178,6 +185,8 @@ - CustomizeColorSchemeModeHandlerFactory> - customize_color_scheme_mode_handler_factory_receiver_{this}; - -+ std::unique_ptr dig_cache_handler_; -+ - base::CallbackListSubscription glic_settings_state_subscription_; - - WEB_UI_CONTROLLER_TYPE_DECL(); ---- a/chrome/browser/ui/webui/settings/settings_ui.cc -+++ b/chrome/browser/ui/webui/settings/settings_ui.cc -@@ -53,6 +53,7 @@ - #include "chrome/browser/ui/ui_features.h" - #include "chrome/browser/ui/views/side_panel/customize_chrome/customize_chrome_utils.h" - #include "chrome/browser/ui/webui/cr_components/customize_color_scheme_mode/customize_color_scheme_mode_handler.h" -+#include "chrome/browser/ui/webui/cr_components/dig_cache/dig_cache_handler.h" - #include "chrome/browser/ui/webui/extension_control_handler.h" - #include "chrome/browser/ui/webui/favicon_source.h" - #include "chrome/browser/ui/webui/managed_ui_handler.h" -@@ -860,6 +861,11 @@ - std::move(pending_receiver)); - } - -+void SettingsUI::BindInterface( -+ mojo::PendingReceiver receiver) { -+ dig_cache_handler_ = std::make_unique(std::move(receiver)); -+} -+ - void SettingsUI::UpdateShowGlicState() { - // The visibility of the Glic page can change based on the user accepting the - // FRE. Propagate this state to the WebUI value used to display the settings ---- a/chrome/browser/chrome_browser_interface_binders_webui_parts_desktop.cc -+++ b/chrome/browser/chrome_browser_interface_binders_webui_parts_desktop.cc -@@ -137,6 +137,7 @@ - #include "ui/webui/resources/cr_components/color_change_listener/color_change_listener.mojom.h" - #include "ui/webui/resources/cr_components/composebox/composebox.mojom.h" - #include "ui/webui/resources/cr_components/customize_color_scheme_mode/customize_color_scheme_mode.mojom.h" -+#include "ui/webui/resources/cr_components/dig_cache/dig_cache.mojom.h" - #include "ui/webui/resources/cr_components/help_bubble/custom_help_bubble.mojom.h" - #include "ui/webui/resources/cr_components/history/foreign_sessions.mojom.h" - #include "ui/webui/resources/cr_components/history/history.mojom.h" -@@ -668,6 +669,7 @@ - #endif // BUILDFLAG(ENABLE_DICE_SUPPORT) - .Add() -+ .Add() - .Add(); - - if (webui_browser::IsWebUIBrowserEnabled()) { --- a/chrome/browser/resources/settings/BUILD.gn +++ b/chrome/browser/resources/settings/BUILD.gn @@ -101,6 +101,7 @@ @@ -483,30 +180,13 @@ "downloads_page/downloads_page.ts", "glic_page/glic_page.ts", "glic_page/glic_login_permissions_page.ts", -@@ -435,8 +436,14 @@ - "site_settings/site_settings_shared.css", - ] - -- mojo_files_deps = [ "//components/autofill/core/browser/data_model/autofill_ai:mojo_bindings_ts__generator" ] -- mojo_files = [ "$root_gen_dir/components/autofill/core/browser/data_model/autofill_ai/autofill_ai_enums.mojom-webui.ts" ] -+ mojo_files_deps = [ -+ "//components/autofill/core/browser/data_model/autofill_ai:mojo_bindings_ts__generator", -+ "//ui/webui/resources/cr_components/dig_cache:mojom_ts__generator", -+ ] -+ mojo_files = [ -+ "$root_gen_dir/components/autofill/core/browser/data_model/autofill_ai/autofill_ai_enums.mojom-webui.ts", -+ "$root_gen_dir/ui/webui/resources/cr_components/dig_cache/dig_cache.mojom-webui.ts", -+ ] - - ts_composite = true - ts_definitions = [ --- a/chrome/browser/resources/settings/route.ts +++ b/chrome/browser/resources/settings/route.ts @@ -296,6 +296,12 @@ loadTimeData.getString('downloadsPageTitle')); } - -+ // DIG: the DIG Network protocol section (local cache, etc.). + ++ // DIG: the DIG Network protocol section (custom node endpoint). + if (visibility.dig !== false) { + r.DIG = r.BASIC.createSection( + '/dig', 'dig', loadTimeData.getString('digPageTitle')); @@ -546,7 +226,7 @@ @@ -181,6 +181,15 @@
- + +