@loopier/publish
The tiny client for publishing to the Loopier network from anywhere — a Node script, a Deno or Bun job, an edge function, a CI step, your own bot. Zero dependencies; the only platform requirement is a WHATWG fetch global (Node 18+, Deno, Bun, Cloudflare Workers).
import { LoopierPublish } from "@loopier/publish";
const loopier = new LoopierPublish({
token: process.env.LOOPIER_PUBLISH_TOKEN!, // workspace publish token — a server secret
workspace: "acme", // your company handle on loopier.xyz
});
await loopier.ship({ title: "Dark mode is live", url: "https://acme.dev/changelog/42" });
await loopier.update({ title: "Payments migration", status: "in progress" });
await loopier.launch({ title: "Acme v2", url: "https://acme.dev" });
What it actually sends
Every verb builds a publish intent — { kind, row } where row is a sanitized projection shape (title, status, url, handle) — and POSTs it to POST {base}/api/network/publish with Authorization: Bearer <token>. It is a request to be published, not a write: the hosted side validates, re-sanitizes, stamps, and writes the public record. The row is a hint, not a trust boundary — fields outside the projector's whitelist are dropped server-side, and nothing you put in extra can smuggle private data past the sanitizer's whitelist.
ship(input)→ kindissue, status defaults to"shipped"update(input)→ kindissue, status defaults to"update"launch(input)→ kindlaunch, status defaults to"launched"publish(intent)→ the raw escape hatch (anyPublishKind)unpublish({ kind, id })→ prospective freeze (retained + attributed, never erased)
The pure intent builders (shipIntent, updateIntent, launchIntent) are exported separately so CLIs and CI steps can --dry-run the exact payload with zero duplication.
Reliability built in
- Retries — transport failures and 408/429/5xx are retried with exponential backoff + jitter (default 3 retries);
Retry-Afteris honored. Other 4xx are final. - Idempotency — every logical call carries an
Idempotency-Keyheader reused across its retries, so a retried delivery can never double-publish. Pass{ idempotencyKey: "release-v1.2.3" }to make retries safe across processes too. - Honest results, no throws — every call resolves to
{ ok: true, status, data, attempts }or{ ok: false, status, error, errorKind, attempts }witherrorKindone ofconfig | network | http. Only constructor misuse throws.
Secret hygiene
The publish token is a server secret. This module never logs it, never echoes it into an error, and scrubs it from any transport message before the string leaves the module. Keep the token in an env var or secret store — never in source, never in a browser.
Options
new LoopierPublish({
token, // required — workspace publish token
workspace: "acme", // optional — default handle stamped into rows
baseUrl: "https://loopier.xyz", // your self-hosted network origin if not the hosted one
timeoutMs: 10_000, // per-attempt timeout
maxRetries: 3, // retries after the first attempt
retryBaseMs: 300, // backoff base (doubles per retry, capped 5s)
fetch: customFetch, // injection for tests / exotic runtimes
});
Develop
npm install # dev deps only (typescript, @types/node) — the shipped code has zero deps
npm run typecheck
npm test # tsc → node --test (no test framework)
npm run build # emit ESM + d.ts into dist/ (used at npm-publish time via prepack)
In-repo consumers (the loopier CLI in packages/loopier-mcp) resolve the TypeScript source directly via exports (same pattern as @loopier/blocks) and bundle it with tsup, so no build step is needed for repo development. At npm-release time prepack builds dist/; flip main/types/exports to dist/ in the release commit when the package first ships to the registry.