All three entrypoints (mod.ts, ./cli, ./testing) carry module docs with runnable examples; every exported symbol and interface member across the public surface is documented. Package description updated (mirrored to the GitHub repo description; JSR-side setting pasted separately). Version 0.1.1 + regenerated release artifacts manifest.
- commit
- f377245b06d797642b061bdf18fc608dda4b9407
- parent
- 26f137e2a53835274452939b558deff19d5a9214
- author
- Dami <dami.etoile@gmail.com>
| M | cli.ts | 22 | +++++++++++++++++----- |
| M | deno.json | 4 | ++-- |
| M | mod.ts | 28 | ++++++++++++++++++++++++++-- |
| M | src/ | 4 | ++++ |
| M | src/ | 17 | +++++++++++++++++ |
| M | src/ | 5 | +++++ |
| M | src/ | 4 | ++-- |
| M | src/ | 15 | +++++++++++++++ |
| M | testing/ | 26 | +++++++++++++++++++++++--- |
@@ -1,8 +1,20 @@
1-// lofi-node CLI — init / start / pair / ticket / status.2-//3-// Ticket verbs write <dataDir>/tickets.json directly; a RUNNING daemon picks4-// changes up via the ticket store's mtime hot-reload, so issue/revoke take5-// effect without a restart and without IPC.1+/**2+ * The lofi-node CLI: `init`, `start`, `pair`, `ticket issue|list|revoke`,3+ * `status`.4+ *5+ * Run it straight from JSR:6+ *7+ * ```sh8+ * dx -A jsr:@nzip/lofi-node/cli init --port 48029+ * dx -A jsr:@nzip/lofi-node/cli start10+ * ```11+ *12+ * Ticket verbs write `<dataDir>/tickets.json` directly; a RUNNING daemon13+ * picks changes up via the ticket store's mtime hot-reload, so issue/revoke14+ * take effect without a restart and without IPC.15+ *16+ * @module17+ */618 719 import { initConfig, loadConfig, saveConfig, type StorageConfig } from "./src/config.ts";820 import { createSyncNode } from "./src/node.ts";@@ -1,8 +1,8 @@
11 {22 "name": "@nzip/lofi-node",3- "version": "0.1.0",3+ "version": "0.1.1",44 "license": "MIT",5- "description": "Self-hostable sync node for lofi apps: an embedded Jazz sync server with iroh node-to-node transport — pair homes by ticket, no static IPs.",5+ "description": "Self-hostable sync node for lofi apps: an embedded Jazz sync server with iroh peer-to-peer transport and ticket-gated access. Pair homes by ticket — no static IPs, no cloud — issue revocable app tickets, and choose where your data lives.",66 "exports": {77 ".": "./mod.ts",88 "./testing": "./testing/mod.ts",@@ -1,5 +1,29 @@
1-// @nzip/lofi-node — self-hostable Jazz sync node with iroh node-to-node2-// transport. Private prove-out; see README for status and roadmap.1+/**2+ * Self-hostable sync node for lofi apps: one daemon embedding a Jazz 2.03+ * sync server, iroh node-to-node transport, and a ticket-gated access layer.4+ *5+ * Create a node, issue an app-connect ticket, and point a lofi app's6+ * `JAZZ_SERVER_URL` at the ticket URL:7+ *8+ * ```ts9+ * import { createSyncNode } from "@nzip/lofi-node";10+ *11+ * const node = await createSyncNode({12+ * appId: crypto.randomUUID(),13+ * backendSecret: "...",14+ * adminSecret: "...",15+ * dataDir: "./data",16+ * access: "ticket",17+ * });18+ * const { ticket } = await node.issueTicket({ label: "phone" });19+ * // the app stores `ticket` passkey-encrypted and uses its URL as serverUrl20+ * ```21+ *22+ * The CLI lives at the `./cli` export (`dx -A jsr:@nzip/lofi-node/cli`), and23+ * in-process test helpers at `./testing`.24+ *25+ * @module26+ */327 428 export { createSyncNode } from "./src/node.ts";529 export type {@@ -32,6 +32,7 @@ export interface AppTicket {
3232 node?: string;3333 }3434 35+/** Persisted record of an issued ticket — digest only, never the secret. */3536 export interface AppTicketRecord {3637 id: string;3738 scope?: "sync" | "provision";@@ -47,15 +48,18 @@ function base64urlNoPad(bytes: Uint8Array): string {
4748 return btoa(bin).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");4849 }4950 51+/** Generate a fresh 256-bit ticket secret (43-char base64url). */5052 export function generateSecret(): string {5153 return base64urlNoPad(crypto.getRandomValues(new Uint8Array(32)));5254 }5355 56+/** SHA-256 hex digest of a ticket secret — what the node stores. */5457 export async function hashSecret(secret: string): Promise<string> {5558 const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(secret));5659 return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");5760 }5861 62+/** Serialize an app ticket to its `lofisync1.` string form. */5963 export function encodeAppTicket(ticket: AppTicket): string {6064 return TICKET_PREFIX + base64urlNoPad(new TextEncoder().encode(JSON.stringify(ticket)));6165 }@@ -3,6 +3,8 @@
33 // and persisted — the node's identity and its Jazz credentials both survive44 // restarts, so tickets and client sessions stay stable.55 6+/** Where this node replicates to: nowhere, a direct Jazz server URL (e.g.7+ * Jazz Cloud), or a peer lofi-node named by its iroh pairing ticket. */68 export type UpstreamConfig = "none" | { url: string } | { peer: string };79 810 /** Where node data lives. jazz-napi supports exactly these two today; the@@ -10,11 +12,18 @@ export type UpstreamConfig = "none" | { url: string } | { peer: string };
1012 * any mounted location (NAS, synced volume) — validated writable at boot. */1113 export type StorageConfig = { type: "sqlite"; path?: string } | { type: "memory" };1214 15+/** Persisted daemon configuration (`<dataDir>/config.json`, version 2 —16+ * v1 files migrate lazily on load). */1317 export interface NodeConfig {18+ /** Config schema version. */1419 v: 2;20+ /** Jazz app id (a UUID — Jazz's catalogue requires UUID ids). */1521 appId: string;22+ /** Jazz backend secret (server-to-server calls). */1623 backendSecret: string;24+ /** Jazz admin secret; in ticket mode it never leaves the node. */1725 adminSecret: string;26+ /** Fixed public port; auto-allocated per start when omitted. */1827 listenPort?: number;1928 /** "ticket": only issued app tickets reach Jazz (CLI init default).2029 * "open": today's behavior (library/test default). */@@ -28,6 +37,8 @@ export interface NodeConfig {
2837 2938 const SUPPORTED_STORAGE = ["sqlite", "memory"];3039 40+/** Throws with an actionable message for storage types jazz-napi cannot41+ * back today (`sqlite` and `memory` are the supported set). */3142 export function validateStorage(storage: StorageConfig): void {3243 if (!SUPPORTED_STORAGE.includes(storage.type)) {3344 throw new Error(@@ -56,10 +67,13 @@ function fromHex(text: string): Uint8Array {
5667 return out;5768 }5869 70+/** Path of the daemon config file inside a data directory. */5971 export function configPath(dataDir: string): string {6072 return `${dataDir}/config.json`;6173 }6274 75+/** Read config.json (null when absent); v1 files are mapped to v2 in76+ * memory — persisted as v2 on the next explicit {@link saveConfig}. */6377 export async function loadConfig(dataDir: string): Promise<NodeConfig | null> {6478 try {6579 const raw = await Deno.readTextFile(configPath(dataDir));@@ -88,11 +102,14 @@ export async function loadConfig(dataDir: string): Promise<NodeConfig | null> {
88102 }89103 }90104 105+/** Write config.json (creates the data directory when needed). */91106 export async function saveConfig(dataDir: string, config: NodeConfig): Promise<void> {92107 await Deno.mkdir(dataDir, { recursive: true });93108 await Deno.writeTextFile(configPath(dataDir), JSON.stringify(config, null, 2) + "\n");94109 }95110 111+/** Create (or return the existing) daemon config; new inits default to112+ * ticket-gated access and SQLite storage, with generated secrets. */96113 export async function initConfig(97114 dataDir: string,98115 overrides: Partial<@@ -1,10 +1,10 @@
11 // GENERATED by tools/release_artifacts.ts — do not edit by hand.22 // Release-asset names + sha256 digests for the native addon; the loader3-// downloads these from the GitHub release for version 0.1.0 and refuses3+// downloads these from the GitHub release for version 0.1.1 and refuses44 // anything that does not match. Regenerate: deno task release:artifacts55 66 export const RELEASE_ARTIFACTS = {7- "version": "0.1.0",7+ "version": "0.1.1",88 "assets": {99 "darwin-aarch64": {1010 "file": "libnumber0_iroh-aarch64-apple-darwin.dylib",@@ -22,9 +22,14 @@ import { type Gate, startGate } from "./gate.ts";
2222 import { type AppTicketRecord, AppTicketStore, encodeAppTicket } from "./appticket.ts";2323 import { MeshUnavailableError } from "./errors.ts";2424 25+/** Options for {@link createSyncNode}. */2526 export interface SyncNodeOptions {27+ /** Jazz app id (a UUID). */2628 appId: string;29+ /** Jazz backend secret. */2730 backendSecret: string;31+ /** Jazz admin secret; in ticket mode the gate injects it server-side and32+ * it never transits clients. */2833 adminSecret: string;2934 /** Omit (or set inMemory) for an ephemeral node — tests, throwaway relays. */3035 dataDir?: string;@@ -50,11 +55,14 @@ export interface SyncNodeOptions {
5055 irohLibPath?: string;5156 }5257 58+/** iroh mesh state: up (with pairing ticket + live tunnel stats), off, or59+ * unavailable with the precise reason (no silent degradation). */5360 export type MeshStatus =5461 | { state: "up"; nodeId: string; ticket: string; connections: TunnelConnStat[] }5562 | { state: "off" }5663 | { state: "unavailable"; reason: string };5764 65+/** Public view of an issued app-connect ticket (never the secret). */5866 export interface AppTicketInfo {5967 id: string;6068 scope: "sync" | "provision";@@ -63,6 +71,7 @@ export interface AppTicketInfo {
6371 revoked: boolean;6472 }6573 74+/** Snapshot returned by {@link SyncNode.status}. */6675 export interface SyncNodeStatus {6776 appId: string;6877 access: "open" | "ticket";@@ -72,6 +81,7 @@ export interface SyncNodeStatus {
7281 mesh: MeshStatus;7382 }7483 84+/** A running sync node — see {@link createSyncNode}. */7585 export interface SyncNode {7686 /** Public URL — what JAZZ_SERVER_URL points at in open mode; in ticket mode7787 * apps use a ticket's own /t/<secret> URL against this same host:port. */@@ -124,6 +134,11 @@ async function probeWritable(path: string): Promise<void> {
124134 }125135 }126136 137+/**138+ * Start a sync node: an embedded Jazz server, optional iroh mesh, and (in139+ * ticket mode) the public access gate. The returned {@link SyncNode} is the140+ * whole operational surface: tickets, pairing, status, stop.141+ */127142 export async function createSyncNode(options: SyncNodeOptions): Promise<SyncNode> {128143 let upstream: UpstreamConfig = options.upstream ?? "none";129144 const meshMode = options.mesh ?? "auto";@@ -1,16 +1,36 @@
1-// In-process test mesh: chained in-memory JazzServers with DIRECT loopback2-// upstream URLs — no iroh, no dylib, no network beyond 127.0.0.1. Proves Jazz3-// chaining semantics; the iroh tunnel has its own integration tests.1+/**2+ * In-process test helpers for lofi-node consumers.3+ *4+ * {@link createTestMesh} chains in-memory Jazz servers with direct loopback5+ * upstream URLs — no iroh, no native addon, no network beyond 127.0.0.1 — so6+ * app test suites can exercise multi-node sync topologies cheaply:7+ *8+ * ```ts9+ * import { createTestMesh } from "@nzip/lofi-node/testing";10+ *11+ * const mesh = await createTestMesh({ nodes: 2 });12+ * // point two clients at mesh.nodes[0].url / mesh.nodes[1].url …13+ * await mesh.stop();14+ * ```15+ *16+ * @module17+ */418 519 import { createSyncNode, type SyncNode } from "../src/node.ts";620 21+/** A running in-process mesh; stop() tears nodes down leaves-first. */722 export interface TestMesh {23+ /** Node 0 is the root; node i+1 uses node i as its direct upstream. */824 nodes: SyncNode[];25+ /** Stop all nodes in reverse creation order. */926 stop(): Promise<void>;1027 }1128 29+/** Options for {@link createTestMesh}. */1230 export interface TestMeshOptions {31+ /** How many chained nodes to start (default 2). */1332 nodes?: number;33+ /** Jazz app id shared by the whole mesh (default: a random UUID). */1434 appId?: string;1535 }1636