From dd59077f2b51a14cd153ffcd7a1e8361e317b0c0 Mon Sep 17 00:00:00 2001 From: Harish Seshadri Date: Tue, 10 Nov 2026 09:48:23 -0800 Subject: [PATCH 1/3] docs: bump @gainratio/browser to 0.4.5, add llms.txt, live probe, and Northstar docs Co-Authored-By: OpenAI Codex --- .github/dependabot.yml | 6 +++ .github/workflows/live-probe.yml | 22 ++++++++++ CLAUDE.md | 43 +++++++++++++++++++ README.md | 47 +++++++++++++-------- docs/index.html | 4 +- index.html | 18 +++++--- llms.txt | 18 ++++++++ package.json | 4 +- scripts/build.mjs | 1 + scripts/check.mjs | 15 ++++++- scripts/live-probe.mjs | 72 ++++++++++++++++++++++++++++++++ security/index.html | 2 +- 12 files changed, 223 insertions(+), 29 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/live-probe.yml create mode 100644 CLAUDE.md create mode 100644 llms.txt create mode 100644 scripts/live-probe.mjs diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..ca79ca5 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly diff --git a/.github/workflows/live-probe.yml b/.github/workflows/live-probe.yml new file mode 100644 index 0000000..fd4c731 --- /dev/null +++ b/.github/workflows/live-probe.yml @@ -0,0 +1,22 @@ +name: Live probe + +on: + workflow_dispatch: + schedule: + - cron: '29 6 * * 1' + +permissions: + contents: read + +jobs: + probe: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + persist-credentials: false + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version: 22 + - name: Probe production browser.gainratio.com + run: node scripts/live-probe.mjs diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..792eba2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,43 @@ +# CLAUDE.md — browser-docs (`browser.gainratio.com`) + +Canonical agent instructions for the EdgeProc Browser documentation site (`gainratio/browser-docs`). + +## Status & Portfolio Pointers + +- **Live URL:** `https://browser.gainratio.com/` (Cloudflare Pages project `browser-docs-bai`) +- **Upstream library repo:** `https://github.com/gainratio/edgeproc-browser` (npm `@gainratio/browser`, current published version `0.4.5`) +- **Portfolio hub:** `https://open-source.gainratio.com/edgeproc-browser/` +- **Portfolio engineering standard §8 (WASM):** Not applicable to this docs site — pure static HTML/CSS with zero client-side JavaScript or WASM assets. +- **Portfolio engineering standard §3.1–§3.4:** §3.1 enforced via strict `default-src 'none'` CSP and executable-script ban in `scripts/check.mjs`; §3.2–§3.4 N/A. + +## Stack & Layout + +- **Runtime:** Node 22+ (build and gate scripts only; zero npm dependencies). +- **Pages:** `index.html`, `docs/index.html`, `examples/index.html`, `security/index.html`, `404.html`. +- **Public root artifacts:** `_headers`, `robots.txt`, `sitemap.xml`, `llms.txt`, `favicon.svg`, `favicon.ico`. +- **Scripts:** + - `scripts/build.mjs` — copies explicit `ROOT_FILES` + `DIRS` allowlist into `dist/`. + - `scripts/check.mjs` — validates HTML tag nesting, headings, anchors, JSON-LD, sitemap, `llms.txt`, and strict `_headers` CSP. + - `scripts/live-probe.mjs` — secret-free production probe (`live-probe.yml`). + +## Core Invariants + +1. **Zero executable scripts on pages.** `scripts/check.mjs` rejects any ` @@ -53,7 +59,7 @@
-

Open source · TypeScript · MIT · v0.4.4

+

Open source · TypeScript · MIT · v0.4.5

Signed data, checked in the browser.

Have the page check that downloaded data was signed by you and not changed, then keep it for offline use. @gainratio/browser verifies a signed bundle in a Web Worker before your app sees a byte.

@@ -65,7 +71,7 @@

Signed data, checked in the browser.

  • SHA-256 content addresses
  • SQLite on OPFS
  • Typed refusals
  • -
  • 0.4.4
  • +
  • 0.4.5
  • AlmaMesh uses it to download its chart engine.

    @@ -285,7 +291,7 @@

    It cannot protect a compromised page

    It is still pre-1.0

    -

    0.4.4 is the current release. Chromium gets the full browser suite; Firefox and WebKit get a cross-browser test. Playwright's WebKit refuses OPFS, and real Safari is not tested.

    +

    0.4.5 is the current release. Chromium gets the full browser suite; Firefox and WebKit get a cross-browser test. Playwright's WebKit refuses OPFS, and real Safari is not tested.

    diff --git a/llms.txt b/llms.txt new file mode 100644 index 0000000..87cedbc --- /dev/null +++ b/llms.txt @@ -0,0 +1,18 @@ +# @gainratio/browser (EdgeProc Browser) — Signed Data Checked in the Browser + +> TypeScript browser engine (`@gainratio/browser` v0.4.5 on npm) for syncing Ed25519-signed, SHA-256 content-addressed data bundles inside a Web Worker with anti-rollback sequence enforcement and SQLite-on-OPFS caching. + +- Live Documentation: https://browser.gainratio.com/ +- Open Source Hub Overview: https://open-source.gainratio.com/edgeproc-browser/ +- Portfolio Hub: https://open-source.gainratio.com/ +- Source Repository: https://github.com/gainratio/edgeproc-browser +- npm Package: https://www.npmjs.com/package/@gainratio/browser +- Upstream Python Publisher (`edge-proc`): https://github.com/gainratio/edge-proc +- Sitemap: https://browser.gainratio.com/sitemap.xml + +## Documentation Pages + +- [Overview & Quickstart](https://browser.gainratio.com/): Why checksums are not signatures, Web Worker verification flow, copyable Node and Vite quickstarts, storage boundaries, and browser compatibility. +- [API & Concepts Reference](https://browser.gainratio.com/docs/): Install `@gainratio/browser`, bundle publishing via `edgeproc`, `EngineClient` + `@gainratio/browser/worker`, `syncIndex`, keyring rotation, SQLite/OPFS storage, vector subpaths, `/seal` passphrase encryption, and typed error codes. +- [Runnable Examples](https://browser.gainratio.com/examples/): Copyable Vite + Worker setup, Node verification, cache inspection, keyring rotation, SQL queries, vector retrieval, sealed exports, and tamper/rollback refusal examples. +- [Security & Threat Model](https://browser.gainratio.com/security/): Trust-root custody, anti-rollback sequence guarantees, expiry handling, OIDC npm provenance, supported versions (`0.4.5`), and private vulnerability reporting. diff --git a/package.json b/package.json index 6476be7..8266c4c 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,8 @@ "description": "Standalone documentation site for @gainratio/browser (browser.gainratio.com)", "scripts": { "build": "node scripts/build.mjs", - "check": "node scripts/check.mjs" + "check": "node scripts/check.mjs", + "gate": "npm run build && npm run check", + "probe:live": "node scripts/live-probe.mjs" } } diff --git a/scripts/build.mjs b/scripts/build.mjs index d73f7fb..7dfe6b5 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -13,6 +13,7 @@ const ROOT_FILES = [ "_headers", "robots.txt", "sitemap.xml", + "llms.txt", "favicon.ico", "favicon.svg", "b1e67f1efb625383dea94135ac083633.txt", diff --git a/scripts/check.mjs b/scripts/check.mjs index 930a412..cadf58c 100644 --- a/scripts/check.mjs +++ b/scripts/check.mjs @@ -23,6 +23,7 @@ const REQUIRED_ASSETS = [ "favicon.svg", "robots.txt", "sitemap.xml", + "llms.txt", "_headers", "scripts/check.mjs", ]; @@ -432,11 +433,20 @@ if (!sitemap.includes('xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"')) { fail("sitemap namespace missing"); } -/* Robots. */ +/* Robots and llms.txt. */ const robots = read("robots.txt"); if (!/^User-agent:\s*\*$/m.test(robots)) fail("robots.txt missing User-agent: *"); if (!/^Allow:\s*\/$/m.test(robots)) fail("robots.txt missing Allow: /"); if (!robots.includes(`Sitemap: ${SITE}/sitemap.xml`)) fail("robots.txt sitemap"); +if (existsSync(join(ROOT, "llms.txt"))) { + const llms = read("llms.txt"); + for (const loc of expectedLocs) { + if (!llms.includes(loc)) fail(`llms.txt missing ${loc}`); + } + if (!llms.includes("https://open-source.gainratio.com/")) { + fail("llms.txt missing https://open-source.gainratio.com/"); + } +} /* Security headers and caching. */ const headers = read("_headers"); @@ -495,7 +505,8 @@ for (const page of PAGES) { const distDir = join(ROOT, "dist"); if (existsSync(distDir)) { - for (const forbidden of ["package.json", "README.md", "SECURITY.md", "LICENSE", "scripts"]) { + if (!existsSync(join(distDir, "llms.txt"))) fail("dist/: missing llms.txt"); + for (const forbidden of ["package.json", "README.md", "CLAUDE.md", "SECURITY.md", "LICENSE", "scripts"]) { if (readdirSync(distDir).includes(forbidden)) fail(`dist/: leaked non-public file ${forbidden}`); } } diff --git a/scripts/live-probe.mjs b/scripts/live-probe.mjs new file mode 100644 index 0000000..3d42672 --- /dev/null +++ b/scripts/live-probe.mjs @@ -0,0 +1,72 @@ +#!/usr/bin/env node +/** + * Secret-free live production probe for https://browser.gainratio.com. + * Verifies robots.txt, sitemap.xml, llms.txt, HTTP 404 on unknown route, + * and canonical/H1/JSON-LD across all sitemap URLs. + */ +const HOST = "browser.gainratio.com"; +const PAGES_HOST = "browser-docs-bai.pages.dev"; +const BASE = `https://${HOST}`; +const failures = []; + +async function fetchRaw(url) { + const res = await fetch(url, { + headers: { "user-agent": "GainRatio-Live-Probe/1.0 (+https://gainratio.com)" }, + signal: AbortSignal.timeout(25_000), + }); + return { + status: res.status, + type: res.headers.get("content-type") ?? "", + mitigated: res.headers.get("cf-mitigated") ?? "", + body: await res.text(), + }; +} + +async function fetchSite(url) { + const res = await fetchRaw(url); + if (res.status === 403 && res.mitigated === "challenge") { + return fetchRaw(url.replace(`https://${HOST}`, `https://${PAGES_HOST}`)); + } + return res; +} + +const robots = await fetchSite(`${BASE}/robots.txt`); +if (robots.status !== 200 || !robots.body.toLowerCase().includes("sitemap:")) { + failures.push(`robots.txt failed (HTTP ${robots.status})`); +} + +const llms = await fetchSite(`${BASE}/llms.txt`); +if (llms.status !== 200 || !llms.body.includes("https://open-source.gainratio.com/")) { + failures.push(`llms.txt failed (HTTP ${llms.status})`); +} + +const unknown = await fetchSite(`${BASE}/nonexistent-live-probe-404`); +if (unknown.status !== 404) { + failures.push(`expected HTTP 404 on unknown route, got HTTP ${unknown.status}`); +} + +const sitemap = await fetchSite(`${BASE}/sitemap.xml`); +if (sitemap.status !== 200) failures.push(`sitemap.xml HTTP ${sitemap.status}`); +const locs = [...sitemap.body.matchAll(/([^<]+)<\/loc>/g)].map((m) => m[1]); +if (locs.length === 0) failures.push("sitemap.xml has 0 entries"); + +for (const url of locs) { + const page = await fetchSite(url); + if (page.status !== 200 || !page.type.includes("text/html")) { + failures.push(`${url}: HTTP ${page.status} (${page.type})`); + continue; + } + const canonical = page.body.match(/]+rel="canonical"[^>]+href="([^"]+)"/i)?.[1] ?? ""; + if (canonical.replace(/\/$/, "") !== url.replace(/\/$/, "")) { + failures.push(`${url}: canonical mismatch (${canonical})`); + } + if ([...page.body.matchAll(/]/gi)].length !== 1) { + failures.push(`${url}: expected 1

    `); + } +} + +if (failures.length > 0) { + for (const f of failures) console.error(`- ${f}`); + process.exit(1); +} +console.log(`Live probe passed for ${BASE} (${locs.length} pages + llms.txt + 404).`); diff --git a/security/index.html b/security/index.html index d9ed10b..0c4be82 100644 --- a/security/index.html +++ b/security/index.html @@ -173,7 +173,7 @@

    Supply chain and release checking

    Runtime dependencies are @noble/ed25519, @hpcc-js/wasm-zstd, and, for the opt-in /seal export only, age-encryption. The chunk store and the optional SQLite subpaths share pinned local WASM assets and do not load code from a CDN.

    Supported versions

    -

    This project is pre-1.0. Only the latest published version gets fixes. Version 0.4.4 is current. Once 1.0 ships, the latest minor of the current major will be supported as well.

    +

    This project is pre-1.0. Only the latest published version gets fixes. Version 0.4.5 is current. Once 1.0 ships, the latest minor of the current major will be supported as well.

    Report a vulnerability

    Please do not open a public issue for a security problem. Report it privately in one of two ways:

    From 517efa3db88630543220a5df55e2e15360f423ca Mon Sep 17 00:00:00 2001 From: Harish Seshadri Date: Tue, 10 Nov 2026 10:17:08 -0800 Subject: [PATCH 2/3] security: validate llms.txt URLs via URL parser instead of substring check Co-Authored-By: OpenAI Codex --- scripts/check.mjs | 18 ++++++++++++++++-- scripts/live-probe.mjs | 20 ++++++++++++++++++-- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/scripts/check.mjs b/scripts/check.mjs index cadf58c..979e715 100644 --- a/scripts/check.mjs +++ b/scripts/check.mjs @@ -440,10 +440,24 @@ if (!/^Allow:\s*\/$/m.test(robots)) fail("robots.txt missing Allow: /"); if (!robots.includes(`Sitemap: ${SITE}/sitemap.xml`)) fail("robots.txt sitemap"); if (existsSync(join(ROOT, "llms.txt"))) { const llms = read("llms.txt"); + const hasHttpsUrl = (expectedHost, expectedPath = "/") => { + for (const [raw] of llms.matchAll(/https?:\/\/[^\s)>\]"`]+/g)) { + try { + const parsed = new URL(raw.replace(/[.,;:]+$/, "")); + if (parsed.protocol === "https:" && parsed.hostname === expectedHost && parsed.pathname === expectedPath) { + return true; + } + } catch { + // ignore non-URL tokens + } + } + return false; + }; for (const loc of expectedLocs) { - if (!llms.includes(loc)) fail(`llms.txt missing ${loc}`); + const target = new URL(loc); + if (!hasHttpsUrl(target.hostname, target.pathname)) fail(`llms.txt missing ${loc}`); } - if (!llms.includes("https://open-source.gainratio.com/")) { + if (!hasHttpsUrl("open-source.gainratio.com", "/")) { fail("llms.txt missing https://open-source.gainratio.com/"); } } diff --git a/scripts/live-probe.mjs b/scripts/live-probe.mjs index 3d42672..1b71e99 100644 --- a/scripts/live-probe.mjs +++ b/scripts/live-probe.mjs @@ -25,18 +25,34 @@ async function fetchRaw(url) { async function fetchSite(url) { const res = await fetchRaw(url); if (res.status === 403 && res.mitigated === "challenge") { - return fetchRaw(url.replace(`https://${HOST}`, `https://${PAGES_HOST}`)); + const fallback = new URL(url); + fallback.hostname = PAGES_HOST; + return fetchRaw(fallback.href); } return res; } +function hasHttpsUrl(text, expectedHost, expectedPath = "/") { + for (const [raw] of text.matchAll(/https?:\/\/[^\s)>\]"`]+/g)) { + try { + const parsed = new URL(raw.replace(/[.,;:]+$/, "")); + if (parsed.protocol === "https:" && parsed.hostname === expectedHost && parsed.pathname === expectedPath) { + return true; + } + } catch { + // ignore non-URL tokens + } + } + return false; +} + const robots = await fetchSite(`${BASE}/robots.txt`); if (robots.status !== 200 || !robots.body.toLowerCase().includes("sitemap:")) { failures.push(`robots.txt failed (HTTP ${robots.status})`); } const llms = await fetchSite(`${BASE}/llms.txt`); -if (llms.status !== 200 || !llms.body.includes("https://open-source.gainratio.com/")) { +if (llms.status !== 200 || !hasHttpsUrl(llms.body, "open-source.gainratio.com", "/")) { failures.push(`llms.txt failed (HTTP ${llms.status})`); } From 1acda5d7aa8fb56f1cf255edc40dd2fea381b998 Mon Sep 17 00:00:00 2001 From: Harish Seshadri Date: Tue, 10 Nov 2026 10:27:16 -0800 Subject: [PATCH 3/3] docs: bump @gainratio/browser version references to 0.4.6 Co-Authored-By: OpenAI Codex --- CLAUDE.md | 6 +++--- README.md | 2 +- docs/index.html | 4 ++-- index.html | 8 ++++---- llms.txt | 4 ++-- security/index.html | 2 +- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 792eba2..34ad5fd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,7 +5,7 @@ Canonical agent instructions for the EdgeProc Browser documentation site (`gainr ## Status & Portfolio Pointers - **Live URL:** `https://browser.gainratio.com/` (Cloudflare Pages project `browser-docs-bai`) -- **Upstream library repo:** `https://github.com/gainratio/edgeproc-browser` (npm `@gainratio/browser`, current published version `0.4.5`) +- **Upstream library repo:** `https://github.com/gainratio/edgeproc-browser` (npm `@gainratio/browser`, current published version `0.4.6`) - **Portfolio hub:** `https://open-source.gainratio.com/edgeproc-browser/` - **Portfolio engineering standard §8 (WASM):** Not applicable to this docs site — pure static HTML/CSS with zero client-side JavaScript or WASM assets. - **Portfolio engineering standard §3.1–§3.4:** §3.1 enforced via strict `default-src 'none'` CSP and executable-script ban in `scripts/check.mjs`; §3.2–§3.4 N/A. @@ -23,14 +23,14 @@ Canonical agent instructions for the EdgeProc Browser documentation site (`gainr ## Core Invariants 1. **Zero executable scripts on pages.** `scripts/check.mjs` rejects any `