From 088e3cdabe65b3cd7bc915a1373bb206d06a15a8 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Tue, 11 Aug 2026 20:40:04 -0400 Subject: [PATCH] standalone: ship the headless engine as one bundleable ESM file A no-build static site couldn't vendor BitGate the way it vendors a single dependency: bare @bitgate/* specifiers don't resolve in a browser, and the headless path spans two packages (the evaluator in core, the kind-1985 label codec in nostr). The widget bundle solved this for the DOM path only. Add @bitgate/standalone (packages/standalone): an entry re-exporting core + nostr, esbuilt into a single self-contained dist/bitgate.js (~44 KB). It's the same canonical code the npm packages ship, with specifiers inlined, and dependency-free -- @bitgate/verify's @noble deps are intentionally not pulled in, so the bundle carries no third-party code. Wired into `npm run build` via build:standalone. Verified: no bare imports in the output (self-contained), and a smoke test composes a full label -> decision flow from the single entry. README Packages table and headless section document it. Addresses the "standalone ESM single-file" item of the Tessera integration feedback (docs/TODO.md). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01BXQCvJPA6sVSQ5j8uBd9bB --- README.md | 6 ++ docs/TODO.md | 12 +++- package.json | 3 +- packages/standalone/build.mjs | 48 ++++++++++++++ packages/standalone/package.json | 19 ++++++ packages/standalone/src/index.js | 15 +++++ packages/standalone/test/standalone.test.js | 70 +++++++++++++++++++++ 7 files changed, 169 insertions(+), 4 deletions(-) create mode 100644 packages/standalone/build.mjs create mode 100644 packages/standalone/package.json create mode 100644 packages/standalone/src/index.js create mode 100644 packages/standalone/test/standalone.test.js diff --git a/README.md b/README.md index 45563ef..31b11c5 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ equivalent to following the operator's seed list. | `@bitgate/verify` | Optional NIP-01 signature verification, so the core stays dependency-free. | | `@bitgate/testing` | Conformance harness. | | `@bitgate/widget` | Drop-in custom elements: viewer controls and a moderator console. | +| `@bitgate/standalone` | The headless engine + Nostr codecs bundled into one self-contained ESM file for no-build static sites. | | `@bitgate/bitvid-compat` | Reference application profile for the characterization corpus. | ## Quick start — a static page @@ -155,6 +156,11 @@ checkout while `visibility` still shows the listing with a warning — something is a runnable end-to-end walkthrough (`node examples/headless-quickstart.mjs`); [docs/labels.md](docs/labels.md) covers the NIP-32 wire format and interop. +No bundler? `npm run build:standalone` produces `packages/standalone/dist/bitgate.js` +— the engine and the Nostr codecs above in one self-contained, dependency-free +ESM file. Self-host it and `import { evaluateTarget, decodeLabels } from "./bitgate.js"`; +everything in this section works from that single file. + ## Extending to a new application Write an adapter saying what your object is in governance terms, and a policy diff --git a/docs/TODO.md b/docs/TODO.md index df18e31..88b2031 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -273,8 +273,14 @@ adoption the obvious choice. ### Medium -- Ship `@bitgate/core` as a standalone ESM single-file (like BitFeed's - `standalone`) so a no-build static site can vendor it the way we vendor - `@tessera/core`. Dependency-free + no build step was a hard requirement for us. +- **[DONE 2026-08-11]** Ship a standalone ESM single-file. **Shipped** + `@bitgate/standalone` (`packages/standalone/`, `npm run build:standalone` → + `dist/bitgate.js`, ~44 KB): the headless engine (core) + the Nostr codecs + (nostr, incl. the kind-1985 label round-trip) esbuilt into one self-contained, + dependency-free ESM file — `@bitgate/verify`'s @noble deps are deliberately + not pulled in. A no-build static site vendors the single file and gets the + whole headless path. Verified dependency-free (no bare imports in the output) + and end-to-end (a smoke test composes a decision from the entry). Documented + in the README Packages table and the headless section; wired into `npm run build`. - A worked example of BitGate feeding a feed ranking (the `ranking` dimension → a score multiplier) would have saved us reinventing it in the feed recipe. diff --git a/package.json b/package.json index 4ddb5a8..653e427 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "check:references": "node scripts/verify-no-reference-imports.mjs", "check:types": "tsc -p tsconfig.json --noEmit && tsc -p packages/widget/tsconfig.json --noEmit", "build:widget": "node packages/widget/build.mjs", - "build": "npm run build:types && npm run build:widget && npm run build:site", + "build:standalone": "node packages/standalone/build.mjs", + "build": "npm run build:types && npm run build:widget && npm run build:standalone && npm run build:site", "build:site": "node packages/site/build.mjs", "dev:site": "node packages/site/build.mjs && node packages/site/serve.mjs" }, diff --git a/packages/standalone/build.mjs b/packages/standalone/build.mjs new file mode 100644 index 0000000..58a3bb9 --- /dev/null +++ b/packages/standalone/build.mjs @@ -0,0 +1,48 @@ +#!/usr/bin/env node +/** + * Bundle the headless engine + Nostr codecs into a single self-contained ESM + * file for no-build static sites. + * + * The output (`dist/bitgate.js`) is the same canonical code the npm packages + * ship — not a second implementation — with the bare `@bitgate/*` specifiers + * resolved and inlined. Consumers copy it onto their own origin and import it + * with a single relative path; they never fetch it from someone else's + * deployment. + * + * Usage: node packages/standalone/build.mjs [--watch] + */ +import { build, context } from "esbuild"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const HERE = dirname(fileURLToPath(import.meta.url)); + +/** @type {import("esbuild").BuildOptions} */ +const options = { + entryPoints: [join(HERE, "src", "index.js")], + outfile: join(HERE, "dist", "bitgate.js"), + bundle: true, + format: "esm", + platform: "browser", + target: ["es2022"], + sourcemap: true, + minify: true, + legalComments: "inline", + banner: { + js: "/*! BitGate (headless standalone) — GPL-3.0-or-later — https://github.com/PR0M3TH3AN/Nostr-Governance */", + }, +}; + +const watch = process.argv.includes("--watch"); + +if (watch) { + const ctx = await context(options); + await ctx.watch(); + console.log("bitgate standalone: watching…"); +} else { + const result = await build(options); + if (result.errors.length) { + process.exit(1); + } + console.log(`✓ Built ${options.outfile}`); +} diff --git a/packages/standalone/package.json b/packages/standalone/package.json new file mode 100644 index 0000000..1a38c46 --- /dev/null +++ b/packages/standalone/package.json @@ -0,0 +1,19 @@ +{ + "name": "@bitgate/standalone", + "version": "0.1.0", + "private": true, + "type": "module", + "license": "GPL-3.0-or-later", + "description": "Headless BitGate engine + Nostr codecs bundled into a single self-contained ESM file for no-build static sites.", + "main": "src/index.js", + "scripts": { + "build": "node build.mjs" + }, + "dependencies": { + "@bitgate/core": "*", + "@bitgate/nostr": "*" + }, + "devDependencies": { + "esbuild": "^0.28.2" + } +} diff --git a/packages/standalone/src/index.js b/packages/standalone/src/index.js new file mode 100644 index 0000000..1bebdb3 --- /dev/null +++ b/packages/standalone/src/index.js @@ -0,0 +1,15 @@ +// @bitgate/standalone — the headless engine and the Nostr codecs it consumes, +// as one bundleable entry. +// +// Bare specifiers (`@bitgate/core`) don't resolve in a browser, and the headless +// path needs two packages (the evaluator in core, the kind-1985 label codec in +// nostr). build.mjs esbuilds this entry into a single self-contained ESM file a +// no-build static site can vendor the way it would vendor any one dependency — +// the requirement the widget bundle already meets for the DOM path, met here for +// the headless path. +// +// Both re-exported packages are dependency-free (@bitgate/verify, the only +// package with third-party deps, is intentionally not pulled in), so the bundle +// carries no external code. +export * from "@bitgate/core"; +export * from "@bitgate/nostr"; diff --git a/packages/standalone/test/standalone.test.js b/packages/standalone/test/standalone.test.js new file mode 100644 index 0000000..019725a --- /dev/null +++ b/packages/standalone/test/standalone.test.js @@ -0,0 +1,70 @@ +// The standalone entry must surface the whole headless path from one import — +// engine (core) + label codec (nostr) — so a no-build site gets everything from +// the single bundled file. Tests the entry, not the built artifact (dist is +// gitignored); the build itself is exercised by `npm run build:standalone`. +import { describe, expect, it } from "vitest"; + +import * as bitgate from "../src/index.js"; + +describe("standalone entry surface", () => { + it("re-exports the core engine and the Nostr label codec together", () => { + for (const name of [ + // core engine + "evaluateTarget", + "createSnapshot", + "reduceAdminState", + "createAuthorityState", + "createPolicyDefinition", + "createEventTarget", + "ALLOWLIST_POLICY", + // nostr label codec + "decodeLabels", + "encodeLabel", + "labelsToContributions", + "LABEL_KIND", + ]) { + expect(bitgate[name], name).toBeDefined(); + } + }); + + it("composes a headless decision end to end from the single entry", () => { + const ROOT = "a1".repeat(32); + const MOD = "b2".repeat(32); + const PLUGIN = "e5".repeat(32); + const NOW = 1_750_000_000; + + const target = bitgate.createEventTarget(PLUGIN); + expect(target).not.toBeNull(); + + const labelEvent = { + ...bitgate.encodeLabel({ value: "deny", namespace: "ns", targets: [/** @type {any} */ (target)] }), + pubkey: MOD, + created_at: NOW, + id: "0".repeat(64), + sig: "", + }; + + const contributions = bitgate.labelsToContributions(bitgate.decodeLabels(labelEvent), { + namespace: "ns", + denyValues: ["deny"], + }); + const authority = bitgate.createAuthorityState({ root: ROOT, actors: { [MOD]: ["moderator"] } }); + const snapshot = bitgate.createSnapshot({ authority, admin: bitgate.reduceAdminState(contributions, authority) }); + + const policy = bitgate.createPolicyDefinition({ + id: "p", + version: "1.0.0", + defaultProfile: "d", + profiles: { d: { name: "d", administrativeDeny: { visibility: "warn", transaction: "deny" }, reports: {}, mutes: {} } }, + }); + + const decision = bitgate.evaluateTarget(/** @type {any} */ (target), snapshot, { + surface: "d", + policyProfile: "d", + policy, + now: NOW, + }); + + expect(decision.transaction?.effect).toBe("deny"); + }); +});