Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
22 changes: 22 additions & 0 deletions .github/workflows/live-probe.yml
Original file line number Diff line number Diff line change
@@ -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
43 changes: 43 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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.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.

## 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 `<script>` tag whose `type` is not `application/ld+json`, any inline `style` attribute, any `on*` event handler, and any external resource URL.
2. **Live npm version parity.** Every version string in `index.html`, `docs/index.html`, and `security/index.html` must match `npm view @gainratio/browser version` (`0.4.6`).
3. **Explicit `dist/` allowlist.** New public root files must be listed in `scripts/build.mjs` `ROOT_FILES`; repo files (`package.json`, `CLAUDE.md`, `README.md`, `scripts/`) must never enter `dist/`.

## Quality Gates (Non-Negotiable)

Run `npm run gate` before every commit:

- **Scar (version lag vs npm):** Docs continued displaying `0.4.4` after `@gainratio/browser@0.4.5` / `0.4.6` shipped to npm. Verify `npm view @gainratio/browser version` whenever updating release claims.
- **Scar (allowlist drift):** Root files omitted from `scripts/build.mjs` `ROOT_FILES` 404 on Cloudflare Pages; `scripts/check.mjs` verifies `dist/llms.txt` exists and repo files are absent.

## Commands

```sh
npm run build # build dist/ from explicit allowlist
npm run check # run full HTML/SEO/link/CSP deployment gate
npm run gate # build + check
npm run probe:live # probe live https://browser.gainratio.com
```
47 changes: 30 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,45 @@
# browser.gainratio.com
# browser.gainratio.com — Documentation for `@gainratio/browser`

Static documentation site for [`@gainratio/browser`](https://www.npmjs.com/package/@gainratio/browser), the signed-bundle engine behind EdgeProc Browser.
Static documentation site for [**`@gainratio/browser`**](https://www.npmjs.com/package/@gainratio/browser) (`gainratio/edgeproc-browser`), the signed-data browser engine.

Pages:
**What problem this solves.** Modern web apps often download data files — a product catalog, a search index, or a sanctions watchlist — and run queries right inside the browser tab. Normally the page blindly trusts whatever file comes back from the network or CDN, even if a mirror serves an outdated copy or a file is corrupted mid-transfer. `@gainratio/browser` checks a cryptographic signature on every download inside a background Web Worker before your app ever touches the data, refuses rollbacks to older versions, and keeps verified data ready for offline use.

- `index.html` — overview, problem, quickstart, copyable Node example, limits, data handling, FAQ
- `docs/index.html` — install, concepts, API, errors, browser support, limits
- `examples/index.html` — copyable Vite, Node, cache, keyring, SQL, vector, seal, and tamper examples
- `security/index.html` — threat model, trust-root lifecycle, reporting
- `404.html` — not found page (noindex)
**What this repo is.** The plain HTML and CSS documentation site served at [browser.gainratio.com](https://browser.gainratio.com/) — zero client-side JavaScript, zero external fonts, and zero trackers.

The site is plain HTML and CSS. It has no build step, no client-side JavaScript, no external fonts, scripts, images, or trackers. It uses local favicon assets only.

## Check
## Run locally in one command

```sh
node scripts/check.mjs
npm run gate
```

The check fails on missing metadata, JSON-LD, sitemap entries, broken internal links or anchors, malformed tag structure, inline executable code, external resources, missing assets, or a weak security header policy.
That builds the clean `dist/` directory and verifies every page's HTML structure, links, anchors, JSON-LD metadata, `llms.txt`, and strict Content-Security-Policy headers.

---

## Under the hood (for developers)

### Site map & architecture

## Deploy
| Path | Purpose |
|---|---|
| `index.html` | Landing page, problem overview, copyable quickstart, storage guarantees, FAQ |
| `docs/index.html` | Install (`@gainratio/browser` `0.4.6`), concepts, Worker & `syncIndex` APIs, keyrings, SQL/OPFS, errors |
| `examples/index.html` | Copyable Vite, Node, cache, keyring, SQL, vector, `/seal`, and tamper-refusal examples |
| `security/index.html` | Threat model, trust-root custody, anti-rollback rules, OIDC provenance, private reporting |
| `404.html` | Not-found recovery page (`noindex,follow`, excluded from `sitemap.xml`) |
| `llms.txt` | Structured plain-text index for AI coding assistants and crawlers |
| `_headers` | Strict CSP (`default-src 'none'`), HSTS, COOP/CORP, and immutable `/assets/*` caching |
| `scripts/build.mjs` | Explicit allowlist copier into `dist/` |
| `scripts/check.mjs` | Zero-dependency HTML tag scanner, link/anchor verifier, SEO, and CSP contract gate |
| `scripts/live-probe.mjs` | Secret-free production probe (`https://browser.gainratio.com`) |

Build `dist/` (which contains only the public web assets) and deploy it as static files to Cloudflare Pages or any static host:
### Deploy

```sh
npm run build && npm run check
npm run gate
npx wrangler pages deploy dist --project-name browser-docs --branch main
```

The custom domain `browser.gainratio.com` should be attached to that deployment. `_headers` supplies the security policy and asset caching.
- **Library source:** [github.com/gainratio/edgeproc-browser](https://github.com/gainratio/edgeproc-browser)
- **Open-source hub:** [open-source.gainratio.com/edgeproc-browser/](https://open-source.gainratio.com/edgeproc-browser/)
- **License:** [MIT](./LICENSE)
4 changes: 2 additions & 2 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ <h1>Install, sync, verify, read.</h1>
<p class="lede">This page covers the documented browser path: install <code>@gainratio/browser</code>, sync a signed bundle in a Worker, verify it, and read the checked bytes. It also covers the documented verification core, keyrings, SQL, vectors, seal, errors, browser support, and limits.</p>

<h2 id="install">Install</h2>
<p>The package is <code>@gainratio/browser</code>, currently version 0.4.4. It is MIT licensed and published with npm provenance from CI. The old name <code>@edgeproc/browser</code> is deprecated.</p>
<p>The package is <code>@gainratio/browser</code>, currently version 0.4.6. It is MIT licensed and published with npm provenance from CI. The old name <code>@edgeproc/browser</code> is deprecated.</p>
<pre><code>npm install @gainratio/browser</code></pre>
<p>pnpm and Bun work too. To pin an exact commit instead of a release, <code>pnpm add github:gainratio/edgeproc-browser#&lt;commit-sha&gt;</code> is documented by the project.</p>

Expand Down Expand Up @@ -343,7 +343,7 @@ <h2 id="limits">Key limits</h2>
<div class="grid-2">
<div>
<ul class="plain">
<li><strong>Pre-1.0.</strong> Only the latest published version gets fixes. 0.4.4 is current.</li>
<li><strong>Pre-1.0.</strong> Only the latest published version gets fixes. 0.4.6 is current.</li>
<li><strong>The trust root is capped at 64 KiB</strong> and fetched with no HTTP-cache reuse.</li>
<li><strong>Idle timeout:</strong> 60 s by default. Any progress event re-arms it; a slow link is not a dead link.</li>
<li><strong>Stall window:</strong> 30 s with no bytes. A transfer that keeps delivering is never cut off, however slow.</li>
Expand Down
18 changes: 12 additions & 6 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,18 @@
"codeRepository": "https://github.com/gainratio/edgeproc-browser",
"programmingLanguage": ["TypeScript", "JavaScript"],
"license": "https://opensource.org/license/mit",
"version": "0.4.4",
"version": "0.4.6",
"url": "https://browser.gainratio.com/",
"sameAs": [
"https://github.com/gainratio/edgeproc-browser",
"https://www.npmjs.com/package/@gainratio/browser"
]
"https://www.npmjs.com/package/@gainratio/browser",
"https://open-source.gainratio.com/edgeproc-browser/"
],
"isPartOf": {
"@type": "WebSite",
"name": "GainRatio Open Source",
"url": "https://open-source.gainratio.com/"
}
}
</script>
</head>
Expand All @@ -53,7 +59,7 @@
<main id="main">
<section class="hero" aria-labelledby="hero-heading">
<div class="wrap">
<p class="eyebrow">Open source · TypeScript · MIT · v0.4.4</p>
<p class="eyebrow">Open source · TypeScript · MIT · v0.4.6</p>
<h1 id="hero-heading">Signed data, checked in the browser.</h1>
<p class="lede">Have the page check that downloaded data was signed by you and not changed, then keep it for offline use. <code>@gainratio/browser</code> verifies a signed bundle in a Web Worker before your app sees a byte.</p>
<div class="hero-actions">
Expand All @@ -65,7 +71,7 @@ <h1 id="hero-heading">Signed data, checked in the browser.</h1>
<li class="pill">SHA-256 content addresses</li>
<li class="pill">SQLite on OPFS</li>
<li class="pill">Typed refusals</li>
<li class="pill">0.4.4</li>
<li class="pill">0.4.6</li>
</ul>
<p class="small muted"><a href="https://github.com/gainratio/almamesh">AlmaMesh</a> uses it to download its chart engine.</p>
</div>
Expand Down Expand Up @@ -285,7 +291,7 @@ <h3>It cannot protect a compromised page</h3>
</div>
<div class="card">
<h3>It is still pre-1.0</h3>
<p>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.</p>
<p>0.4.6 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.</p>
</div>
</div>
</div>
Expand Down
18 changes: 18 additions & 0 deletions llms.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# @gainratio/browser (EdgeProc Browser) — Signed Data Checked in the Browser

> TypeScript browser engine (`@gainratio/browser` v0.4.6 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.6`), and private vulnerability reporting.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
1 change: 1 addition & 0 deletions scripts/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const ROOT_FILES = [
"_headers",
"robots.txt",
"sitemap.xml",
"llms.txt",
"favicon.ico",
"favicon.svg",
"b1e67f1efb625383dea94135ac083633.txt",
Expand Down
29 changes: 27 additions & 2 deletions scripts/check.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const REQUIRED_ASSETS = [
"favicon.svg",
"robots.txt",
"sitemap.xml",
"llms.txt",
"_headers",
"scripts/check.mjs",
];
Expand Down Expand Up @@ -432,11 +433,34 @@ 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");
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) {
const target = new URL(loc);
if (!hasHttpsUrl(target.hostname, target.pathname)) fail(`llms.txt missing ${loc}`);
}
if (!hasHttpsUrl("open-source.gainratio.com", "/")) {
fail("llms.txt missing https://open-source.gainratio.com/");
}
}

/* Security headers and caching. */
const headers = read("_headers");
Expand Down Expand Up @@ -495,7 +519,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}`);
}
}
Expand Down
88 changes: 88 additions & 0 deletions scripts/live-probe.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#!/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") {
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 || !hasHttpsUrl(llms.body, "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>([^<]+)<\/loc>/g)].map((m) => m[1]);
if (locs.length === 0) failures.push("sitemap.xml has 0 <loc> 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(/<link[^>]+rel="canonical"[^>]+href="([^"]+)"/i)?.[1] ?? "";
if (canonical.replace(/\/$/, "") !== url.replace(/\/$/, "")) {
failures.push(`${url}: canonical mismatch (${canonical})`);
}
if ([...page.body.matchAll(/<h1[\s>]/gi)].length !== 1) {
failures.push(`${url}: expected 1 <h1>`);
}
}

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).`);
2 changes: 1 addition & 1 deletion security/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ <h2 id="supply">Supply chain and release checking</h2>
<p>Runtime dependencies are <code>@noble/ed25519</code>, <code>@hpcc-js/wasm-zstd</code>, and, for the opt-in <code>/seal</code> export only, <code>age-encryption</code>. The chunk store and the optional SQLite subpaths share pinned local WASM assets and do not load code from a CDN.</p>

<h2 id="versions">Supported versions</h2>
<p>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.</p>
<p>This project is pre-1.0. Only the latest published version gets fixes. Version 0.4.6 is current. Once 1.0 ships, the latest minor of the current major will be supported as well.</p>

<h2 id="reporting">Report a vulnerability</h2>
<p>Please do <strong>not</strong> open a public issue for a security problem. Report it privately in one of two ways:</p>
Expand Down
Loading