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.
@@ -1 +1 @@
1
- {"version":3,"file":"node-commands.js","sourceRoot":"","sources":["../../src/node/node-commands.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"node-commands.js","sourceRoot":"","sources":["../../src/node/node-commands.ts"],"names":[],"mappings":"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"}
@@ -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
  /** The hosts pinnace can provision for. v1 = Hetzner ONLY. */
@@ -92,6 +104,19 @@ export interface ProvisionInput {
92
104
  gateways?: string[];
93
105
  /** The Kubo version to install. Defaults to a pinned known-good release. */
94
106
  kuboVersion?: string;
107
+ /**
108
+ * The `pinnace` version to install on the box (`npm install -g
109
+ * pinnace@<this>`). Defaults to {@link DEFAULT_PINNACE_VERSION} (the current
110
+ * published release). PINNED, never floating `latest`, so a box boot is
111
+ * reproducible. Overridable per-box (e.g. to roll a box onto a newer agent).
112
+ */
113
+ pinnaceVersion?: string;
114
+ /**
115
+ * The Node.js major version to install via NodeSource (`setup_<this>.x`).
116
+ * Defaults to {@link DEFAULT_NODE_MAJOR} (a current active LTS). A named knob
117
+ * so the LTS bump is one obvious edit, not a literal buried in a shell line.
118
+ */
119
+ nodeMajor?: string;
95
120
  /** The MFS directory sites live under. Defaults to `/sites`. */
96
121
  sitesDir?: string;
97
122
  }
@@ -1 +1 @@
1
- {"version":3,"file":"cloud-init.d.ts","sourceRoot":"","sources":["../../src/provision/cloud-init.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,gCAAgC,CAAC;AAE7D,8DAA8D;AAC9D,MAAM,MAAM,QAAQ,GAAG,SAAS,CAAC;AAEjC,yEAAyE;AACzE,eAAO,MAAM,cAAc,EAAE,SAAS,QAAQ,EAAgB,CAAC;AAE/D;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC9B,mDAAmD;IACnD,IAAI,EAAE,QAAQ,CAAC;IACf,0EAA0E;IAC1E,SAAS,EAAE,MAAM,CAAC;IAClB,gFAAgF;IAChF,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,2DAA2D;IAC3D,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;;OAMG;IACH,IAAI,EAAE,QAAQ,CAAC;IACf;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,4EAA4E;IAC5E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,+DAA+D;AAC/D,MAAM,WAAW,WAAW;IAC3B,gFAAgF;IAChF,IAAI,EAAE,MAAM,CAAC;IACb,8BAA8B;IAC9B,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,wEAAwE;AACxE,MAAM,WAAW,eAAe;IAC/B,mEAAmE;IACnE,IAAI,EAAE,QAAQ,CAAC;IACf,6CAA6C;IAC7C,SAAS,EAAE,WAAW,CAAC;CACvB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC5B,6CAA6C;IAC7C,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,0EAA0E;IAC1E,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,eAAe,CAAC;CAClD;AA+XD;;;GAGG;AACH,eAAO,MAAM,mBAAmB,EAAE,YAWjC,CAAC;AAOF;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,eAAe,CAUhE"}
1
+ {"version":3,"file":"cloud-init.d.ts","sourceRoot":"","sources":["../../src/provision/cloud-init.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AACH,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,gCAAgC,CAAC;AAE7D,8DAA8D;AAC9D,MAAM,MAAM,QAAQ,GAAG,SAAS,CAAC;AAEjC,yEAAyE;AACzE,eAAO,MAAM,cAAc,EAAE,SAAS,QAAQ,EAAgB,CAAC;AAE/D;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC9B,mDAAmD;IACnD,IAAI,EAAE,QAAQ,CAAC;IACf,0EAA0E;IAC1E,SAAS,EAAE,MAAM,CAAC;IAClB,gFAAgF;IAChF,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,2DAA2D;IAC3D,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;;OAMG;IACH,IAAI,EAAE,QAAQ,CAAC;IACf;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,4EAA4E;IAC5E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,+DAA+D;AAC/D,MAAM,WAAW,WAAW;IAC3B,gFAAgF;IAChF,IAAI,EAAE,MAAM,CAAC;IACb,8BAA8B;IAC9B,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,wEAAwE;AACxE,MAAM,WAAW,eAAe;IAC/B,mEAAmE;IACnE,IAAI,EAAE,QAAQ,CAAC;IACf,6CAA6C;IAC7C,SAAS,EAAE,WAAW,CAAC;CACvB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC5B,6CAA6C;IAC7C,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,0EAA0E;IAC1E,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,eAAe,CAAC;CAClD;AAqcD;;;GAGG;AACH,eAAO,MAAM,mBAAmB,EAAE,YAWjC,CAAC;AAOF;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,eAAe,CAUhE"}
@@ -2,6 +2,23 @@
2
2
  export const HOST_PROVIDERS = ['hetzner'];
3
3
  /** Defaults kept in one place so the template + docs never drift. */
4
4
  const DEFAULT_KUBO_VERSION = 'v0.38.1';
5
+ /**
6
+ * The pinned `pinnace` version the box installs (`npm install -g
7
+ * pinnace@<this>`). Mirrors {@link DEFAULT_KUBO_VERSION}: a NAMED knob, not a
8
+ * literal, so a release bump is one obvious edit here. PINNED (never floating
9
+ * `latest`) so a box boot is reproducible: the same cloud-init always installs
10
+ * the same agent. Overridable per-box via {@link ProvisionInput.pinnaceVersion}.
11
+ * `pinnace@0.1.0` is the first published release (npm, public, OIDC provenance).
12
+ */
13
+ const DEFAULT_PINNACE_VERSION = '0.2.0';
14
+ /**
15
+ * The pinned Node.js major the box installs via NodeSource (`setup_<this>.x`).
16
+ * Node 22 is a current active LTS; Node 20 (the old literal) is the OLDEST LTS
17
+ * (EOL ~2026-04) and incoherent with the repo's own Node 24 toolchain. A NAMED
18
+ * knob (mirrors {@link DEFAULT_KUBO_VERSION}) so the LTS bump is one edit.
19
+ * Overridable per-box via {@link ProvisionInput.nodeMajor}.
20
+ */
21
+ const DEFAULT_NODE_MAJOR = '22';
5
22
  const DEFAULT_SITES_DIR = '/sites';
6
23
  const DEFAULT_GATEWAYS = [
7
24
  'https://{cid}.ipfs.dweb.link/',
@@ -88,6 +105,8 @@ function renderHetznerCloudInit(input) {
88
105
  const publisherEndpoint = input.role === 'replica' ? (input.publisherEndpoint ?? '') : '';
89
106
  const gateways = input.gateways ?? DEFAULT_GATEWAYS;
90
107
  const kuboVersion = input.kuboVersion ?? DEFAULT_KUBO_VERSION;
108
+ const pinnaceVersion = input.pinnaceVersion ?? DEFAULT_PINNACE_VERSION;
109
+ const nodeMajor = input.nodeMajor ?? DEFAULT_NODE_MAJOR;
91
110
  const sitesDir = input.sitesDir ?? DEFAULT_SITES_DIR;
92
111
  const warmGateways = gateways.join(' ');
93
112
  const timerUnits = TIMERS.map(renderTimerUnits).join('\n');
@@ -123,6 +142,22 @@ packages:
123
142
  - debian-archive-keyring
124
143
  - apt-transport-https
125
144
 
145
+ # ---------------------------------------------------------------------------
146
+ # Service users. cloud-init's \`users:\` module runs BEFORE \`runcmd\`, so the
147
+ # dedicated \`ipfs\` user is GUARANTEED to exist before any boot step uses it
148
+ # (e.g. \`install -o ipfs ...\`). Creating it here (not mid-\`ipfs-setup.sh\`,
149
+ # which is a \`set -e\` block that could abort before its \`useradd\`) closes the
150
+ # first-boot race where a later step hit "invalid user 'ipfs'".
151
+ # \`default\` keeps cloud-init's normal login user; we only ADD \`ipfs\`.
152
+ # ---------------------------------------------------------------------------
153
+ users:
154
+ - default
155
+ - name: ipfs
156
+ system: true
157
+ home: /var/lib/ipfs
158
+ shell: /usr/sbin/nologin
159
+ lock_passwd: true
160
+
126
161
  # ---------------------------------------------------------------------------
127
162
  # On-box environment consumed by the pinnace node timers + the setup scripts.
128
163
  # ---------------------------------------------------------------------------
@@ -145,6 +180,21 @@ write_files:
145
180
  # MFS directory that holds your sites (one entry per site).
146
181
  SITES_DIR="${sitesDir}"
147
182
 
183
+ # On-box PATHS the \`pinnace node\` verbs read (they assemble their
184
+ # NodeCommandContext from these keys):
185
+ # DASHBOARD_DIR - where \`node status\` writes status.json (the dashboard
186
+ # vhost \${DASH_DOMAIN} serves this dir).
187
+ # RECORDS_DIR - where \`node republish\` (publisher) EXPORTS the signed
188
+ # record; UNDER the dashboard dir so it is served at
189
+ # \${DASH_DOMAIN}/records/<id>.ipns-record, exactly where a
190
+ # replica's PUBLISHER_ENDPOINT + /records/ fetch looks.
191
+ # CACHE_DIR - where \`node mirror\` (replica) CACHES the last good
192
+ # record for the publisher-outage fallback (under the
193
+ # ipfs user's writable home).
194
+ DASHBOARD_DIR="/var/www/ipfs-dash"
195
+ RECORDS_DIR="/var/www/ipfs-dash/records"
196
+ CACHE_DIR="/var/lib/ipfs/records-cache"
197
+
148
198
  # Role: "publisher" holds IPNS keys and signs/exports records; "replica"
149
199
  # holds NO key and MIRRORS the publisher's signed records. The pinnace node
150
200
  # timers self-gate on this, so scheduling all timers on every box is safe.
@@ -155,6 +205,12 @@ write_files:
155
205
 
156
206
  KUBO_VERSION="${kuboVersion}"
157
207
 
208
+ # Node.js major installed via NodeSource (setup_<major>.x) and the PINNED
209
+ # pinnace version installed on the box. Both are reproducible: the same
210
+ # cloud-init always installs the same agent on the same runtime.
211
+ NODE_MAJOR="${nodeMajor}"
212
+ PINNACE_VERSION="${pinnaceVersion}"
213
+
158
214
  # -- Kubo installer / initializer -----------------------------------------
159
215
  - path: /usr/local/sbin/ipfs-setup.sh
160
216
  permissions: "0755"
@@ -253,13 +309,19 @@ write_files:
253
309
  content: |
254
310
  #!/usr/bin/env bash
255
311
  set -euo pipefail
256
- # Install Node.js (for npm) then the pinnace binary globally. The on-box
257
- # timers invoke \`pinnace node <verb>\`: one codebase, client + on-box.
312
+ source /etc/pinnace-node.env
313
+ # Install Node.js (for npm) then the PINNED pinnace binary globally. The
314
+ # on-box timers invoke \`pinnace node <verb>\`: one codebase, client +
315
+ # on-box. NODE_MAJOR / PINNACE_VERSION come from /etc/pinnace-node.env so
316
+ # the runtime + agent version are reproducible and one-edit overridable.
317
+ # This script is invoked NON-FATALLY at boot (\`|| true\`): a transient
318
+ # npm/registry hiccup must NOT abort provisioning (Kubo, the firewall and
319
+ # Caddy are already up by the time this runs). Re-run it manually to retry.
258
320
  if ! command -v npm >/dev/null 2>&1; then
259
- curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
321
+ curl -fsSL "https://deb.nodesource.com/setup_\${NODE_MAJOR}.x" | bash -
260
322
  apt-get install -y nodejs
261
323
  fi
262
- npm install -g pinnace
324
+ npm install -g "pinnace@\${PINNACE_VERSION}"
263
325
  pinnace version
264
326
 
265
327
  # -- Caddy reverse proxy for the HTTPS API (auto TLS) ---------------------
@@ -335,12 +397,20 @@ runcmd:
335
397
  # Configure the HTTPS API proxy (no-op if API_DOMAIN unset)
336
398
  - /usr/local/sbin/write-caddyfile.sh
337
399
 
338
- # Install the pinnace binary (the box runs the same CLI as the client).
339
- - /usr/local/sbin/pinnace-setup.sh
400
+ # Dashboard dir, owned by the ipfs service user (guaranteed to exist: created
401
+ # by the \`users:\` module above, BEFORE runcmd). Done BEFORE the pinnace
402
+ # install so a transient install failure can never skip it.
403
+ - install -d -o ipfs -g ipfs /var/www/ipfs-dash
404
+
405
+ # Install the pinned pinnace binary (the box runs the same CLI as the client).
406
+ # BOOT-SAFE: \`|| true\` so a transient npm/registry failure does NOT abort the
407
+ # boot (Kubo, the firewall and Caddy are already up). Re-run
408
+ # /usr/local/sbin/pinnace-setup.sh manually to retry; the timers below pick it
409
+ # up on their next tick once the binary is present.
410
+ - /usr/local/sbin/pinnace-setup.sh || true
340
411
 
341
412
  # Enable the pinnace node timers. republish self-gates to publisher, mirror to
342
413
  # replica, so enabling all of them on every box is safe (ADR-0002).
343
- - install -d -o ipfs -g ipfs /var/www/ipfs-dash
344
414
  ${enableTimers}
345
415
 
346
416
  # =============================================================================
@@ -1 +1 @@
1
- {"version":3,"file":"cloud-init.js","sourceRoot":"","sources":["../../src/provision/cloud-init.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cloud-init.js","sourceRoot":"","sources":["../../src/provision/cloud-init.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinnace",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "license": "AGPL-3.0-only",
5
5
  "description": "Self-host a static website on IPFS across one or more self-owned Kubo nodes.",
6
6
  "repository": {
package/src/cli/run.ts CHANGED
@@ -15,14 +15,48 @@
15
15
  * read or mutated — mirroring the `NodeCommandOps` injectable-ops pattern in
16
16
  * node-commands and the explicit-`env` resolver in config-resolution).
17
17
  *
18
- * The on-box `pinnace node <verb>` and the `pinnace site <verb>` namespaces are
19
- * validated here for surface coherence (one CLI), but their full dispatch is
20
- * wired by their own tasks (node-agent-commands, site-management).
18
+ * The on-box `pinnace node <verb>` and the `pinnace site <verb>` namespaces,
19
+ * the `promote` verb, dispatch through the SAME {@link RunContext}/
20
+ * {@link ClientDeps} seam (they are NOT a forked dispatch idiom): the on-box
21
+ * `node` verbs assemble a {@link NodeCommandContext} from the box env
22
+ * (`/etc/pinnace-node.env`, exported into `process.env` by the systemd timer's
23
+ * `EnvironmentFile`) and call the core `runNodeCommand`; `site`/`promote`
24
+ * assemble a per-host {@link KuboRpcClient} from the resolved config and call
25
+ * the site / promote core.
21
26
  */
22
27
  import {readFileSync} from 'node:fs';
23
28
  import {name} from '../index.js';
24
- import {NODE_VERBS, type NodeVerb} from '../node/node-commands.js';
25
- import {SITE_VERBS, type SiteVerb} from '../site/site-management.js';
29
+ import {
30
+ NODE_VERBS,
31
+ runNodeCommand as coreRunNodeCommand,
32
+ type NodeVerb,
33
+ type NodeCommandContext,
34
+ type NodeCommandResult,
35
+ } from '../node/node-commands.js';
36
+ import {
37
+ SITE_VERBS,
38
+ listSites as coreListSites,
39
+ removeSite as coreRemoveSite,
40
+ addSite as coreAddSite,
41
+ type SiteVerb,
42
+ type ListSitesInput,
43
+ type SiteListing,
44
+ type RemoveSiteInput,
45
+ type RemoveSiteResult,
46
+ type AddSiteInput,
47
+ type AddSiteResult,
48
+ } from '../site/site-management.js';
49
+ import {makeStatusOp} from '../status/status-report.js';
50
+ import {
51
+ promoteReplicaToPublisher as corePromoteReplicaToPublisher,
52
+ type PromoteReplicaInput,
53
+ type PromoteReplicaResult,
54
+ } from '../publisher/record-sequence.js';
55
+ import {
56
+ deriveIpnsKey as coreDeriveIpnsKey,
57
+ type DeriveIpnsInput as DeriveIpnsKeyInput,
58
+ type DerivedIpnsKey,
59
+ } from '../derive/ipns-key-derivation.js';
26
60
  import {KuboRpcClient} from '../rpc/kubo-rpc-client.js';
27
61
  import {
28
62
  provision as coreProvision,
@@ -81,6 +115,23 @@ export interface ClientDeps {
81
115
  statusReport(input: StatusReportInput): Promise<StatusReport>;
82
116
  /** `derive` -> the master + site `id` -> IPNS id derivation (no deploy). */
83
117
  deriveIpnsId(input: DeriveIpnsInput): string;
118
+ /** `node <verb>` -> the on-box command runner (context assembled by the CLI). */
119
+ runNodeCommand(
120
+ verb: NodeVerb,
121
+ ctx: NodeCommandContext,
122
+ ): Promise<NodeCommandResult>;
123
+ /** `site list` -> enumerate the node's MFS sites. */
124
+ listSites(input: ListSitesInput): Promise<SiteListing[]>;
125
+ /** `site remove` -> drop an MFS site + unpin its content. */
126
+ removeSite(input: RemoveSiteInput): Promise<RemoveSiteResult>;
127
+ /** `site add` -> place an already-imported CID into MFS as a site. */
128
+ addSite(input: AddSiteInput): Promise<AddSiteResult>;
129
+ /** `promote` -> derive the per-site key material from the master + `id`. */
130
+ deriveIpnsKey(input: DeriveIpnsKeyInput): DerivedIpnsKey;
131
+ /** `promote` -> import the key + flip the node's role to publisher (story 14). */
132
+ promoteReplicaToPublisher(
133
+ input: PromoteReplicaInput,
134
+ ): Promise<PromoteReplicaResult>;
84
135
  }
85
136
 
86
137
  /** The real core, used when a caller does not inject stubs. */
@@ -90,6 +141,12 @@ const DEFAULT_DEPS: ClientDeps = {
90
141
  emitCi: coreEmitCi,
91
142
  statusReport: coreStatusReport,
92
143
  deriveIpnsId: coreDeriveIpnsId,
144
+ runNodeCommand: coreRunNodeCommand,
145
+ listSites: coreListSites,
146
+ removeSite: coreRemoveSite,
147
+ addSite: coreAddSite,
148
+ deriveIpnsKey: coreDeriveIpnsKey,
149
+ promoteReplicaToPublisher: corePromoteReplicaToPublisher,
93
150
  };
94
151
 
95
152
  /**
@@ -263,6 +320,9 @@ export async function run(
263
320
  if (command === 'derive' || command === 'ipns-id') {
264
321
  return runDerive(rest, rc);
265
322
  }
323
+ if (command === 'promote') {
324
+ return runPromote(rest, rc);
325
+ }
266
326
 
267
327
  rc.err(`${name()}: unknown command '${command}'`);
268
328
  return 1;
@@ -340,9 +400,11 @@ function parseArgs(argv: readonly string[]): ParsedArgs {
340
400
 
341
401
  /**
342
402
  * `provision --host <h> --api-domain <d> --acme-email <e> --bearer-token <t>
343
- * --role <r> [...]` -> core {@link ClientDeps.provision}. Purely arg-driven
344
- * (provisioning inputs are per-box and not stored in `pinnace.json`); prints the
345
- * generated cloud-init to stdout.
403
+ * --role <r> [--pinnace-version <v>] [--node-major <n>] [--kubo-version <v>]
404
+ * [...]` -> core {@link ClientDeps.provision}. Purely arg-driven (provisioning
405
+ * inputs are per-box and not stored in `pinnace.json`); prints the generated
406
+ * cloud-init to stdout. The pinned pinnace/Node/Kubo versions default to the
407
+ * generator's named knobs and are overridable per-box via these flags.
346
408
  */
347
409
  function runProvision(argv: readonly string[], rc: ResolvedRunContext): number {
348
410
  const {flags} = parseArgs(argv);
@@ -380,6 +442,9 @@ function runProvision(argv: readonly string[], rc: ResolvedRunContext): number {
380
442
  input.dashboardDomain = flags['dashboard-domain'];
381
443
  if (flags['publisher-endpoint'])
382
444
  input.publisherEndpoint = flags['publisher-endpoint'];
445
+ if (flags['kubo-version']) input.kuboVersion = flags['kubo-version'];
446
+ if (flags['pinnace-version']) input.pinnaceVersion = flags['pinnace-version'];
447
+ if (flags['node-major']) input.nodeMajor = flags['node-major'];
383
448
 
384
449
  const result = rc.deps.provision(input);
385
450
  rc.out(result.cloudInit.contents);
@@ -611,14 +676,22 @@ function cliOverridesFromFlags(flags: Record<string, string>): CliOverrides {
611
676
  }
612
677
 
613
678
  /**
614
- * Parse `pinnace site <verb>` and validate the verb. The full context (Kubo
615
- * client from config-resolution, the site name / CID args) is assembled by the
616
- * CLI wiring in the site-management task; this thin router validates the verb
617
- * belongs to the `site` namespace. The three verbs (list/remove/add) are
618
- * implemented in `../site/site-management.ts`.
679
+ * `pinnace site <verb> [args] [--host <name>]` -> the site core
680
+ * ({@link ClientDeps.listSites}/{@link ClientDeps.removeSite}/
681
+ * {@link ClientDeps.addSite}). Assembles a per-host {@link KuboRpcClient} from
682
+ * the resolved config (endpoint) + the host's env-only token, then dispatches:
683
+ * - `list` -> listSites({client})
684
+ * - `remove <id>` -> removeSite({client, id})
685
+ * - `add <id> <cid>` -> addSite({client, id, cid})
686
+ * The `--host <name>` selects which configured node to act on; it may be
687
+ * omitted only when the config has exactly one host (see {@link selectHost}).
619
688
  */
620
- function runSiteCli(argv: readonly string[], rc: ResolvedRunContext): number {
621
- const [verb] = argv;
689
+ async function runSiteCli(
690
+ argv: readonly string[],
691
+ rc: ResolvedRunContext,
692
+ ): Promise<number> {
693
+ const {flags, positionals} = parseArgs(argv);
694
+ const [verb, ...verbArgs] = positionals;
622
695
  if (!verb) {
623
696
  rc.err(`pinnace site: expected a verb (${SITE_VERBS.join(', ')})`);
624
697
  return 1;
@@ -629,16 +702,68 @@ function runSiteCli(argv: readonly string[], rc: ResolvedRunContext): number {
629
702
  );
630
703
  return 1;
631
704
  }
705
+
706
+ const cli = cliOverridesFromFlags(flags);
707
+ const cfg = resolveConfig({file: rc.file, env: rc.env, cli});
708
+ const client = buildHostClient('pinnace site', flags['host'], cfg, rc, cli);
709
+ if (!client) return 1; // buildHostClient already emitted the loud error.
710
+
711
+ if (verb === 'list') {
712
+ const sites = await rc.deps.listSites({client});
713
+ for (const site of sites) {
714
+ rc.out(
715
+ `${site.id}: cid ${site.cid}${site.ipns ? ` ipns ${site.ipns}` : ''}`,
716
+ );
717
+ }
718
+ return 0;
719
+ }
720
+ if (verb === 'remove') {
721
+ const [id] = verbArgs;
722
+ if (!id) {
723
+ rc.err(
724
+ 'pinnace site remove: usage: pinnace site remove <id> [--host <name>]',
725
+ );
726
+ return 1;
727
+ }
728
+ const result = await rc.deps.removeSite({client, id});
729
+ rc.out(
730
+ `removed ${result.id}${result.cid ? ` (cid ${result.cid}, unpinned=${result.unpinned})` : ''}`,
731
+ );
732
+ return 0;
733
+ }
734
+ // verb === 'add'
735
+ const [id, cid] = verbArgs;
736
+ if (!id || !cid) {
737
+ rc.err(
738
+ 'pinnace site add: usage: pinnace site add <id> <cid> [--host <name>]',
739
+ );
740
+ return 1;
741
+ }
742
+ const result = await rc.deps.addSite({client, id, cid});
743
+ rc.out(`added ${result.id} -> ${result.cid}`);
632
744
  return 0;
633
745
  }
634
746
 
635
747
  /**
636
- * Parse `pinnace node <verb>` and validate the verb. The full on-box context
637
- * (local Kubo client, role, on-box paths) is assembled by the cloud-init /
638
- * config-resolution wiring in the node-agent-commands task; this thin router
639
- * only validates the verb belongs to the `node` namespace.
748
+ * `pinnace node <verb>` -> the on-box command core ({@link
749
+ * ClientDeps.runNodeCommand}). This is the load-bearing on-box wiring: the verb
750
+ * runs ON a provisioned box (invoked by the systemd timer, whose
751
+ * `EnvironmentFile=/etc/pinnace-node.env` exports the box config into the
752
+ * environment {@link RunContext.env} reads). It assembles a
753
+ * {@link NodeCommandContext} from that env — a LOCAL Kubo client (127.0.0.1:5001
754
+ * + `RPC_BEARER_TOKEN`), the box `role` (`NODE_ROLE`), and the on-box paths
755
+ * (`RECORDS_DIR`/`CACHE_DIR`/`DASHBOARD_DIR`/`SITES_DIR`), the replica
756
+ * `PUBLISHER_ENDPOINT`, and the `WARM_GATEWAYS` list — and injects the OWNED
757
+ * `status` op ({@link makeStatusOp}, the real per-site announce/gateway report)
758
+ * so the production status path is not the thin default stub. It then invokes
759
+ * the core (NOT a validate-and-return-0). A live run proved that without this
760
+ * the `republish`/`mirror` timers are no-ops. The token is env-only and its
761
+ * absence is a LOUD, named error (never a silent empty bearer / 401).
640
762
  */
641
- function runNodeCli(argv: readonly string[], rc: ResolvedRunContext): number {
763
+ async function runNodeCli(
764
+ argv: readonly string[],
765
+ rc: ResolvedRunContext,
766
+ ): Promise<number> {
642
767
  const [verb] = argv;
643
768
  if (!verb) {
644
769
  rc.err(`pinnace node: expected a verb (${NODE_VERBS.join(', ')})`);
@@ -650,5 +775,196 @@ function runNodeCli(argv: readonly string[], rc: ResolvedRunContext): number {
650
775
  );
651
776
  return 1;
652
777
  }
778
+
779
+ const env = rc.env;
780
+ const token = env['RPC_BEARER_TOKEN'];
781
+ if (!token) {
782
+ rc.err(
783
+ 'pinnace node: RPC_BEARER_TOKEN not set — the on-box bearer is env-only ' +
784
+ '(read from /etc/pinnace-node.env); never a silent empty token',
785
+ );
786
+ return 1;
787
+ }
788
+ const role = env['NODE_ROLE'];
789
+ if (role !== 'publisher' && role !== 'replica') {
790
+ rc.err(
791
+ "pinnace node: NODE_ROLE must be 'publisher' or 'replica' " +
792
+ '(read from /etc/pinnace-node.env)',
793
+ );
794
+ return 1;
795
+ }
796
+
797
+ // The box's OWN daemon: the local Kubo RPC on 127.0.0.1:5001 (Caddy fronts it
798
+ // for external clients, but the on-box agent speaks to it directly).
799
+ const client = new KuboRpcClient({
800
+ baseUrl: 'http://127.0.0.1:5001',
801
+ token,
802
+ });
803
+
804
+ const ctx: NodeCommandContext = {
805
+ client,
806
+ role,
807
+ // The production status path uses the OWNED status op (real announce +
808
+ // gateway report), not the thin defaultStatus stand-in.
809
+ ops: {status: makeStatusOp()},
810
+ };
811
+ if (env['SITES_DIR']) ctx.sitesDir = env['SITES_DIR'];
812
+ if (env['RECORDS_DIR']) ctx.recordsDir = env['RECORDS_DIR'];
813
+ if (env['CACHE_DIR']) ctx.cacheDir = env['CACHE_DIR'];
814
+ if (env['DASHBOARD_DIR']) ctx.dashboardDir = env['DASHBOARD_DIR'];
815
+ if (env['PUBLISHER_ENDPOINT'])
816
+ ctx.publisherEndpoint = env['PUBLISHER_ENDPOINT'];
817
+ const gateways = splitWarmGateways(env['WARM_GATEWAYS']);
818
+ if (gateways.length > 0) ctx.gateways = gateways;
819
+
820
+ const result = await rc.deps.runNodeCommand(verb as NodeVerb, ctx);
821
+ if (result.skipped) {
822
+ rc.out(`pinnace node ${verb}: skipped (${result.skippedReason})`);
823
+ return 0;
824
+ }
825
+ for (const site of result.sites) {
826
+ rc.out(
827
+ ` ${site.id}${site.cid ? ` cid ${site.cid}` : ''}${site.status ? ` (${site.status})` : ''}`,
828
+ );
829
+ }
653
830
  return 0;
654
831
  }
832
+
833
+ /**
834
+ * `pinnace promote <id> [--host <name>]` -> {@link
835
+ * ClientDeps.promoteReplicaToPublisher} (spec user story 14). Derives the
836
+ * per-site key from the env-only master + the site `id` (the KDF input,
837
+ * {@link ClientDeps.deriveIpnsKey}), assembles the chosen host's client, and
838
+ * promotes it: import the key + flip the role to publisher, recovering the name
839
+ * within the record's validity window without content downtime. The master is
840
+ * env-ONLY (never from `pinnace.json`); its absence is a LOUD error.
841
+ */
842
+ async function runPromote(
843
+ argv: readonly string[],
844
+ rc: ResolvedRunContext,
845
+ ): Promise<number> {
846
+ const {flags, positionals} = parseArgs(argv);
847
+ const [siteId] = positionals;
848
+ if (!siteId) {
849
+ rc.err('pinnace promote: usage: pinnace promote <id> [--host <name>]');
850
+ return 1;
851
+ }
852
+
853
+ const master = resolveMasterSecret({env: rc.env});
854
+ if (!master) {
855
+ rc.err(
856
+ 'pinnace promote: master secret not set — export PINNACE_MASTER (env-only; never read from pinnace.json)',
857
+ );
858
+ return 1;
859
+ }
860
+
861
+ const cli = cliOverridesFromFlags(flags);
862
+ const cfg = resolveConfig({file: rc.file, env: rc.env, cli});
863
+ const host = pickHost('pinnace promote', flags['host'], cfg, rc);
864
+ if (!host) return 1;
865
+ const client = clientForHost('pinnace promote', host, rc, cli);
866
+ if (!client) return 1;
867
+
868
+ // The single `id` IS the KDF input (no separate keyId), matching `derive`.
869
+ const id = cfg.sites.find((s) => s.id === siteId)?.id ?? siteId;
870
+ const derived = rc.deps.deriveIpnsKey({master, keyId: id});
871
+ const result = await rc.deps.promoteReplicaToPublisher({
872
+ client,
873
+ currentRole: host.role,
874
+ keyName: id,
875
+ derived,
876
+ });
877
+ rc.out(
878
+ `promoted ${result.keyName} to ${result.role}${result.ipns ? ` (ipns ${result.ipns})` : ''}`,
879
+ );
880
+ return 0;
881
+ }
882
+
883
+ // ---------------------------------------------------------------------------
884
+ // Host selection + client assembly (shared by site + promote).
885
+ // ---------------------------------------------------------------------------
886
+
887
+ /**
888
+ * Pick the configured host to act on. `--host <name>` selects it by name; it
889
+ * may be omitted ONLY when the config has exactly one host (the unambiguous
890
+ * case). Zero hosts, an unknown name, or an omitted name with several hosts is
891
+ * a LOUD, specific error (returns undefined after emitting it).
892
+ */
893
+ function pickHost(
894
+ prefix: string,
895
+ hostName: string | undefined,
896
+ cfg: {hosts: Array<{name: string; endpoint: string; role: HostRole}>},
897
+ rc: ResolvedRunContext,
898
+ ): {name: string; endpoint: string; role: HostRole} | undefined {
899
+ if (cfg.hosts.length === 0) {
900
+ rc.err(`${prefix}: no hosts configured (add hosts to pinnace.json)`);
901
+ return undefined;
902
+ }
903
+ if (hostName) {
904
+ const match = cfg.hosts.find((h) => h.name === hostName);
905
+ if (!match) {
906
+ rc.err(
907
+ `${prefix}: unknown host '${hostName}'; configured hosts: ${cfg.hosts
908
+ .map((h) => h.name)
909
+ .join(', ')}`,
910
+ );
911
+ return undefined;
912
+ }
913
+ return match;
914
+ }
915
+ if (cfg.hosts.length > 1) {
916
+ rc.err(
917
+ `${prefix}: multiple hosts configured; pass --host <name> (one of ${cfg.hosts
918
+ .map((h) => h.name)
919
+ .join(', ')})`,
920
+ );
921
+ return undefined;
922
+ }
923
+ return cfg.hosts[0];
924
+ }
925
+
926
+ /**
927
+ * Build a {@link KuboRpcClient} for a chosen host, resolving its bearer token
928
+ * env-only (CLI > env, no file). A missing token is a LOUD, named error
929
+ * (returns undefined after emitting it).
930
+ */
931
+ function clientForHost(
932
+ prefix: string,
933
+ host: {name: string; endpoint: string},
934
+ rc: ResolvedRunContext,
935
+ cli: CliOverrides,
936
+ ): KuboRpcClient | undefined {
937
+ let token: string;
938
+ try {
939
+ token = resolveHostToken({hostName: host.name, env: rc.env, cli});
940
+ } catch (error) {
941
+ if (error instanceof MissingHostTokenError) {
942
+ rc.err(`${prefix}: ${error.message}`);
943
+ return undefined;
944
+ }
945
+ throw error;
946
+ }
947
+ return new KuboRpcClient({baseUrl: host.endpoint, token});
948
+ }
949
+
950
+ /** Pick a host AND build its client in one step (the common site path). */
951
+ function buildHostClient(
952
+ prefix: string,
953
+ hostName: string | undefined,
954
+ cfg: {hosts: Array<{name: string; endpoint: string; role: HostRole}>},
955
+ rc: ResolvedRunContext,
956
+ cli: CliOverrides,
957
+ ): KuboRpcClient | undefined {
958
+ const host = pickHost(prefix, hostName, cfg, rc);
959
+ if (!host) return undefined;
960
+ return clientForHost(prefix, host, rc, cli);
961
+ }
962
+
963
+ /** Split the space-separated `WARM_GATEWAYS` env value into a template list. */
964
+ function splitWarmGateways(value: string | undefined): string[] {
965
+ if (!value) return [];
966
+ return value
967
+ .split(/\s+/)
968
+ .map((s) => s.trim())
969
+ .filter(Boolean);
970
+ }