diff --git a/.flow/config.json b/.flow/config.json new file mode 100644 index 0000000..3b584a9 --- /dev/null +++ b/.flow/config.json @@ -0,0 +1,5 @@ +{ + "memory": { + "enabled": false + } +} diff --git a/.flow/epics/fn-1.json b/.flow/epics/fn-1.json new file mode 100644 index 0000000..9e1e01d --- /dev/null +++ b/.flow/epics/fn-1.json @@ -0,0 +1,13 @@ +{ + "branch_name": "fn-152-c1", + "created_at": "2026-06-08T22:04:26.033805Z", + "depends_on_epics": [], + "id": "fn-1", + "next_task": 1, + "plan_review_status": "unknown", + "plan_reviewed_at": null, + "spec_path": ".flow/specs/fn-1.md", + "status": "open", + "title": "Stream C \u2014 auths-verify-widget GTM: resolver-doc truth + embed/SRI/playground", + "updated_at": "2026-06-08T22:36:13.268099Z" +} diff --git a/.flow/meta.json b/.flow/meta.json new file mode 100644 index 0000000..3c4f647 --- /dev/null +++ b/.flow/meta.json @@ -0,0 +1,4 @@ +{ + "next_epic": 1, + "schema_version": 2 +} diff --git a/.flow/specs/fn-1.md b/.flow/specs/fn-1.md new file mode 100644 index 0000000..88b742f --- /dev/null +++ b/.flow/specs/fn-1.md @@ -0,0 +1,95 @@ +# Stream C — `auths-verify-widget` GTM + +> Source: `roadmap/go_to_market/go_to_market.md` → "Stream C — `auths-verify-widget` (the most launch-ready artifact)". +> Roadmap task IDs preserved for traceability: **G0.5 / G4.1 (D-5)**, **G4.4**, **G4.5**. + +## Overview + +`@auths-dev/verify@0.3.0` is the most launch-ready artifact in the GTM plan: live on unpkg *and* jsDelivr, WASM base64-inlined into a single file so the one-line ` +``` +No version pin (implicit `@latest`), no `integrity=`, no `crossorigin`. The full build base64-inlines WASM into the single `dist/auths-verify.mjs` (671 KB) — so **one sha384 covers the entire runtime**, the SRI-friendly path. The slim build (`dist/slim/`) is ambiguous: no separate `.wasm` is emitted and it's the same size as full (see Investigation). + +## Changes + +1. **Investigation (do first):** determine what the slim build actually ships for WASM (`package.json:25` `build:slim` runs `vite build --mode slim` *without* `INLINE_WASM`; `vite.config.ts:7-32` inline plugin; `verifier-bridge.ts:11,22-44` sentinel + `wasm-url` override). Confirm whether slim fetches a separate `.wasm` at runtime. Record the finding in the task notes. +2. **README CDN guidance** — replace the bare snippet with a **pinned + SRI** one: + ```html + + ``` + - Add a short subsection explaining: pin an exact version (SRI + `@latest` breaks on every release); `crossorigin="anonymous"` is **required** or the browser blocks the cross-origin script; jsDelivr equivalent + that it can show the SRI hash for you. + - State the **full-vs-slim** SRI truth from the investigation: recommend the **full** inlined bundle for SRI users (one hash = everything); document slim honestly (if it fetches a sibling `.wasm`, the ` +``` + +> **Pin a version with SRI — never `@latest`.** The `integrity` hash is +> byte-exact, so it only validates against an immutable, version-pinned URL +> (`@0.3.0`). A moving `@latest` URL changes bytes on every release and would +> break the hash — and the badge — without warning. `crossorigin="anonymous"` +> is required: without it the browser fetches the cross-origin script opaquely, +> can't read it to verify, and the load fails. The hash above is for `@0.3.0`; +> regenerate it for any other version with `npm run sri`, or copy it from the +> file's page on [jsDelivr](https://www.jsdelivr.com/), which displays the SRI +> hash for you. The WASM verifier is base64-inlined into this single `.mjs`, so +> one `integrity` hash covers the entire runtime — there is no separate `.wasm` +> fetch to protect. + +[jsDelivr](https://www.jsdelivr.com/) serves the same published file, so the same pin + `integrity` works there too: + ```html - + ``` **npm (for bundlers):** @@ -33,15 +63,17 @@ Add the widget to any page and point it at a repository: That's it. The widget will: -1. Call the GitHub API to read the repository's `refs/auths/` identity data -2. Extract the public key from the identity's `did:key` +1. Fetch the repository's latest GitHub Release and locate the `*.auths.json` attestation asset +2. Read the verification key (Ed25519 or P-256) from the attestation 3. Load the WASM verification engine -4. Cryptographically verify the full attestation chain +4. Cryptographically verify the attestation chain 5. Display a badge showing the result (Verified, Invalid, Expired, etc.) -**Prerequisite:** The repository owner must have set up an Auths identity with [`auths init`](https://github.com/auths-dev/auths). If the repo doesn't have Auths identity data, the widget will show an error. +**Prerequisite:** The repository owner must have published an Auths attestation as a `*.auths.json` asset on a GitHub Release (Gitea repos expose it via `refs/auths/` instead). If the repo has no Auths attestation, the widget will show an error. -**Supported forges:** GitHub and Gitea (including self-hosted). GitLab is not supported for auto-resolve because its API does not expose custom Git refs — use manual mode instead. +**Supported forges:** GitHub (via Release assets) and Gitea (via Git refs, including self-hosted). GitLab is not supported for auto-resolve because its API does not expose custom Git refs — use manual mode instead. + +> **Build your embed in the browser:** the [Embed Builder](https://auths-dev.github.io/auths-verify-widget/) lets you paste a repo URL, preview the live badge, and copy a version-pinned, SRI-protected snippet. ## Display Modes @@ -113,7 +145,7 @@ Or for a full chain: |---|---|---| | `attestation` | JSON string | Single attestation to verify | | `attestations` | JSON array string | Chain of attestations to verify | -| `public-key` | hex string | Root/issuer Ed25519 public key (64 hex chars) | +| `public-key` | hex string | Root/issuer public key — Ed25519 (64 hex chars) or P-256 (66 hex chars, compressed) | ## JavaScript API @@ -160,13 +192,14 @@ Available properties: `--auths-{state}-bg`, `--auths-{state}-fg`, `--auths-{stat When you set `repo="https://github.com/user/repo"`: -1. The widget parses the URL and detects the forge (GitHub, Gitea, or GitLab) -2. It calls the forge's REST API to list Git refs under `refs/auths/` -3. It reads `identity.json` from `refs/auths/identity` to get the controller DID -4. It extracts the Ed25519 public key from the `did:key:z...` identifier (pure TypeScript, no WASM needed) -5. It reads `attestation.json` from each device ref under `refs/auths/devices/nodes/` -6. It loads the WASM verification engine and cryptographically verifies the attestation chain -7. It renders the result as a badge +1. The widget parses the URL and detects the forge (GitHub, Gitea, or GitLab). +2. It resolves the attestation data — the mechanism depends on the forge: + - **GitHub:** fetches the repository's latest Release (`/repos/{owner}/{repo}/releases/latest`), finds the `*.auths.json` asset, and downloads it (via the Contents API, falling back to the Release asset API). + - **Gitea:** reads the auths data from Git refs under `refs/auths/` via the Gitea REST API. + - **GitLab:** not supported for auto-resolve (its REST API does not expose the required data) — use manual mode. +3. It derives the verification key (Ed25519 or P-256): for GitHub, the device public key carried in the attestation; for Gitea, extracted from the controller's `did:key`/CESR identifier (pure TypeScript, no WASM needed). +4. It loads the WASM verification engine and cryptographically verifies the attestation chain. +5. It renders the result as a badge. The resolver layer uses dynamic imports — if you only use manual `attestation`/`public-key` attributes, the resolver code is never loaded (zero bundle size impact). @@ -222,8 +255,10 @@ npm run build ``` Outputs: -- `dist/auths-verify.mjs` — single file with WASM base64-inlined -- `dist/slim/auths-verify.mjs` — smaller JS, loads `.wasm` separately +- `dist/auths-verify.mjs` — single self-contained file with the WASM base64-inlined. Recommended for CDN + SRI: one file, one `integrity` hash covers the whole runtime. +- `dist/slim/auths-verify.mjs` — the `./slim` export. Note: it currently also inlines the WASM (via `vite-plugin-wasm`), so it is the same size as the full bundle and shares the same single-hash SRI story; there is no separate `.wasm` to fetch today. + +Generate the SRI hash for the built bundle(s) with `npm run sri`. ## License diff --git a/examples/auto-resolve.html b/examples/auto-resolve.html index 598b9d7..ee4f2e4 100644 --- a/examples/auto-resolve.html +++ b/examples/auto-resolve.html @@ -20,7 +20,8 @@

Auto-Resolve from Repository

Instead of supplying raw attestation JSON and a public key hex string, just point the widget at a repository URL. The widget automatically - fetches identity and attestation data from the forge's Git refs. + fetches the attestation data from the forge — a *.auths.json + asset on the latest GitHub Release, or Git refs on Gitea.

GitHub Repository

diff --git a/examples/embed-builder.html b/examples/embed-builder.html new file mode 100644 index 0000000..427c2ce --- /dev/null +++ b/examples/embed-builder.html @@ -0,0 +1,220 @@ + + + + + + auths-verify — Embed Builder + + + +

Embed Builder

+

+ Paste a repository URL and copy a ready-to-use <auths-verify> + embed — version-pinned and integrity-protected. Works with a full URL + (https://github.com/owner/repo) or the owner/repo + shorthand. +

+ +
+
+ + +
+
+ + +
+
+ + +
+

Embed snippet

+ + + + +
+
+ +

Live preview

+

+ Renders the badge from the pinned CDN bundle in a sandboxed frame. A repo + shows Verified only once it has published a + *.auths.json asset on a GitHub Release; otherwise the badge + reports an error. Unauthenticated GitHub API calls are limited to ~60/hour + per IP, so the preview refreshes a moment after you stop typing. +

+ + +

+ The integrity hash is byte-exact and pinned to a specific + version. Regenerate it with npm run sri when bumping versions, + or copy it from the file's page on + jsDelivr. +

+ + + + diff --git a/examples/embed-snippet.ts b/examples/embed-snippet.ts new file mode 100644 index 0000000..a124323 --- /dev/null +++ b/examples/embed-snippet.ts @@ -0,0 +1,87 @@ +/** + * Pure embed-snippet builder for the Embed Builder page. + * + * Reuses `detectForge` (the single source of truth for repo-URL normalization) + * so the generated snippet round-trips through the exact same parser the widget + * uses at runtime. No URL parsing is re-implemented here. + */ +import { detectForge } from '../src/resolvers/detect'; +import type { ForgeConfig } from '../src/resolvers/types'; + +// Keep VERSION + SRI in sync with the README CDN snippet and `npm run sri`. +export const VERSION = '0.3.0'; +export const SRI = 'sha384-M1UJQ02k36YqkLbXIPrV98mCZKA7pm3J2TX5PNGwi+ZJwjcJC2CoKN8dCJZpe0+l'; + +export const cdnUrl = (version = VERSION): string => + `https://unpkg.com/@auths-dev/verify@${version}/dist/auths-verify.mjs`; + +/** Reconstruct the canonical web URL (full form) from a ForgeConfig. */ +export function canonicalRepoUrl(cfg: ForgeConfig): string { + // GitHub's baseUrl is api.github.com; its web origin is github.com. + const origin = cfg.type === 'github' ? 'https://github.com' : cfg.baseUrl; + return `${origin}/${cfg.owner}/${cfg.repo}`; +} + +export type BuildResult = + | { ok: true; snippet: string; repo: string; forge: ForgeConfig['type']; forgeAttr: string } + | { ok: false; error: string }; + +/** + * Build the copy-paste embed (pinned + SRI `\n\n` + + ``; + + return { ok: true, snippet, repo, forge: cfg.type, forgeAttr }; +} + +/** + * Build a self-contained document that renders a live badge for `repo` from the + * pinned CDN bundle. Intended for `iframe.srcdoc` with `sandbox="allow-scripts"` + * so the demo runs but cannot script the parent page. + * + * The `integrity` attribute is intentionally omitted here so the preview always + * loads the published bytes (the copyable snippet from `buildEmbed` keeps SRI). + * SRI only blocks *tampered* bytes, so the rendered result is identical either + * way. + */ +export function previewDocument(repo: string, forgeAttr = ''): string { + return [ + '', + '', + '', + '', + ``, + ``, + '', + ].join('\n'); +} diff --git a/examples/index.html b/examples/index.html index f0e41d0..b1552d0 100644 --- a/examples/index.html +++ b/examples/index.html @@ -16,6 +16,8 @@

auths-verify Examples