clone

a self-hosted sync node for lofi apps: jazz sync over iroh, ticket-gated, with no static ip or cloud needed

docs/ticket-custody-accuracy
lofi-node / src / native / addon.ts
addon.ts 9.0K
1// Load the vendored iroh-js napi addon (native/iroh-js) and type the subset2// lofi-node uses. The addon is upstream n0-computer/iroh-ffi's iroh-js crate3// plus the lofi_ext framing module — see native/iroh-js/UPSTREAM.md.4//5// NEVER call the watch APIs (watchAddr/watchPaths/…): at upstream v1.1.0 they6// panic outside a tokio context and ABORT the process under Deno.78import { createRequire } from "node:module";9import { pathToFileURL } from "node:url";10import { MeshUnavailableError } from "../errors.ts";11import { resolveIrohLib } from "./loader.ts";12import { RELEASE_ARTIFACTS } from "./artifacts.ts";1314export interface SecretKey {15 toBytes(): number[];16 public(): EndpointId;17}1819export interface EndpointId {20 toString(): string;21 equals(other: EndpointId): boolean;22}2324export interface EndpointAddr {25 id(): EndpointId;26 directAddresses(): string[];27 relayUrl(): string | null;28 toString(): string;29}3031export interface EndpointTicket {32 endpointAddr(): EndpointAddr;33 toString(): string;34}3536export interface SendStream {37 finish(): Promise<void>;38}3940export interface RecvStream {41 stop(errorCode: bigint): Promise<void>;42}4344export interface BiStream {45 readonly send: SendStream;46 readonly recv: RecvStream;47}4849export interface ConnectionStats {50 [key: string]: unknown;51}5253export interface PathSnapshot {54 [key: string]: unknown;55}5657export interface Connection {58 remoteId(): EndpointId;59 openBi(): Promise<BiStream>;60 acceptBi(): Promise<BiStream>;61 close(errorCode: bigint, reason: number[]): void;62 rtt(): number | null;63 stats(): ConnectionStats;64 paths(): PathSnapshot[];65}6667export interface Accepting {68 connect(): Promise<Connection>;69}7071export interface Incoming {72 accept(): Promise<Accepting>;73}7475export interface Endpoint {76 id(): EndpointId;77 addr(): EndpointAddr;78 connect(addr: EndpointAddr, alpn: number[]): Promise<Connection>;79 acceptNext(): Promise<Incoming | null>;80 close(): Promise<void>;81 isClosed(): boolean;82}8384/** Opaque relay election handle (constructed via the RelayMode factories). */85export interface RelayMode {86 toString(): string;87}8889export interface EndpointBuilder {90 applyN0(): void;91 applyN0DisableRelay(): void;92 relayMode(mode: RelayMode): void;93 secretKey(bytes: number[]): void;94 alpns(alpns: number[][]): void;95 bind(): Promise<Endpoint>;96}9798export interface IrohAddon {99 Endpoint: { builder(): EndpointBuilder };100 EndpointTicket: {101 fromAddr(addr: EndpointAddr): EndpointTicket;102 fromString(s: string): EndpointTicket;103 };104 RelayMode: {105 disabled(): RelayMode;106 customFromUrls(urls: string[]): RelayMode;107 };108 SecretKey: { fromBytes(bytes: number[]): SecretKey; generate(): SecretKey };109 // lofi_ext110 writeFrame(stream: SendStream, data: Uint8Array): Promise<void>;111 readFrame(stream: RecvStream, max?: number | null): Promise<Uint8Array | null>;112 maxFrame(): number;113}114115// Lazy + cwd-anchored: import.meta.url is an https: URL when this module is116// served from the JSR registry, and createRequire refuses non-file URLs. The117// anchor is irrelevant anyway — we only ever require absolute staged paths.118let lazyRequire: ReturnType<typeof createRequire> | null = null;119function nodeRequire(path: string): unknown {120 lazyRequire ??= createRequire(pathToFileURL(`${Deno.cwd()}/`).href);121 return lazyRequire(path);122}123124/** Cache key for extracted artifacts — bump when the vendored tag or125 * lofi_ext surface changes (see native/iroh-js/UPSTREAM.md). */126export const IROH_JS_VERSION = "1.1.0-lofi.1";127128function osCacheDir(): string {129 const home = Deno.env.get("HOME") ?? Deno.env.get("USERPROFILE") ?? ".";130 switch (Deno.build.os) {131 case "darwin":132 return `${home}/Library/Caches`;133 case "windows":134 return Deno.env.get("LOCALAPPDATA") ?? `${home}/AppData/Local`;135 default:136 return Deno.env.get("XDG_CACHE_HOME") ?? `${home}/.cache`;137 }138}139140/** Give require() a real on-disk .node file. Prefer a sibling shim next to141 * the artifact (dev flow: cargo output, refreshed on mtime). When the142 * artifact lives in a read-only place — a `deno compile` binary's embedded143 * vfs — extract to a version-keyed OS cache instead (atomic: tmp + rename,144 * safe under concurrent launches). */145function stageNodeArtifact(srcPath: string): string {146 if (srcPath.endsWith(".node")) return srcPath;147 const shim = srcPath.replace(/\.(dylib|so|dll)$/, ".node");148 try {149 const src = Deno.statSync(srcPath);150 let stale = true;151 try {152 const dst = Deno.statSync(shim);153 stale = (src.mtime?.getTime() ?? 1) > (dst.mtime?.getTime() ?? 0);154 } catch {155 // no shim yet156 }157 if (stale) Deno.copyFileSync(srcPath, shim);158 return shim;159 } catch {160 // Sibling not writable (embedded vfs / read-only install): cache extract.161 }162 const dir =163 `${osCacheDir()}/lofi-node/iroh-js-${IROH_JS_VERSION}-${Deno.build.os}-${Deno.build.arch}`;164 const target = `${dir}/iroh.node`;165 try {166 Deno.statSync(target);167 return target; // version-keyed: presence means done168 } catch {169 // extract below170 }171 Deno.mkdirSync(dir, { recursive: true });172 const tmp = `${target}.${Deno.pid}.tmp`;173 Deno.writeFileSync(tmp, Deno.readFileSync(srcPath));174 Deno.renameSync(tmp, target);175 return target;176}177178/** Fetch the platform's addon from the GitHub release pinned by the179 * committed artifacts.ts, verify its sha256 against the in-package digest180 * (the JSR artifact pins the binaries it will accept), and stage it into the181 * version-keyed OS cache. This is how JSR consumers get the native layer —182 * the package itself ships no binaries. */183async function downloadReleaseArtifact(platform: string): Promise<string> {184 const asset = (RELEASE_ARTIFACTS.assets as Record<string, { file: string; sha256: string }>)[185 platform186 ];187 if (!asset) {188 throw new MeshUnavailableError(`no release artifact for ${platform}`);189 }190 const dir = `${osCacheDir()}/lofi-node/iroh-js-release-${RELEASE_ARTIFACTS.version}-${platform}`;191 const target = `${dir}/iroh.node`;192 try {193 Deno.statSync(target);194 return target; // version-keyed: verified at download time195 } catch {196 // download below197 }198 const url =199 `https://github.com/FelineStateMachine/lofi-node/releases/download/v${RELEASE_ARTIFACTS.version}/${asset.file}`;200 let bytes: Uint8Array;201 try {202 const res = await fetch(url);203 if (!res.ok) {204 await res.body?.cancel();205 throw new Error(`HTTP ${res.status}`);206 }207 bytes = new Uint8Array(await res.arrayBuffer());208 } catch (e) {209 throw new MeshUnavailableError(210 `could not download native addon from ${url}: ${(e as Error).message}`,211 );212 }213 const digest = await crypto.subtle.digest("SHA-256", bytes.buffer as ArrayBuffer);214 const sha256 = [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");215 if (sha256 !== asset.sha256) {216 throw new MeshUnavailableError(217 `native addon checksum mismatch for ${asset.file}: got ${sha256.slice(0, 16)}…, ` +218 `expected ${asset.sha256.slice(0, 16)}… — refusing to load`,219 );220 }221 Deno.mkdirSync(dir, { recursive: true });222 const tmp = `${target}.${Deno.pid}.tmp`;223 Deno.writeFileSync(tmp, bytes);224 Deno.renameSync(tmp, target);225 return target;226}227228/** Load + probe the addon. Resolution: env/explicit → cargo build output →229 * in-repo prebuilt → GitHub-release download (JSR consumers). Throws230 * MeshUnavailableError with a precise reason (no silent degradation —231 * lofi's boot-gate ethos). */232export async function loadIrohAddon(explicitPath?: string): Promise<IrohAddon> {233 const resolved = resolveIrohLib(explicitPath);234 if (resolved.status === "unsupported-platform") {235 throw new MeshUnavailableError(`no iroh-js build for ${resolved.platform}`);236 }237 // require() only loads native addons from `.node` files — cargo emits238 // .dylib/.so/.dll, and a compiled binary's embedded artifacts aren't239 // directly loadable at all; stage a real .node file either way.240 let loadPath: string;241 if (resolved.status === "not-found") {242 // Running from the JSR cache (no repo checkout): fetch the pinned243 // release artifact instead.244 const platform = `${Deno.build.os}-${Deno.build.arch}`;245 loadPath = await downloadReleaseArtifact(platform);246 } else {247 try {248 loadPath = stageNodeArtifact(resolved.path);249 } catch (e) {250 throw new MeshUnavailableError(251 `could not stage .node artifact for ${resolved.path}: ${(e as Error).message}`,252 );253 }254 }255 let addon: IrohAddon;256 try {257 addon = nodeRequire(loadPath) as IrohAddon;258 } catch (e) {259 throw new MeshUnavailableError(`addon load failed for ${loadPath}: ${(e as Error).message}`);260 }261 // Probe: core surface + the lofi_ext marker prove this is OUR build, not a262 // stock upstream artifact.263 if (typeof addon.Endpoint?.builder !== "function") {264 throw new MeshUnavailableError(`addon at ${loadPath} lacks the Endpoint surface`);265 }266 if (typeof addon.maxFrame !== "function" || typeof addon.writeFrame !== "function") {267 throw new MeshUnavailableError(268 `addon at ${loadPath} lacks lofi_ext framing — built from stock upstream?`,269 );270 }271 return addon;272}