npm.io
0.1.0 • Published 4d ago

@loopier/publish

Licence
Apache-2.0
Version
0.1.0
Deps
0
Size
59 kB
Vulns
0
Weekly
0

@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) → kind issue, status defaults to "shipped"
  • update(input) → kind issue, status defaults to "update"
  • launch(input) → kind launch, status defaults to "launched"
  • publish(intent) → the raw escape hatch (any PublishKind)
  • 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-After is honored. Other 4xx are final.
  • Idempotency — every logical call carries an Idempotency-Key header 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 } with errorKind one of config | 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.

Keywords