pinnace 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -313,11 +313,15 @@ async function defaultWarm(
313
313
  }
314
314
 
315
315
  /**
316
- * Default `status`. Reuses the `status-report` core logic (injected via
317
- * {@link NodeCommandContext.ops}.status; this default is a thin stand-in until
318
- * that task lands) and writes the resulting report as `status.json` under the
319
- * box's dashboard directory ONLY. Reporting per-site CID / IPNS id happens in
320
- * the reused core; this wrapper just persists it where the dashboard reads.
316
+ * Default `status` a thin stand-in used ONLY when no `status` op is injected.
317
+ * The PRODUCTION on-box path (`pinnace node status`, wired in cli/run.ts's
318
+ * `runNodeCli`) injects the OWNED `status` op ({@link makeStatusOp} from
319
+ * status-report) via {@link NodeCommandContext.ops}.status, which is the real
320
+ * per-site CID/IPNS/announce/gateway report. This default remains as the
321
+ * seam's safe fallback (e.g. a direct `runNodeCommand` call that does not want
322
+ * the live external checks / a hermetic test that injects its own op), so a
323
+ * bare call still reports current CID + IPNS id without reaching the network.
324
+ * The dashboard write is done by the command layer ({@link runNodeCommand}).
321
325
  */
322
326
  async function defaultStatus(
323
327
  ctx: NodeCommandContext,
@@ -41,6 +41,18 @@
41
41
  * (interval well under the record-expiry window, strategy `all`).
42
42
  * - Caddy HTTPS reverse proxy for the RPC API (auto TLS), which forwards the
43
43
  * operator's bearer header straight through to the token-guarded Kubo API.
44
+ *
45
+ * INSTALL CHANNEL + BOOT-SAFETY (task `cloud-init-pinnace-install-channel`):
46
+ * - The box installs a PINNED `pinnace` ({@link DEFAULT_PINNACE_VERSION}) on a
47
+ * named current Node LTS ({@link DEFAULT_NODE_MAJOR}), both overridable per
48
+ * box. Pinned (never floating `latest`) so a boot is reproducible.
49
+ * - The install is BOOT-SAFE: `pinnace-setup.sh` is invoked with `|| true`, so
50
+ * a transient npm/registry failure can NOT abort the boot. Kubo, the
51
+ * firewall and Caddy come up regardless of the agent install.
52
+ * - The dedicated `ipfs` service user is created by cloud-init's `users:`
53
+ * module, which runs BEFORE `runcmd`, so no boot step can hit "invalid user
54
+ * 'ipfs'" (closes the first-boot race in
55
+ * work/notes/observations/cloud-init-first-boot-ipfs-user-race-and-set-e-abort.md).
44
56
  */
45
57
  import type {HostRole} from '../config/config-resolution.js';
46
58
 
@@ -95,6 +107,19 @@ export interface ProvisionInput {
95
107
  gateways?: string[];
96
108
  /** The Kubo version to install. Defaults to a pinned known-good release. */
97
109
  kuboVersion?: string;
110
+ /**
111
+ * The `pinnace` version to install on the box (`npm install -g
112
+ * pinnace@<this>`). Defaults to {@link DEFAULT_PINNACE_VERSION} (the current
113
+ * published release). PINNED, never floating `latest`, so a box boot is
114
+ * reproducible. Overridable per-box (e.g. to roll a box onto a newer agent).
115
+ */
116
+ pinnaceVersion?: string;
117
+ /**
118
+ * The Node.js major version to install via NodeSource (`setup_<this>.x`).
119
+ * Defaults to {@link DEFAULT_NODE_MAJOR} (a current active LTS). A named knob
120
+ * so the LTS bump is one obvious edit, not a literal buried in a shell line.
121
+ */
122
+ nodeMajor?: string;
98
123
  /** The MFS directory sites live under. Defaults to `/sites`. */
99
124
  sitesDir?: string;
100
125
  }
@@ -131,6 +156,23 @@ export interface HostProvider {
131
156
 
132
157
  /** Defaults kept in one place so the template + docs never drift. */
133
158
  const DEFAULT_KUBO_VERSION = 'v0.38.1';
159
+ /**
160
+ * The pinned `pinnace` version the box installs (`npm install -g
161
+ * pinnace@<this>`). Mirrors {@link DEFAULT_KUBO_VERSION}: a NAMED knob, not a
162
+ * literal, so a release bump is one obvious edit here. PINNED (never floating
163
+ * `latest`) so a box boot is reproducible: the same cloud-init always installs
164
+ * the same agent. Overridable per-box via {@link ProvisionInput.pinnaceVersion}.
165
+ * `pinnace@0.1.0` is the first published release (npm, public, OIDC provenance).
166
+ */
167
+ const DEFAULT_PINNACE_VERSION = '0.2.0';
168
+ /**
169
+ * The pinned Node.js major the box installs via NodeSource (`setup_<this>.x`).
170
+ * Node 22 is a current active LTS; Node 20 (the old literal) is the OLDEST LTS
171
+ * (EOL ~2026-04) and incoherent with the repo's own Node 24 toolchain. A NAMED
172
+ * knob (mirrors {@link DEFAULT_KUBO_VERSION}) so the LTS bump is one edit.
173
+ * Overridable per-box via {@link ProvisionInput.nodeMajor}.
174
+ */
175
+ const DEFAULT_NODE_MAJOR = '22';
134
176
  const DEFAULT_SITES_DIR = '/sites';
135
177
  const DEFAULT_GATEWAYS: readonly string[] = [
136
178
  'https://{cid}.ipfs.dweb.link/',
@@ -239,6 +281,8 @@ function renderHetznerCloudInit(input: ProvisionInput): string {
239
281
  input.role === 'replica' ? (input.publisherEndpoint ?? '') : '';
240
282
  const gateways = input.gateways ?? DEFAULT_GATEWAYS;
241
283
  const kuboVersion = input.kuboVersion ?? DEFAULT_KUBO_VERSION;
284
+ const pinnaceVersion = input.pinnaceVersion ?? DEFAULT_PINNACE_VERSION;
285
+ const nodeMajor = input.nodeMajor ?? DEFAULT_NODE_MAJOR;
242
286
  const sitesDir = input.sitesDir ?? DEFAULT_SITES_DIR;
243
287
  const warmGateways = gateways.join(' ');
244
288
 
@@ -278,6 +322,22 @@ packages:
278
322
  - debian-archive-keyring
279
323
  - apt-transport-https
280
324
 
325
+ # ---------------------------------------------------------------------------
326
+ # Service users. cloud-init's \`users:\` module runs BEFORE \`runcmd\`, so the
327
+ # dedicated \`ipfs\` user is GUARANTEED to exist before any boot step uses it
328
+ # (e.g. \`install -o ipfs ...\`). Creating it here (not mid-\`ipfs-setup.sh\`,
329
+ # which is a \`set -e\` block that could abort before its \`useradd\`) closes the
330
+ # first-boot race where a later step hit "invalid user 'ipfs'".
331
+ # \`default\` keeps cloud-init's normal login user; we only ADD \`ipfs\`.
332
+ # ---------------------------------------------------------------------------
333
+ users:
334
+ - default
335
+ - name: ipfs
336
+ system: true
337
+ home: /var/lib/ipfs
338
+ shell: /usr/sbin/nologin
339
+ lock_passwd: true
340
+
281
341
  # ---------------------------------------------------------------------------
282
342
  # On-box environment consumed by the pinnace node timers + the setup scripts.
283
343
  # ---------------------------------------------------------------------------
@@ -300,6 +360,21 @@ write_files:
300
360
  # MFS directory that holds your sites (one entry per site).
301
361
  SITES_DIR="${sitesDir}"
302
362
 
363
+ # On-box PATHS the \`pinnace node\` verbs read (they assemble their
364
+ # NodeCommandContext from these keys):
365
+ # DASHBOARD_DIR - where \`node status\` writes status.json (the dashboard
366
+ # vhost \${DASH_DOMAIN} serves this dir).
367
+ # RECORDS_DIR - where \`node republish\` (publisher) EXPORTS the signed
368
+ # record; UNDER the dashboard dir so it is served at
369
+ # \${DASH_DOMAIN}/records/<id>.ipns-record, exactly where a
370
+ # replica's PUBLISHER_ENDPOINT + /records/ fetch looks.
371
+ # CACHE_DIR - where \`node mirror\` (replica) CACHES the last good
372
+ # record for the publisher-outage fallback (under the
373
+ # ipfs user's writable home).
374
+ DASHBOARD_DIR="/var/www/ipfs-dash"
375
+ RECORDS_DIR="/var/www/ipfs-dash/records"
376
+ CACHE_DIR="/var/lib/ipfs/records-cache"
377
+
303
378
  # Role: "publisher" holds IPNS keys and signs/exports records; "replica"
304
379
  # holds NO key and MIRRORS the publisher's signed records. The pinnace node
305
380
  # timers self-gate on this, so scheduling all timers on every box is safe.
@@ -310,6 +385,12 @@ write_files:
310
385
 
311
386
  KUBO_VERSION="${kuboVersion}"
312
387
 
388
+ # Node.js major installed via NodeSource (setup_<major>.x) and the PINNED
389
+ # pinnace version installed on the box. Both are reproducible: the same
390
+ # cloud-init always installs the same agent on the same runtime.
391
+ NODE_MAJOR="${nodeMajor}"
392
+ PINNACE_VERSION="${pinnaceVersion}"
393
+
313
394
  # -- Kubo installer / initializer -----------------------------------------
314
395
  - path: /usr/local/sbin/ipfs-setup.sh
315
396
  permissions: "0755"
@@ -408,13 +489,19 @@ write_files:
408
489
  content: |
409
490
  #!/usr/bin/env bash
410
491
  set -euo pipefail
411
- # Install Node.js (for npm) then the pinnace binary globally. The on-box
412
- # timers invoke \`pinnace node <verb>\`: one codebase, client + on-box.
492
+ source /etc/pinnace-node.env
493
+ # Install Node.js (for npm) then the PINNED pinnace binary globally. The
494
+ # on-box timers invoke \`pinnace node <verb>\`: one codebase, client +
495
+ # on-box. NODE_MAJOR / PINNACE_VERSION come from /etc/pinnace-node.env so
496
+ # the runtime + agent version are reproducible and one-edit overridable.
497
+ # This script is invoked NON-FATALLY at boot (\`|| true\`): a transient
498
+ # npm/registry hiccup must NOT abort provisioning (Kubo, the firewall and
499
+ # Caddy are already up by the time this runs). Re-run it manually to retry.
413
500
  if ! command -v npm >/dev/null 2>&1; then
414
- curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
501
+ curl -fsSL "https://deb.nodesource.com/setup_\${NODE_MAJOR}.x" | bash -
415
502
  apt-get install -y nodejs
416
503
  fi
417
- npm install -g pinnace
504
+ npm install -g "pinnace@\${PINNACE_VERSION}"
418
505
  pinnace version
419
506
 
420
507
  # -- Caddy reverse proxy for the HTTPS API (auto TLS) ---------------------
@@ -490,12 +577,20 @@ runcmd:
490
577
  # Configure the HTTPS API proxy (no-op if API_DOMAIN unset)
491
578
  - /usr/local/sbin/write-caddyfile.sh
492
579
 
493
- # Install the pinnace binary (the box runs the same CLI as the client).
494
- - /usr/local/sbin/pinnace-setup.sh
580
+ # Dashboard dir, owned by the ipfs service user (guaranteed to exist: created
581
+ # by the \`users:\` module above, BEFORE runcmd). Done BEFORE the pinnace
582
+ # install so a transient install failure can never skip it.
583
+ - install -d -o ipfs -g ipfs /var/www/ipfs-dash
584
+
585
+ # Install the pinned pinnace binary (the box runs the same CLI as the client).
586
+ # BOOT-SAFE: \`|| true\` so a transient npm/registry failure does NOT abort the
587
+ # boot (Kubo, the firewall and Caddy are already up). Re-run
588
+ # /usr/local/sbin/pinnace-setup.sh manually to retry; the timers below pick it
589
+ # up on their next tick once the binary is present.
590
+ - /usr/local/sbin/pinnace-setup.sh || true
495
591
 
496
592
  # Enable the pinnace node timers. republish self-gates to publisher, mirror to
497
593
  # replica, so enabling all of them on every box is safe (ADR-0002).
498
- - install -d -o ipfs -g ipfs /var/www/ipfs-dash
499
594
  ${enableTimers}
500
595
 
501
596
  # =============================================================================