feat: distribution channels — JSR, browser/OPFS, CLI, binaries, Docker (issue #6)#8
Merged
Conversation
Move the node:fs dependency out of the kernel (core.ts) into a dedicated
adapter (adapter/node-fs.ts) so the kernel imports nothing from node:. The
default Node entry (index.ts) wires the node:fs adapter in as the default
filesystem, preserving open({ path }) durability; the new browser entry
(browser.ts, exposed as @libredb/libredb/browser via the exports map) carries
no default filesystem so its import graph is free of node: builtins.
A path-backed open with no filesystem now throws a clear error in the kernel.
Tests that exercise real-disk durability open through the Node entry; a new
browser.test.ts pins both the runtime behaviour and the no-node-imports
guarantee via a static import-graph walk.
Phase 0 of issue #6 (distribution channels).
Add a jsr.json manifest (source-based exports for the default and ./browser entries) and a JSR publish job in the release workflow, authenticated via OIDC after the npm publish succeeds. sync-version.ts now keeps jsr.json's version in step with package.json (single source of truth), backstopped by a test. README gains an 'Install elsewhere' section covering JSR, pinned CDN imports, and the browser entry. Phase 1 of issue #6 (distribution channels).
There was a problem hiding this comment.
Pull request overview
Phase 0 of “distribution channels” (#6): refactors LibreDB so the kernel is runtime-agnostic (no node: imports), while preserving Node/Bun durability ergonomics via a Node-specific entry wrapper and adding a dedicated browser entry point.
Changes:
- Move the default
node:fsfilesystem adapter out ofsrc/core.tsintosrc/adapter/node-fs.ts, and make the kernel require an explicit filesystem for path-backed opens. - Add a browser entry (
src/browser.ts) and tests that pin both behavior and the “nonode:builtins in the browser import graph” guarantee. - Update package exports / size limits and adjust tests to import
openthrough the Node entry when they need real-path persistence.
Reviewed changes
Copilot reviewed 23 out of 23 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/core.ts | Removes node:fs dependency from the kernel; adds explicit runtime errors for empty path and missing fs when path is provided. |
| src/adapter/node-fs.ts | Introduces the Node/Bun FileSystem adapter previously embedded in the kernel. |
| src/index.ts | Wraps kernel open to default fs to nodeFileSystem() when path is provided and fs is omitted (Node/Bun ergonomics). |
| src/browser.ts | Adds a browser-safe entry that re-exports the kernel open (no default filesystem). |
| src/browser.test.ts | Adds tests for browser entry behavior and an import-graph walk asserting no transitive node: builtins. |
| src/index.test.ts | Adds tests pinning Node entry semantics (default fs, in-memory open, empty-path error, injected fs passthrough). |
| src/core.fs.test.ts | Adds tests asserting kernel throws on open({ path }) without fs, and on empty path. |
| src/core.kv.test.ts | Switches durability tests to import open via the Node entry. |
| src/core.recovery.test.ts | Switches recovery tests to import open via the Node entry. |
| src/lens/catalog.test.ts | Switches persistence tests to import open via the Node entry. |
| src/lens/document.test.ts | Switches persistence tests to import open via the Node entry. |
| src/lens/kv.test.ts | Switches persistence tests to import open via the Node entry. |
| src/lens/relational.test.ts | Switches persistence tests to import open via the Node entry. |
| package.json | Adds exports entries/conditions for ./browser and browser condition on .. |
| knip.json | Registers src/browser.ts as an entry for knip. |
| .size-limit.json | Adds a size-limit target for dist/browser.js. |
| docs/superpowers/specs/2026-06-29-distribution-channels-design.md | Adds the research/design document for distribution channels and Phase 0 rationale. |
| .changeset/distribution-browser-entry.md | Adds a minor changeset documenting the new browser entry and kernel refactor. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Introduce a zero-dependency `libredb` bin (node:util parseArgs) built on the public API. run(argv, io) is the whole CLI as a pure function returning an exit code, so every command and error path is unit-tested; main.ts is a thin process shim (excluded from coverage, validated by an end-to-end smoke test). Read commands open through a read-only filesystem adapter so inspection never mutates the file: append/fsync refuse and truncate is a no-op, so a crash-torn tail is recovered in memory while the bytes on disk are left untouched. Phase 2a of issue #6 (distribution channels).
…ory lock Extend the CLI with set, delete, and import. import bulk-sets keys from a JSON object in a single transaction so a load is atomic — a crash mid-write lands nothing. All writes acquire an advisory <path>.lock first (LibreDB is single-process with no file locking), turning a concurrent writer into a loud error instead of corruption; --force overrides a stale lock. README documents the full CLI. Phase 2b of issue #6 (distribution channels).
tssecurity:S8707 flags CLI-argument-derived paths reaching the filesystem as a path-escape vulnerability. That is by design here: LibreDB is an embedded, single-process database and `libredb` is its local CLI, both built to open the path their caller names (like sqlite3/cat). There is no privilege boundary to cross, and confining to a base directory would defeat the tool. Scoped to src/adapter and src/cli only.
Add a release job that cross-compiles the CLI with `bun build --compile` for linux/macOS/windows (x64 + arm64) on one runner, attaching each self-contained executable and its .sha256 to the GitHub Release. Not shipped to npm/JSR. Adds a `bun run compile` script for local native builds, README download notes, and gitignore entries for compiled output. Phase 3 of issue #6 (distribution channels).
Add a Dockerfile that compiles the CLI with bun and copies the self-contained binary into a distroless/cc runtime, plus a release job that builds it multi-arch (linux/amd64 + linux/arm64) and pushes to ghcr.io/<owner>/libredb tagged with the version and latest. The image is a CLI shell, not a server — LibreDB stays embedded. Verified locally: build + volume-mounted set/get/stats roundtrip. README documents the docker run usage. Phase 4 of issue #6 (distribution channels).
A FileSystemSyncAccessHandle exposes synchronous read/write/getSize/truncate/ flush/close, which map onto the kernel's synchronous filesystem seam, so a LibreDB database can be durable in the browser with no async core. opfsFileSystem wraps an already-open handle (async acquisition is the caller's, keeping open() synchronous) and is exported from the browser entry. A locally-declared SyncAccessHandle type names the handle shape, so the package needs no DOM lib types and the adapter is fully unit-tested against an in-memory fake (write, reopen-recovery, torn-tail truncate). Phase 5 of issue #6 (distribution channels).
- opfs: loop append() until every byte is written (a short write() return must not persist a torn record while fsync reports success), mirroring node-fs; add a short-write test. - publish.yml: gate binaries and docker behind needs:publish and guard every job with if:!prerelease, so a failed gate or a pre-release cannot ship a split or latest-tagged release. - Dockerfile: pin oven/bun:1.3.14 (match .bun-version) and copy only src/. - Docs: drop stale 'future OPFS adapter' wording (it shipped); reconcile the design doc's CLI section with what shipped (no repl, stats by kind, <path>.lock).
The docker job now logs into both GHCR and Docker Hub and pushes the same multi-arch build to both (docker.io/<DOCKER_HUB_USERNAME>/libredb :version + :latest) in one buildx run. Bump to 0.1.1 to exercise the full release pipeline end-to-end with the added registry.
The Docker Hub username is a repository variable (not a secret), so the login step read an empty username and failed. Reference vars.DOCKER_HUB_USERNAME instead. Bump to 0.1.2 to re-run the full pipeline (0.1.1 already published npm/ JSR but its Docker push never ran).
- lock.ts: acquireLock now treats only EEXIST as 'locked' and rethrows other IO errors (missing dir, permissions) with the original attached as cause, so real failures are not misreported; add a test for the surfaced-error path. - core.ts: reword OpenOptions.fs doc — fs is optional at the type level but required at runtime for a path-backed open (kernel/browser throw; Node defaults). - design spec: label the node:fs-coupling bullet as the pre-Phase-0 baseline. - .dockerignore: correct the comment (only src/ is copied into the build). Addresses Copilot PR review comments.
Add BrowserOpenOptions (a union making fs mandatory whenever path is present) and type the browser entry's open with it, so a path-backed open without a filesystem is a compile error for TS users instead of only a runtime throw. The kernel is unchanged: assigning its wider-typed open here is sound by parameter contravariance (BrowserOpenOptions is a subtype of OpenOptions). A @ts-expect-error test locks the guarantee; the runtime guard still protects JS callers. Addresses the browser-entry Copilot review comment.
- run.ts: dispatch via a Map so an inherited property name (toString, __proto__) can never resolve to a handler; malformed import JSON now returns the usage exit code 2 instead of falling through to 1. - lock.ts: close the lock fd in a finally so a failed sentinel write cannot leak the descriptor. - publish.yml: rewrite the node -p version reads to single-quote form (no nested double quotes) — equivalent, and stops the recurring false-positive flag. Adds tests for the prototype-property and malformed-JSON cases.
Address the external PR review's two actionable polish items: - sync-version.ts now rewrites the esm.sh pin in README.md to the current version, so the CDN example never drifts behind a release (single source of truth, like core.ts and jsr.json). - publish.yml header documents the Docker Hub one-time setup (DOCKER_HUB_USERNAME variable + DOCKER_HUB_TOKEN secret) and why an unset username fails fast. The review's other points were already resolved (publish.yml quoting, browser options type) or are intentional (single whole-file read on recovery).
- run.ts: the delete command now guards reserved keys like set/import, so the CLI cannot remove a catalog/internal key and corrupt the layout; add a test. - README: clarify that importing the main entry in a browser gives Node types unless TS is configured for the browser condition — use the /browser subpath for the fs-required-with-path typing. Addresses Copilot review comments.
The browser open is typed with BrowserOpenOptions (fs required when a path is given), so re-exporting the kernel's permissive OpenOptions from this entry advertised a contract it does not honor. Drop it; keep Database/FileSystem/ WalFile, which a browser consumer needs to inject a custom filesystem. Tightens the entry's type surface to its real API.
- dropOwnLock: use existsSync for the 'no lock' case so a non-ENOENT read error (e.g. EACCES) propagates instead of being silently swallowed. - treat an empty <path>.lock as ours and removable, so a stray left by a failed sentinel write (ENOSPC/EIO) is recoverable via --force instead of wedging future writes; a non-empty non-sentinel file is still refused (protects unrelated user data). Add a test for the empty-stray case.
Security-review (supply-chain) follow-ups: - Dockerfile: pin oven/bun:1.3.14 and gcr.io/distroless/cc-debian12 by digest. Both tags are mutable (cc-debian12 is rolling), so the digests make the image reproducible and resistant to an upstream tag override. Verified the build locally with the pinned digests. - README: note that --force is only for clearing a stale lock from a crashed writer, since the advisory lock cannot stop two simultaneous forcing writers. (Other review items are accepted design decisions or maintainer-side: npm --provenance lands when the repo is public; Docker Hub token scope is a registry setting.)
The badge row covered only npm; add JSR and Docker Hub (Docker-logo) badges so all three registries this project publishes to are visible, and link the Docker Hub and GHCR image pages from the container section.
bunx jsr can resolve 'jsr' to the repo-root jsr.json on some Bun versions
("Cannot run jsr.json"), which would fail the JSR job at release time after npm
already published. Switch the job to Node + `npx jsr publish` — JSR's canonical
invocation, robust across environments — and drop setup-bun (npx needs only
Node). OIDC (id-token) is unchanged.
The browser/CLI/OPFS features already shipped publicly in the 0.1.0-0.1.2 test releases, so the stable release is a patch (0.1.3) finalizing them — keeps Studio in the ^0.1.x range and avoids colliding with the burned versions on npm/JSR. changeset:version consumed the three changesets, wrote CHANGELOG.md, and synced the version across core.ts, jsr.json, and the README CDN pin.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Implements issue #6 (distribution channels) as a phased, autonomously-built PR.
Each phase was developed with TDD, gated locally, pushed, and only advanced once
all of CI (gate, SonarCloud quality gate, CodeQL) was green. Ready for final
human review — please do not expect it pre-merged.
Design:
docs/superpowers/specs/2026-06-29-distribution-channels-design.md.Phases (all CI-green)
node:fsout of thekernel into
src/adapter/node-fs.ts; kernel now imports nothing fromnode:.New
@libredb/libredb/browserentry (in-memory; injected fs for persistence)via the
browserexport condition. ABun.Transpilerimport-graph test provesthe browser entry pulls in no
node:builtins. (changeset: minor)jsr.json(source exports), a JSR publish job(OIDC),
sync-version.tskeeps jsr.json in step (test-backstopped), README"Install elsewhere". Verified with
jsr publish --dry-run.npx libredb). Read:inspect/stats/get/scan(read-only fsadapter — inspection never mutates the file). Write:
set/delete/import(advisory
<path>.lockwith sentinel;--force; reserved-key guard; atomicbulk import). Zero-dep
parseArgs; bin shim excluded from coverage + e2e smoketest. (changeset: minor)
bun build --compilecross-compile matrix(linux/macOS/windows, x64+arm64) → GitHub Release +
.sha256.bun run compilefor local builds. Verified native compile locally.
server. Verified locally: build + volume-mounted set/get/stats roundtrip.
opfsFileSystemmaps the synchronous kernelfs seam onto a
FileSystemSyncAccessHandle(no async core); exported from thebrowser entry; a local
SyncAccessHandletype means no DOM-lib dependency.100% tested against an in-memory fake. (changeset: minor)
Verification
bun run gategreen (261 tests, 100% line/function/statement coverage).publintclean;attw --profile esm-onlygreen for.and./browser.whole-PR pass); all real findings fixed (notably: OPFS short-write loop, CLI
reserved-key guard + lock sentinel, release-workflow gating + prerelease guard).
with rationale — opening a caller-named path is by design for an embedded DB).
Maintainer prerequisites before the first release using this
@libredbscope +libredbpackage on jsr.io and link thisrepo (enables the OIDC publish).
GITHUB_TOKENcovers GHCR and Release uploads.latest+ Docker:latest;mark a release as a pre-release to skip all publishing.