pinnace 0.0.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.
Files changed (83) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +164 -0
  3. package/dist/car/car-build.d.ts +26 -0
  4. package/dist/car/car-build.d.ts.map +1 -0
  5. package/dist/car/car-build.js +113 -0
  6. package/dist/car/car-build.js.map +1 -0
  7. package/dist/ci/ci-emit.d.ts +116 -0
  8. package/dist/ci/ci-emit.d.ts.map +1 -0
  9. package/dist/ci/ci-emit.js +190 -0
  10. package/dist/ci/ci-emit.js.map +1 -0
  11. package/dist/cli/bin.d.ts +3 -0
  12. package/dist/cli/bin.d.ts.map +1 -0
  13. package/dist/cli/bin.js +17 -0
  14. package/dist/cli/bin.js.map +1 -0
  15. package/dist/cli/run.d.ts +94 -0
  16. package/dist/cli/run.d.ts.map +1 -0
  17. package/dist/cli/run.js +731 -0
  18. package/dist/cli/run.js.map +1 -0
  19. package/dist/config/config-resolution.d.ts +186 -0
  20. package/dist/config/config-resolution.d.ts.map +1 -0
  21. package/dist/config/config-resolution.js +137 -0
  22. package/dist/config/config-resolution.js.map +1 -0
  23. package/dist/deploy/deploy.d.ts +121 -0
  24. package/dist/deploy/deploy.d.ts.map +1 -0
  25. package/dist/deploy/deploy.js +150 -0
  26. package/dist/deploy/deploy.js.map +1 -0
  27. package/dist/derive/ipns-key-derivation.d.ts +37 -0
  28. package/dist/derive/ipns-key-derivation.d.ts.map +1 -0
  29. package/dist/derive/ipns-key-derivation.js +130 -0
  30. package/dist/derive/ipns-key-derivation.js.map +1 -0
  31. package/dist/index.d.ts +27 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +29 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/node/node-commands.d.ts +115 -0
  36. package/dist/node/node-commands.d.ts.map +1 -0
  37. package/dist/node/node-commands.js +231 -0
  38. package/dist/node/node-commands.js.map +1 -0
  39. package/dist/provision/cloud-init.d.ts +162 -0
  40. package/dist/provision/cloud-init.d.ts.map +1 -0
  41. package/dist/provision/cloud-init.js +460 -0
  42. package/dist/provision/cloud-init.js.map +1 -0
  43. package/dist/publisher/key-import.d.ts +105 -0
  44. package/dist/publisher/key-import.d.ts.map +1 -0
  45. package/dist/publisher/key-import.js +76 -0
  46. package/dist/publisher/key-import.js.map +1 -0
  47. package/dist/publisher/record-sequence.d.ts +90 -0
  48. package/dist/publisher/record-sequence.d.ts.map +1 -0
  49. package/dist/publisher/record-sequence.js +250 -0
  50. package/dist/publisher/record-sequence.js.map +1 -0
  51. package/dist/rpc/kubo-rpc-client.d.ts +154 -0
  52. package/dist/rpc/kubo-rpc-client.d.ts.map +1 -0
  53. package/dist/rpc/kubo-rpc-client.js +207 -0
  54. package/dist/rpc/kubo-rpc-client.js.map +1 -0
  55. package/dist/rpc/mock-kubo.d.ts +87 -0
  56. package/dist/rpc/mock-kubo.d.ts.map +1 -0
  57. package/dist/rpc/mock-kubo.js +129 -0
  58. package/dist/rpc/mock-kubo.js.map +1 -0
  59. package/dist/site/site-management.d.ts +126 -0
  60. package/dist/site/site-management.d.ts.map +1 -0
  61. package/dist/site/site-management.js +123 -0
  62. package/dist/site/site-management.js.map +1 -0
  63. package/dist/status/status-report.d.ts +145 -0
  64. package/dist/status/status-report.d.ts.map +1 -0
  65. package/dist/status/status-report.js +192 -0
  66. package/dist/status/status-report.js.map +1 -0
  67. package/package.json +45 -2
  68. package/src/car/car-build.ts +135 -0
  69. package/src/ci/ci-emit.ts +284 -0
  70. package/src/cli/bin.ts +20 -0
  71. package/src/cli/run.ts +970 -0
  72. package/src/config/config-resolution.ts +264 -0
  73. package/src/deploy/deploy.ts +250 -0
  74. package/src/derive/ipns-key-derivation.ts +173 -0
  75. package/src/index.ts +146 -0
  76. package/src/node/node-commands.ts +395 -0
  77. package/src/provision/cloud-init.ts +646 -0
  78. package/src/publisher/key-import.ts +141 -0
  79. package/src/publisher/record-sequence.ts +336 -0
  80. package/src/rpc/kubo-rpc-client.ts +281 -0
  81. package/src/rpc/mock-kubo.ts +194 -0
  82. package/src/site/site-management.ts +241 -0
  83. package/src/status/status-report.ts +291 -0
@@ -0,0 +1,460 @@
1
+ /** The hosts, in a stable order (help text / iteration / validation). */
2
+ export const HOST_PROVIDERS = ['hetzner'];
3
+ /** Defaults kept in one place so the template + docs never drift. */
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';
22
+ const DEFAULT_SITES_DIR = '/sites';
23
+ const DEFAULT_GATEWAYS = [
24
+ 'https://{cid}.ipfs.dweb.link/',
25
+ 'https://{cid}.ipfs.cf-ipfs.com/',
26
+ 'https://ipfs.io/ipfs/{cid}',
27
+ ];
28
+ /**
29
+ * The four timers, ported from the reference bash-unit cadences but pointed at
30
+ * `pinnace node <verb>` instead of the bash scripts (ADR-0002). Order is stable
31
+ * (deterministic output). Cadences mirror the reference: IPNS republish/mirror
32
+ * well under record expiry, warm/status more frequent.
33
+ */
34
+ const TIMERS = [
35
+ {
36
+ verb: 'republish',
37
+ description: 'Republish + export IPNS records (publisher role)',
38
+ onBootSec: '8min',
39
+ onUnitActiveSec: '6h',
40
+ },
41
+ {
42
+ verb: 'mirror',
43
+ description: 'Mirror publisher IPNS records (replica role)',
44
+ onBootSec: '9min',
45
+ onUnitActiveSec: '3h',
46
+ },
47
+ {
48
+ verb: 'warm',
49
+ description: 'Warm public IPFS gateway caches for our CIDs',
50
+ onBootSec: '5min',
51
+ onUnitActiveSec: '30min',
52
+ },
53
+ {
54
+ verb: 'status',
55
+ description: 'Regenerate the per-site status JSON for the dashboard',
56
+ onBootSec: '6min',
57
+ onUnitActiveSec: '15min',
58
+ },
59
+ ];
60
+ /** Render the `pinnace-<verb>` systemd service + timer unit pair. */
61
+ function renderTimerUnits(t) {
62
+ return ` - path: /etc/systemd/system/pinnace-${t.verb}.service
63
+ permissions: "0644"
64
+ owner: root:root
65
+ content: |
66
+ [Unit]
67
+ Description=${t.description}
68
+ After=ipfs.service
69
+
70
+ [Service]
71
+ Type=oneshot
72
+ User=ipfs
73
+ Group=ipfs
74
+ EnvironmentFile=/etc/pinnace-node.env
75
+ Environment=IPFS_PATH=/var/lib/ipfs/.ipfs
76
+ ExecStart=/usr/local/bin/pinnace node ${t.verb}
77
+
78
+ - path: /etc/systemd/system/pinnace-${t.verb}.timer
79
+ permissions: "0644"
80
+ owner: root:root
81
+ content: |
82
+ [Unit]
83
+ Description=Schedule pinnace node ${t.verb}
84
+
85
+ [Timer]
86
+ OnBootSec=${t.onBootSec}
87
+ OnUnitActiveSec=${t.onUnitActiveSec}
88
+ Persistent=true
89
+
90
+ [Install]
91
+ WantedBy=timers.target
92
+ `;
93
+ }
94
+ /**
95
+ * Render the full Hetzner cloud-init. Deterministic: same input -> byte-
96
+ * identical output (snapshot-locked). Encodes the hardened-node invariants
97
+ * (ufw 4001 TCP+UDP + 443, never 5001 raw; localhost-bound API/gateway;
98
+ * AcceleratedDHTClient + reprovide + `Routing.Type auto`; hardened systemd
99
+ * Kubo unit; Caddy HTTPS + bearer proxy) and schedules the `pinnace node`
100
+ * timers (NOT bash).
101
+ */
102
+ function renderHetznerCloudInit(input) {
103
+ const dashboardDomain = input.dashboardDomain ?? '';
104
+ const corsOrigins = input.corsOrigins ?? (dashboardDomain ? `https://${dashboardDomain}` : '');
105
+ const publisherEndpoint = input.role === 'replica' ? (input.publisherEndpoint ?? '') : '';
106
+ const gateways = input.gateways ?? DEFAULT_GATEWAYS;
107
+ const kuboVersion = input.kuboVersion ?? DEFAULT_KUBO_VERSION;
108
+ const pinnaceVersion = input.pinnaceVersion ?? DEFAULT_PINNACE_VERSION;
109
+ const nodeMajor = input.nodeMajor ?? DEFAULT_NODE_MAJOR;
110
+ const sitesDir = input.sitesDir ?? DEFAULT_SITES_DIR;
111
+ const warmGateways = gateways.join(' ');
112
+ const timerUnits = TIMERS.map(renderTimerUnits).join('\n');
113
+ const enableTimers = TIMERS.map((t) => ` - systemctl enable --now pinnace-${t.verb}.timer`).join('\n');
114
+ return `#cloud-config
115
+ # =============================================================================
116
+ # ${input.host} Cloud -> self-hosted IPFS (Kubo) node for a static website.
117
+ #
118
+ # Generated by \`pinnace provision --host ${input.host}\`. This is the programmatic
119
+ # successor to the shell \`sed\`-template prototype (superseded).
120
+ #
121
+ # What this gives you:
122
+ # - Kubo running as a hardened systemd service (user: ipfs)
123
+ # - Swarm port 4001 (TCP+UDP) open so public gateways can dial you
124
+ # - RPC API (5001) bound to localhost, exposed ONLY via Caddy HTTPS + bearer
125
+ # - AcceleratedDHTClient + reprovide tuned so gateways can DISCOVER your node
126
+ # - The \`pinnace\` binary installed + its on-box subcommands
127
+ # (\`pinnace node republish|mirror|warm|status\`) scheduled on role-gated
128
+ # systemd timers. Kubo owns pinning + reprovide; pinnace owns IPNS
129
+ # republish/export, replica mirror/fallback, gateway warm, and status.
130
+ # (See docs/adr/0002-on-box-agent-boundary.md.)
131
+ # =============================================================================
132
+
133
+ package_update: true
134
+ package_upgrade: true
135
+
136
+ packages:
137
+ - curl
138
+ - ca-certificates
139
+ - ufw
140
+ - jq
141
+ - debian-keyring
142
+ - debian-archive-keyring
143
+ - apt-transport-https
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
+
161
+ # ---------------------------------------------------------------------------
162
+ # On-box environment consumed by the pinnace node timers + the setup scripts.
163
+ # ---------------------------------------------------------------------------
164
+ write_files:
165
+ - path: /etc/pinnace-node.env
166
+ permissions: "0600"
167
+ owner: root:root
168
+ content: |
169
+ API_DOMAIN="${input.apiDomain}"
170
+ DASH_DOMAIN="${dashboardDomain}"
171
+ API_CORS_ORIGINS="${corsOrigins}"
172
+ ACME_EMAIL="${input.acmeEmail}"
173
+ RPC_BEARER_TOKEN="${input.bearerToken}"
174
+
175
+ # Gateways to warm through. {cid} is replaced with each site's current CID
176
+ # by \`pinnace node warm\`, which AUTO-DISCOVERS sites from MFS (\${SITES_DIR}).
177
+ # Any MFS entry whose name ends in .eth is ALSO warmed via eth.limo.
178
+ WARM_GATEWAYS="${warmGateways}"
179
+
180
+ # MFS directory that holds your sites (one entry per site).
181
+ SITES_DIR="${sitesDir}"
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
+
198
+ # Role: "publisher" holds IPNS keys and signs/exports records; "replica"
199
+ # holds NO key and MIRRORS the publisher's signed records. The pinnace node
200
+ # timers self-gate on this, so scheduling all timers on every box is safe.
201
+ NODE_ROLE="${input.role}"
202
+
203
+ # REPLICA ONLY: where to fetch the publisher's exported signed records.
204
+ PUBLISHER_ENDPOINT="${publisherEndpoint}"
205
+
206
+ KUBO_VERSION="${kuboVersion}"
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
+
214
+ # -- Kubo installer / initializer -----------------------------------------
215
+ - path: /usr/local/sbin/ipfs-setup.sh
216
+ permissions: "0755"
217
+ owner: root:root
218
+ content: |
219
+ #!/usr/bin/env bash
220
+ set -euo pipefail
221
+ source /etc/pinnace-node.env
222
+
223
+ ARCH="$(dpkg --print-architecture)" # amd64 / arm64
224
+ TARBALL="kubo_\${KUBO_VERSION}_linux-\${ARCH}.tar.gz"
225
+ URL="https://dist.ipfs.tech/kubo/\${KUBO_VERSION}/\${TARBALL}"
226
+
227
+ cd /tmp
228
+ curl -fsSL "$URL" -o "$TARBALL"
229
+ tar -xzf "$TARBALL"
230
+ bash kubo/install.sh
231
+ ipfs --version
232
+
233
+ if ! id ipfs >/dev/null 2>&1; then
234
+ useradd --system --create-home --home-dir /var/lib/ipfs --shell /usr/sbin/nologin ipfs
235
+ fi
236
+ install -d -o ipfs -g ipfs /var/lib/ipfs
237
+
238
+ export IPFS_PATH=/var/lib/ipfs/.ipfs
239
+ if [ ! -f "$IPFS_PATH/config" ]; then
240
+ # 'server' profile disables local-network announce (good for a datacenter box)
241
+ sudo -u ipfs env IPFS_PATH="$IPFS_PATH" ipfs init --profile server
242
+ fi
243
+
244
+ cfg() { sudo -u ipfs env IPFS_PATH="$IPFS_PATH" ipfs config "$@"; }
245
+
246
+ # --- Reachability: bind API + gateway to localhost only (NEVER public) ---
247
+ cfg Addresses.API "/ip4/127.0.0.1/tcp/5001"
248
+ cfg Addresses.Gateway "/ip4/127.0.0.1/tcp/8080"
249
+
250
+ # --- Discoverability: keep provider records fresh so gateways find us ---
251
+ cfg --json Routing.AcceleratedDHTClient true
252
+ cfg Routing.Type "auto"
253
+ # Re-announce everything we serve; interval well under the record expiry.
254
+ # (Kubo 0.38 uses Provide.*; the pre-0.38 keys FATAL the daemon at boot.)
255
+ cfg Provide.Interval "12h"
256
+ cfg Provide.Strategy "all"
257
+
258
+ # --- Resource hygiene for a small box ---
259
+ cfg Datastore.StorageMax "40GB"
260
+ cfg --json Swarm.ConnMgr.HighWater 200
261
+ cfg --json Swarm.ConnMgr.LowWater 100
262
+
263
+ # --- RPC API auth: require a bearer token even on localhost ---
264
+ cfg --json API.Authorizations "{
265
+ \\"uploader\\": {
266
+ \\"AuthSecret\\": \\"bearer:\${RPC_BEARER_TOKEN}\\",
267
+ \\"AllowedPaths\\": [\\"/api/v0\\"]
268
+ }
269
+ }"
270
+
271
+ cfg --json API.HTTPHeaders.Access-Control-Allow-Origin "[\\"*\\"]"
272
+ cfg --json API.HTTPHeaders.Access-Control-Allow-Methods "[\\"POST\\"]"
273
+ cfg --json API.HTTPHeaders.Access-Control-Allow-Headers "[\\"Authorization\\"]"
274
+
275
+ chown -R ipfs:ipfs /var/lib/ipfs
276
+
277
+ # -- systemd unit for the daemon (hardened) -------------------------------
278
+ - path: /etc/systemd/system/ipfs.service
279
+ permissions: "0644"
280
+ owner: root:root
281
+ content: |
282
+ [Unit]
283
+ Description=IPFS Kubo daemon
284
+ After=network-online.target
285
+ Wants=network-online.target
286
+
287
+ [Service]
288
+ User=ipfs
289
+ Group=ipfs
290
+ Environment=IPFS_PATH=/var/lib/ipfs/.ipfs
291
+ ExecStart=/usr/local/bin/ipfs daemon --migrate=true --enable-gc
292
+ Restart=on-failure
293
+ RestartSec=5
294
+ LimitNOFILE=65536
295
+ # hardening
296
+ NoNewPrivileges=true
297
+ ProtectSystem=strict
298
+ ProtectHome=true
299
+ ReadWritePaths=/var/lib/ipfs
300
+ PrivateTmp=true
301
+
302
+ [Install]
303
+ WantedBy=multi-user.target
304
+
305
+ # -- pinnace installer: the box runs the SAME binary as the client --------
306
+ - path: /usr/local/sbin/pinnace-setup.sh
307
+ permissions: "0755"
308
+ owner: root:root
309
+ content: |
310
+ #!/usr/bin/env bash
311
+ set -euo pipefail
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.
320
+ if ! command -v npm >/dev/null 2>&1; then
321
+ curl -fsSL "https://deb.nodesource.com/setup_\${NODE_MAJOR}.x" | bash -
322
+ apt-get install -y nodejs
323
+ fi
324
+ npm install -g "pinnace@\${PINNACE_VERSION}"
325
+ pinnace version
326
+
327
+ # -- Caddy reverse proxy for the HTTPS API (auto TLS) ---------------------
328
+ - path: /usr/local/sbin/write-caddyfile.sh
329
+ permissions: "0755"
330
+ owner: root:root
331
+ content: |
332
+ #!/usr/bin/env bash
333
+ set -euo pipefail
334
+ source /etc/pinnace-node.env
335
+ if [ -z "\${API_DOMAIN}" ]; then
336
+ echo "API_DOMAIN not set; skipping Caddy config (API stays localhost-only)."
337
+ exit 0
338
+ fi
339
+ cat > /etc/caddy/Caddyfile <<EOF
340
+ {
341
+ email \${ACME_EMAIL}
342
+ }
343
+
344
+ \${API_DOMAIN} {
345
+ @cors_preflight method OPTIONS
346
+ header {
347
+ Access-Control-Allow-Origin "\${API_CORS_ORIGINS}"
348
+ Access-Control-Allow-Methods "POST, OPTIONS"
349
+ Access-Control-Allow-Headers "Authorization, Content-Type"
350
+ Vary Origin
351
+ }
352
+ respond @cors_preflight 204
353
+
354
+ # Only the Kubo RPC API. Kubo enforces the bearer token via
355
+ # API.Authorizations; Caddy just forwards the header.
356
+ reverse_proxy 127.0.0.1:5001
357
+ }
358
+ EOF
359
+
360
+ if [ -n "\${DASH_DOMAIN}" ]; then
361
+ cat >> /etc/caddy/Caddyfile <<EOF
362
+
363
+ \${DASH_DOMAIN} {
364
+ root * /var/www/ipfs-dash
365
+ file_server
366
+ }
367
+ EOF
368
+ fi
369
+ systemctl restart caddy
370
+
371
+ ${timerUnits}
372
+ # ---------------------------------------------------------------------------
373
+ # Boot sequence
374
+ # ---------------------------------------------------------------------------
375
+ runcmd:
376
+ # Install Caddy (official repo)
377
+ - curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
378
+ - curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
379
+ - apt-get update
380
+ - apt-get install -y caddy
381
+
382
+ # Firewall: SSH + IPFS swarm + HTTP/HTTPS (for the API). NOT 5001 raw, NOT 8080.
383
+ - ufw default deny incoming
384
+ - ufw default allow outgoing
385
+ - ufw allow 22/tcp
386
+ - ufw allow 4001/tcp
387
+ - ufw allow 4001/udp
388
+ - ufw allow 80/tcp
389
+ - ufw allow 443/tcp
390
+ - ufw --force enable
391
+
392
+ # Install + start Kubo
393
+ - /usr/local/sbin/ipfs-setup.sh
394
+ - systemctl daemon-reload
395
+ - systemctl enable --now ipfs.service
396
+
397
+ # Configure the HTTPS API proxy (no-op if API_DOMAIN unset)
398
+ - /usr/local/sbin/write-caddyfile.sh
399
+
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
411
+
412
+ # Enable the pinnace node timers. republish self-gates to publisher, mirror to
413
+ # replica, so enabling all of them on every box is safe (ADR-0002).
414
+ ${enableTimers}
415
+
416
+ # =============================================================================
417
+ # NOTES
418
+ # -----
419
+ # 1. DNS: point an A record (API_DOMAIN) at this box's IPv4 BEFORE first boot,
420
+ # or re-run /usr/local/sbin/write-caddyfile.sh after DNS propagates.
421
+ # 2. Verify discoverability from OUTSIDE the box after ~15 min:
422
+ # https://delegated-ipfs.dev/routing/v1/providers/<CID>
423
+ # Your PeerID should appear in the providers list.
424
+ # =============================================================================
425
+ `;
426
+ }
427
+ /**
428
+ * The Hetzner provider: the first implementation of {@link HostProvider}.
429
+ * Emits the hardened-node cloud-init to a conventional filename.
430
+ */
431
+ export const hetznerHostProvider = {
432
+ host: 'hetzner',
433
+ provision(input) {
434
+ return {
435
+ host: 'hetzner',
436
+ cloudInit: {
437
+ path: 'cloud-init.yaml',
438
+ contents: renderHetznerCloudInit(input),
439
+ },
440
+ };
441
+ },
442
+ };
443
+ /** The provider registry (host -> provider). v1 has a single entry. */
444
+ const PROVIDERS = {
445
+ hetzner: hetznerHostProvider,
446
+ };
447
+ /**
448
+ * Generate the provisioning cloud-init for the requested host: dispatches to
449
+ * the matching {@link HostProvider} and returns its cloud-init file. Throws
450
+ * LOUDLY on an unknown/unimplemented host (the seam exists so callers can add
451
+ * hosts later, but v1 only ships `hetzner`): never a silent no-op.
452
+ */
453
+ export function provision(input) {
454
+ const provider = PROVIDERS[input.host];
455
+ if (!provider) {
456
+ throw new Error(`unsupported host '${input.host}'; v1 provisions only ${HOST_PROVIDERS.join(', ')} (other hosts are Out of Scope but the HostProvider seam exists)`);
457
+ }
458
+ return provider.provision(input);
459
+ }
460
+ //# sourceMappingURL=cloud-init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloud-init.js","sourceRoot":"","sources":["../../src/provision/cloud-init.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Import a derived per-site IPNS key into the **publisher** node's keystore.
3
+ *
4
+ * This module wires the frozen master-key -> per-site key derivation (see
5
+ * `../derive/ipns-key-derivation.ts`, {@link DerivedIpnsKey}) into a running
6
+ * publisher node: it serializes the ed25519 keypair into the libp2p-protobuf
7
+ * form Kubo's `ipfs key import` (`key/import`) expects, then POSTs those bytes
8
+ * to the publisher's keystore via the {@link KuboRpcClient} seam. The client
9
+ * only ever supplies key MATERIAL.
10
+ *
11
+ * THE LOAD-BEARING INVARIANT (docs/adr/0003-*): deriving a key client-side is
12
+ * NOT client-side record SIGNING. A future reader will look at client-side key
13
+ * derivation + import and reasonably wonder "isn't this the fully-keyless C-1
14
+ * client-signing model the spec excludes?" — it is NOT. This module hands the
15
+ * node key MATERIAL; the NODE then signs the IPNS record and owns sequence
16
+ * numbers / validity via `name/publish` (owned by the on-box `republish` verb,
17
+ * `../node/node-commands.ts`). No signing primitive runs here; the only RPC
18
+ * this module issues is `key/import`. Spec Out of Scope excludes the C-1
19
+ * fully-keyless client-signing model; ADR-0003 records that invariant durably.
20
+ *
21
+ * PUBLISHER-ONLY (CONTEXT.md `publisher`, `replica`): exactly one node per
22
+ * shared IPNS name holds the key; replicas are KEYLESS and only re-announce the
23
+ * publisher's signed record. So this module REFUSES to import onto a `replica`
24
+ * ({@link KeyImportRoleError}) rather than silently proceeding — a wrong-role
25
+ * import is a caller error, not a no-op, because it would put a signing key on a
26
+ * box that must never hold one. (Design note: this refusal is a new, loud ERROR
27
+ * gated on the existing `HostRole` concept; it does not invent a role. See the
28
+ * done record's Decisions block.)
29
+ *
30
+ * SCOPE: ONLY key import into the publisher. Deriving the key is the
31
+ * `ipns-key-derivation` task; the publish/refresh timers and the record
32
+ * export/mirror are the `publisher-replica-model` task. This module does
33
+ * neither — it lands the key and stops.
34
+ */
35
+ import type { DerivedIpnsKey } from '../derive/ipns-key-derivation.js';
36
+ import type { HostRole } from '../config/config-resolution.js';
37
+ import type { KuboRpcClient } from '../rpc/kubo-rpc-client.js';
38
+ /**
39
+ * The frozen libp2p-protobuf `PrivateKey` prefix for an ed25519 key with a
40
+ * 64-byte `Data`:
41
+ *
42
+ * 0x08 0x01 — field 1 (Type), varint, KeyType Ed25519 = 1
43
+ * 0x12 0x40 — field 2 (Data), length-delimited, length = 64
44
+ *
45
+ * followed by the 64-byte ed25519 raw key (32-byte seed || 32-byte public key,
46
+ * the Go `ed25519.PrivateKey` layout libp2p `Raw()` emits). This is the
47
+ * `libp2p-protobuf-cleartext` format `ipfs key import` defaults to.
48
+ *
49
+ * @see https://github.com/libp2p/go-libp2p/blob/master/core/crypto/pb/crypto.proto
50
+ * @see https://github.com/libp2p/go-libp2p/blob/master/core/crypto/ed25519.go
51
+ */
52
+ export declare const LIBP2P_ED25519_PRIVATE_KEY_PREFIX: Uint8Array<ArrayBuffer>;
53
+ /**
54
+ * Serialize a {@link DerivedIpnsKey} into the libp2p-protobuf-cleartext
55
+ * `PrivateKey` bytes `ipfs key import` expects.
56
+ *
57
+ * The libp2p ed25519 private key `Data` is the 64-byte concatenation of the
58
+ * 32-byte private seed and the 32-byte public key (so the public key need not
59
+ * be recomputed on load); we wrap it in the `PrivateKey { Type=Ed25519, Data }`
60
+ * protobuf via {@link LIBP2P_ED25519_PRIVATE_KEY_PREFIX}. Pure and offline: no
61
+ * node, network, or signing — this only reshapes bytes we already derived.
62
+ */
63
+ export declare function serializeIpnsKeyForImport(derived: DerivedIpnsKey): Uint8Array;
64
+ /** A loud refusal to import a key onto a non-publisher node (a replica). */
65
+ export declare class KeyImportRoleError extends Error {
66
+ /** The role the caller passed (must be `publisher` to import). */
67
+ readonly role: HostRole;
68
+ /** The key name the import was attempted under. */
69
+ readonly keyName: string;
70
+ constructor(
71
+ /** The role the caller passed (must be `publisher` to import). */
72
+ role: HostRole,
73
+ /** The key name the import was attempted under. */
74
+ keyName: string);
75
+ }
76
+ /** Inputs to {@link importIpnsKeyIntoPublisher}. */
77
+ export interface ImportIpnsKeyInput {
78
+ /** The Kubo RPC client for the PUBLISHER node (per-node, bearer-guarded). */
79
+ client: KuboRpcClient;
80
+ /** The target node's role; MUST be `publisher` or the import is refused. */
81
+ role: HostRole;
82
+ /** The keystore key name to import under (the site name / `key/list` Name). */
83
+ keyName: string;
84
+ /** The derived per-site key (seed + public key) from `ipns-key-derivation`. */
85
+ derived: DerivedIpnsKey;
86
+ }
87
+ /**
88
+ * The Kubo `key/import` JSON response (`Name` + IPNS `Id`), as far as we read.
89
+ */
90
+ export interface KeyImportResult {
91
+ Name?: string;
92
+ Id?: string;
93
+ }
94
+ /**
95
+ * Serialize the derived key and import it into the PUBLISHER node's keystore
96
+ * via Kubo `key/import`. REFUSES ({@link KeyImportRoleError}) on any non-
97
+ * publisher role, touching the node not at all, so a replica stays keyless.
98
+ *
99
+ * The client supplies key MATERIAL only. The node signs IPNS records itself via
100
+ * `name/publish` (a separate concern the on-box `republish` verb owns); nothing
101
+ * here signs, and the only RPC issued is `key/import`. See the module doc +
102
+ * ADR-0003 for the "no client-side record signing" invariant.
103
+ */
104
+ export declare function importIpnsKeyIntoPublisher(input: ImportIpnsKeyInput): Promise<KeyImportResult>;
105
+ //# sourceMappingURL=key-import.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"key-import.d.ts","sourceRoot":"","sources":["../../src/publisher/key-import.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,kCAAkC,CAAC;AACrE,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,gCAAgC,CAAC;AAC7D,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,2BAA2B,CAAC;AAE7D;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,iCAAiC,yBAE5C,CAAC;AAEH;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,cAAc,GAAG,UAAU,CAiB7E;AAED,4EAA4E;AAC5E,qBAAa,kBAAmB,SAAQ,KAAK;IAE3C,kEAAkE;IAClE,QAAQ,CAAC,IAAI,EAAE,QAAQ;IACvB,mDAAmD;IACnD,QAAQ,CAAC,OAAO,EAAE,MAAM;;IAHxB,kEAAkE;IACzD,IAAI,EAAE,QAAQ;IACvB,mDAAmD;IAC1C,OAAO,EAAE,MAAM;CASzB;AAED,oDAAoD;AACpD,MAAM,WAAW,kBAAkB;IAClC,6EAA6E;IAC7E,MAAM,EAAE,aAAa,CAAC;IACtB,4EAA4E;IAC5E,IAAI,EAAE,QAAQ,CAAC;IACf,+EAA+E;IAC/E,OAAO,EAAE,MAAM,CAAC;IAChB,+EAA+E;IAC/E,OAAO,EAAE,cAAc,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;CACZ;AAED;;;;;;;;;GASG;AACH,wBAAsB,0BAA0B,CAC/C,KAAK,EAAE,kBAAkB,GACvB,OAAO,CAAC,eAAe,CAAC,CAM1B"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * The frozen libp2p-protobuf `PrivateKey` prefix for an ed25519 key with a
3
+ * 64-byte `Data`:
4
+ *
5
+ * 0x08 0x01 — field 1 (Type), varint, KeyType Ed25519 = 1
6
+ * 0x12 0x40 — field 2 (Data), length-delimited, length = 64
7
+ *
8
+ * followed by the 64-byte ed25519 raw key (32-byte seed || 32-byte public key,
9
+ * the Go `ed25519.PrivateKey` layout libp2p `Raw()` emits). This is the
10
+ * `libp2p-protobuf-cleartext` format `ipfs key import` defaults to.
11
+ *
12
+ * @see https://github.com/libp2p/go-libp2p/blob/master/core/crypto/pb/crypto.proto
13
+ * @see https://github.com/libp2p/go-libp2p/blob/master/core/crypto/ed25519.go
14
+ */
15
+ export const LIBP2P_ED25519_PRIVATE_KEY_PREFIX = Uint8Array.from([
16
+ 0x08, 0x01, 0x12, 0x40,
17
+ ]);
18
+ /**
19
+ * Serialize a {@link DerivedIpnsKey} into the libp2p-protobuf-cleartext
20
+ * `PrivateKey` bytes `ipfs key import` expects.
21
+ *
22
+ * The libp2p ed25519 private key `Data` is the 64-byte concatenation of the
23
+ * 32-byte private seed and the 32-byte public key (so the public key need not
24
+ * be recomputed on load); we wrap it in the `PrivateKey { Type=Ed25519, Data }`
25
+ * protobuf via {@link LIBP2P_ED25519_PRIVATE_KEY_PREFIX}. Pure and offline: no
26
+ * node, network, or signing — this only reshapes bytes we already derived.
27
+ */
28
+ export function serializeIpnsKeyForImport(derived) {
29
+ if (derived.seed.length !== 32) {
30
+ throw new Error(`expected a 32-byte ed25519 seed, got ${derived.seed.length}`);
31
+ }
32
+ if (derived.publicKey.length !== 32) {
33
+ throw new Error(`expected a 32-byte ed25519 public key, got ${derived.publicKey.length}`);
34
+ }
35
+ const prefix = LIBP2P_ED25519_PRIVATE_KEY_PREFIX;
36
+ const out = new Uint8Array(prefix.length + 64);
37
+ out.set(prefix, 0);
38
+ out.set(derived.seed, prefix.length); // Data[0..32): the private seed.
39
+ out.set(derived.publicKey, prefix.length + 32); // Data[32..64): the pubkey.
40
+ return out;
41
+ }
42
+ /** A loud refusal to import a key onto a non-publisher node (a replica). */
43
+ export class KeyImportRoleError extends Error {
44
+ role;
45
+ keyName;
46
+ constructor(
47
+ /** The role the caller passed (must be `publisher` to import). */
48
+ role,
49
+ /** The key name the import was attempted under. */
50
+ keyName) {
51
+ super(`refusing key/import '${keyName}' onto a ${role}: only the publisher ` +
52
+ `holds the IPNS key; replicas are keyless and re-announce the ` +
53
+ `publisher's signed record`);
54
+ this.role = role;
55
+ this.keyName = keyName;
56
+ this.name = 'KeyImportRoleError';
57
+ }
58
+ }
59
+ /**
60
+ * Serialize the derived key and import it into the PUBLISHER node's keystore
61
+ * via Kubo `key/import`. REFUSES ({@link KeyImportRoleError}) on any non-
62
+ * publisher role, touching the node not at all, so a replica stays keyless.
63
+ *
64
+ * The client supplies key MATERIAL only. The node signs IPNS records itself via
65
+ * `name/publish` (a separate concern the on-box `republish` verb owns); nothing
66
+ * here signs, and the only RPC issued is `key/import`. See the module doc +
67
+ * ADR-0003 for the "no client-side record signing" invariant.
68
+ */
69
+ export async function importIpnsKeyIntoPublisher(input) {
70
+ if (input.role !== 'publisher') {
71
+ throw new KeyImportRoleError(input.role, input.keyName);
72
+ }
73
+ const keyBytes = serializeIpnsKeyForImport(input.derived);
74
+ return await input.client.keyImport(input.keyName, keyBytes);
75
+ }
76
+ //# sourceMappingURL=key-import.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"key-import.js","sourceRoot":"","sources":["../../src/publisher/key-import.ts"],"names":[],"mappings":"AAsCA;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,iCAAiC,GAAG,UAAU,CAAC,IAAI,CAAC;IAChE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI;CACtB,CAAC,CAAC;AAEH;;;;;;;;;GASG;AACH,MAAM,UAAU,yBAAyB,CAAC,OAAuB;IAChE,IAAI,OAAO,CAAC,IAAI,CAAC,MAAM,KAAK,EAAE,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CACd,wCAAwC,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,CAC7D,CAAC;IACH,CAAC;IACD,IAAI,OAAO,CAAC,SAAS,CAAC,MAAM,KAAK,EAAE,EAAE,CAAC;QACrC,MAAM,IAAI,KAAK,CACd,8CAA8C,OAAO,CAAC,SAAS,CAAC,MAAM,EAAE,CACxE,CAAC;IACH,CAAC;IACD,MAAM,MAAM,GAAG,iCAAiC,CAAC;IACjD,MAAM,GAAG,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;IAC/C,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IACnB,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,iCAAiC;IACvE,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC,CAAC,4BAA4B;IAC5E,OAAO,GAAG,CAAC;AACZ,CAAC;AAED,4EAA4E;AAC5E,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAGlC;IAEA;IAJV;IACC,kEAAkE;IACzD,IAAc;IACvB,mDAAmD;IAC1C,OAAe;QAExB,KAAK,CACJ,wBAAwB,OAAO,YAAY,IAAI,uBAAuB;YACrE,+DAA+D;YAC/D,2BAA2B,CAC5B,CAAC;QARO,SAAI,GAAJ,IAAI,CAAU;QAEd,YAAO,GAAP,OAAO,CAAQ;QAOxB,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;IAClC,CAAC;CACD;AAsBD;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,0BAA0B,CAC/C,KAAyB;IAEzB,IAAI,KAAK,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;QAChC,MAAM,IAAI,kBAAkB,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;IACzD,CAAC;IACD,MAAM,QAAQ,GAAG,yBAAyB,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAC1D,OAAO,MAAM,KAAK,CAAC,MAAM,CAAC,SAAS,CAAkB,KAAK,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;AAC/E,CAAC"}