// ipfs add artifacts/ --recursive --cid-version=1 --hidden --max-directory-links=1000 --chunker=size-1048576 import * as Fs from "@std/fs"; import * as Path from "@std/path"; import { cpSync } from "node:fs"; import { load } from "@std/dotenv"; import { MemoryBlockstore } from "blockstore-core/memory"; import { importer } from "ipfs-unixfs-importer"; import { CarWriter } from "@ipld/car"; import { CID } from "multiformats/cid"; import * as DagPB from "@ipld/dag-pb"; import artifactsJson from "../../artifacts/artifacts.json" with { type: "json", }; import type { Artifact } from "../../common/types.d.ts"; import { execSync } from "../../common/index.ts"; import { createCAR } from "../../common/car.ts"; const artifacts: Record = artifactsJson as any; //////////////////////////////////////////// // VERIFY IPFS DAEMON IS RUNNING //////////////////////////////////////////// // Fail fast if the local IPFS node isn't up — everything downstream (dag // import/put, Filebase pinning, IPNS publish) depends on it, and a missing // daemon would otherwise surface as an obscure error or a pinning poll that // times out after 10 minutes. We hit the HTTP API directly (the same thing // the `ipfs` CLI does under the hood) rather than spawning a subprocess. const IPFS_API = "http://127.0.0.1:5001"; let daemonUp = false; try { const res = await fetch(`${IPFS_API}/api/v0/version`, { method: "POST" }); daemonUp = res.ok; } catch { daemonUp = false; } if (!daemonUp) { console.error("❌ IPFS daemon is not running."); console.error(" Start it with: ipfs daemon"); Deno.exit(1); } // Also verify the node is connected to the network — Filebase fetches the // CID over bitswap, so an isolated daemon (0 peers) would cause the pin poll // to time out after 10 minutes. let peerCount = 0; try { const peersRes = await fetch(`${IPFS_API}/api/v0/swarm/peers`, { method: "POST", }); if (peersRes.ok) { const peersResult: { Peers?: unknown[] } = await peersRes.json(); peerCount = peersResult.Peers?.length ?? 0; } } catch { // Treat as 0 peers — will fail the check below. } if (peerCount === 0) { console.error("❌ IPFS daemon has no network peers."); console.error( " Filebase can't fetch the CID over bitswap — check your connectivity.", ); Deno.exit(1); } console.log(`✅️ IPFS daemon is running (${peerCount} peers)`); //////////////////////////////////////////// // ADD CARS TO IPFS //////////////////////////////////////////// Fs.ensureDirSync("cars"); for (const artifact of Object.values(artifacts)) { const carPath = Path.join("cars", artifact.cid + ".car"); const exists = Fs.existsSync(carPath); if (!exists) { const inputPath = Path.join("artifacts", artifact.cid); const tmpDir = Deno.makeTempDirSync(); cpSync(inputPath, tmpDir, { dereference: true, preserveTimestamps: true, recursive: true, }); await createCAR(tmpDir, carPath); Deno.removeSync(tmpDir, { recursive: true }); } execSync( "ipfs", "dag", "import", carPath, ); } //////////////////////////////////////////// // GENERATE ROOT DAG & ADD BLOCKS TO IPFS //////////////////////////////////////////// const blockstore = new MemoryBlockstore(); const source = [ "artifacts/_redirects", "artifacts/artifacts.json", "artifacts/index.html", "artifacts/versions.json", ].map( (path) => { return { path: path.split("/")[1], content: Deno.readFileSync(path), }; }, ); // Walk dist/ recursively (including subdirectories like chronicle/, fonts/, // images/, styles/) and sort for deterministic directory CIDs. const distFiles = []; for (const entry of Fs.walkSync("dist")) { if (!entry.isFile) continue; distFiles.push({ path: entry.path.split("/").slice(1).join("/"), content: Deno.readFileSync(entry.path), }); } distFiles.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0)); source.push(...distFiles); const links = []; const importerRoots: CID[] = []; for await (const entry of importer(source, blockstore)) { // Only add top-level entries (path has no "/") as root DAG links. // Nested files are reachable through their parent directory link. if (!entry.path?.includes("/")) { links.push( DagPB.createLink(entry.path ?? "", Number(entry.size), entry.cid), ); } importerRoots.push(entry.cid); } { const { writer: carWriter, out } = CarWriter.create(importerRoots); const importCmd = new Deno.Command("ipfs", { args: ["dag", "import", "--silent"], stdin: "piped", }); const importProc = importCmd.spawn(); const pipePromise = ReadableStream.from(out).pipeTo(importProc.stdin); for await (const { cid, bytes: bytesGen } of blockstore.getAll()) { const chunks: Uint8Array[] = []; for await (const chunk of bytesGen) chunks.push(chunk); const size = chunks.reduce((n, c) => n + c.length, 0); const bytes = new Uint8Array(size); let offset = 0; for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.length; } await carWriter.put({ cid, bytes }); } await carWriter.close(); await pipePromise; await importProc.output(); } Object.values(artifacts).forEach((artifact) => { links.push( DagPB.createLink(artifact.cid, 0, CID.parse(artifact.cid)), ); }); const bytes = DagPB.encode({ Data: new Uint8Array([8, 1]), Links: links.toSorted((a, b) => { const aName = a.Name ?? ""; const bName = b.Name ?? ""; if (aName < bName) return -1; if (aName > bName) return 1; return 0; }), }); //////////////////////////////////////////// // ADD ROOT DAG TO IPFS //////////////////////////////////////////// const cmd = new Deno.Command("ipfs", { args: ["dag", "put", "--store-codec", "dag-pb", "--input-codec", "dag-pb"], stdin: "piped", stdout: "piped", }); const process = cmd.spawn(); const writer = process.stdin.getWriter(); await writer.write(bytes); await writer.close(); const cid = (await process.stdout.text()).trim(); console.log("✅️ Added artifacts DAG to IPFS with CID: " + cid); //////////////////////////////////////////// // SET DNSLINK //////////////////////////////////////////// const env = await load(); const deleteCmd = new Deno.Command("lexicon", { args: [ "dnsimple", "delete", "diffuse.sh", "TXT", "--name", "_dnslink.elements.diffuse.sh", ], env, }); await deleteCmd.spawn().output(); const createCmd = new Deno.Command("lexicon", { args: [ "dnsimple", "create", "diffuse.sh", "TXT", "--name", "_dnslink.elements.diffuse.sh", "--content", `"dnslink=/ipfs/${cid}"`, ], env, }); await createCmd.spawn().output(); console.log("✅️ DNSLink set"); //////////////////////////////////////////// // PIN TO FILEBASE //////////////////////////////////////////// // Filebase's Pinning Service API and RPC API use a per-bucket access token // generated from the Console (Access Keys → IPFS RPC API endpoint → // Choose Bucket to Generate Token). This token is scoped to the `diffuse-sh` // bucket and is sent verbatim as the Bearer token — no encoding needed. const filebaseToken = env.FILEBASE_TOKEN; // Human-readable bucket object name: `diffuse-deploy-`. // Filebase requires unique names per bucket (409 otherwise), and the `name` // field is capped at 255 chars — well within range. const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); const objectName = `diffuse-deploy-${timestamp}`; const pinResponse = await fetch("https://api.filebase.io/v1/ipfs/pins", { method: "POST", headers: { "Authorization": `Bearer ${filebaseToken}`, "Content-Type": "application/json", }, body: JSON.stringify({ cid, name: objectName, meta: { project: "diffuse-sh" }, }), }); if (!pinResponse.ok) { console.error( `❌ Filebase pin failed: ${pinResponse.status} ${pinResponse.statusText}`, ); console.error(await pinResponse.text()); Deno.exit(1); } const pinResult = await pinResponse.json(); console.log( `✅️ Pin requested on Filebase bucket 'diffuse-sh' as '${objectName}' (requestid: ${pinResult.requestid}, status: ${pinResult.status})`, ); // The Pinning Service API is asynchronous: the POST above returns immediately // with status `queued` (or `pinning`). Publishing the IPNS record before the // pin completes means resolvers could fetch a CID that Filebase's node can't // yet serve, so we poll GET /pins/{requestid} until it reaches `pinned`. const requestId = pinResult.requestid ?? pinResult.requestId; if (!requestId) { console.warn( "⚠️ Pin response had no requestid; cannot poll for completion. Proceeding anyway.", ); } else { const maxAttempts = 120; // 120 × 5s = 10 min ceiling const pollIntervalMs = 5000; let pinned = false; for (let attempt = 1; attempt <= maxAttempts; attempt++) { const statusResponse = await fetch( `https://api.filebase.io/v1/ipfs/pins/${requestId}`, { headers: { "Authorization": `Bearer ${filebaseToken}` } }, ); if (!statusResponse.ok) { console.warn( `⚠️ Pin status poll failed: ${statusResponse.status} ${statusResponse.statusText}`, ); break; } const statusResult = await statusResponse.json(); const status = statusResult.status; if (status === "pinned") { pinned = true; console.log(`✅️ Pin complete (status: pinned)`); break; } if (status === "failed") { console.error(`❌ Filebase pin failed (status: failed)`); console.error(JSON.stringify(statusResult, null, 2)); Deno.exit(1); } console.log( ` pin status: ${status} (attempt ${attempt}/${maxAttempts})`, ); await new Promise((r) => setTimeout(r, pollIntervalMs)); } if (!pinned) { console.error( `❌ Pin did not reach 'pinned' status within ${ maxAttempts * pollIntervalMs / 1000 }s`, ); Deno.exit(1); } } //////////////////////////////////////////// // UPDATE FILEBASE SITE 'diffuse' (IPNS PUBLISH) //////////////////////////////////////////// const siteKey = "diffuse.myfilebase.site"; // 1. Verify the IPNS key exists before publishing. // Filebase sites are backed by an IPNS keypair; if the key name doesn't // match the site name, the publish would silently target the wrong key // (or the node's default 'self' key). const keyListResponse = await fetch( "https://rpc.filebase.io/api/v0/key/list", { method: "POST", headers: { "Authorization": `Bearer ${filebaseToken}` }, }, ); if (!keyListResponse.ok) { console.error( `❌ Filebase key/list failed: ${keyListResponse.status} ${keyListResponse.statusText}`, ); console.error(await keyListResponse.text()); Deno.exit(1); } const keyListResult = await keyListResponse.json(); const keys = keyListResult.Keys ?? keyListResult.keys ?? []; const siteKeyExists = keys.some( (k: any) => (k.Name ?? k.name) === siteKey, ); if (!siteKeyExists) { console.error( `❌ IPNS key '${siteKey}' not found. Available keys: ${ keys.map((k: any) => k.Name ?? k.name).join(", ") || "(none)" }`, ); console.error( ` Generate it in the Filebase Console (Sites → diffuse → manage keys) or via /api/v0/key/gen`, ); Deno.exit(1); } // 2. Publish the CID to IPNS using the site's key. // ttl=1m makes resolvers re-check sooner (default 5m), trading a small // resolution overhead for faster propagation after deploys. const publishResponse = await fetch( `https://rpc.filebase.io/api/v0/name/publish?arg=/ipfs/${cid}&key=${siteKey}&ttl=1m`, { method: "POST", headers: { "Authorization": `Bearer ${filebaseToken}` }, }, ); if (!publishResponse.ok) { console.error( `❌ Filebase IPNS publish failed: ${publishResponse.status} ${publishResponse.statusText}`, ); console.error(await publishResponse.text()); Deno.exit(1); } const publishResult = await publishResponse.json(); const ipnsName = publishResult.Name ?? publishResult.name; console.log( `✅️ Published CID to IPNS key '${siteKey}' (${ipnsName ?? "published"})`, );