pinnace 0.0.0 → 0.1.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 +15 -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 +78 -0
  16. package/dist/cli/run.d.ts.map +1 -0
  17. package/dist/cli/run.js +506 -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 +227 -0
  38. package/dist/node/node-commands.js.map +1 -0
  39. package/dist/provision/cloud-init.d.ts +137 -0
  40. package/dist/provision/cloud-init.d.ts.map +1 -0
  41. package/dist/provision/cloud-init.js +390 -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 +654 -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 +391 -0
  77. package/src/provision/cloud-init.ts +551 -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,137 @@
1
+ /**
2
+ * The **cloud-init generator** behind the `HostProvider` seam (CONTEXT.md `host
3
+ * provider seam`). `provision` asks the core for a ready-to-paste cloud-init:
4
+ * the host-specific YAML that stands up a hardened Kubo node. Hetzner is the
5
+ * FIRST (and, in v1, only) implementation; other hosts are Out of Scope but
6
+ * slot in behind this same seam later (spec "Out of Scope"; user stories 1, 21).
7
+ *
8
+ * This module is PURE: {@link provision} takes a plain host/site/role config
9
+ * and returns the cloud-init file (path + contents) as data. It does NOT touch
10
+ * the filesystem, the network, or SSH. The CLI wrapper (`provision` command)
11
+ * is what hands the YAML to the operator. Keeping the emit pure lets it be
12
+ * snapshot-tested as a string with no fixtures (test-first policy) and reused
13
+ * as a TypeScript API (CONTEXT.md `core vs cli`).
14
+ *
15
+ * SEAM DISCIPLINE (user story 21): only host-specific provisioning lives behind
16
+ * `HostProvider`. Deploy/publish/status speak ONLY Kubo RPC and must not import
17
+ * this module. Adding a host later touches nothing but this file + the
18
+ * {@link HOST_PROVIDERS} registry. (The dependency direction is one-way: this
19
+ * module may reference the shared `HostRole` type, but nothing in deploy/
20
+ * publish/status references this module.)
21
+ *
22
+ * KEY CHANGE FROM THE REFERENCE (docs/adr/0002-on-box-agent-boundary.md): the
23
+ * emitted cloud-init does NOT bake the reference's four bash scripts
24
+ * (`ipfs-warm.sh`, `ipfs-ipns-publish.sh`, `ipfs-ipns-mirror.sh`,
25
+ * `ipfs-status.sh`). Instead it INSTALLS the `pinnace` binary on the box and
26
+ * schedules the on-box subcommands (`pinnace node republish|mirror|warm|
27
+ * status`) on role-gated systemd timers. One codebase runs both as the
28
+ * operator's client and as the box's periodic agent, so the record/warm/mirror/
29
+ * status logic has a single implementation (no bash/TS drift). Kubo still owns
30
+ * pinning (`dag/import --pin-roots`) + provider-record freshness
31
+ * (`Provide.Interval`); the pinnace timers own ONLY IPNS republish/export,
32
+ * replica mirror/fallback, gateway warm, and status. The reference bash is used
33
+ * here as the BEHAVIOURAL SPEC of what each timer must do, not as code to emit.
34
+ *
35
+ * INVARIANTS ENCODED (ported, not copied, from `~/searches/ipfs-hetzner/`):
36
+ * - Kubo as a hardened systemd unit (dedicated `ipfs` user, sandboxing).
37
+ * - ufw opens 4001 TCP+UDP (swarm, so gateways can dial) + 80/443 (Caddy/ACME);
38
+ * the RPC API (5001) and gateway (8080) are bound to localhost and NEVER
39
+ * opened in the firewall.
40
+ * - discoverability: AcceleratedDHTClient + `Routing.Type auto` + reprovide
41
+ * (interval well under the record-expiry window, strategy `all`).
42
+ * - Caddy HTTPS reverse proxy for the RPC API (auto TLS), which forwards the
43
+ * operator's bearer header straight through to the token-guarded Kubo API.
44
+ */
45
+ import type { HostRole } from '../config/config-resolution.js';
46
+ /** The hosts pinnace can provision for. v1 = Hetzner ONLY. */
47
+ export type HostName = 'hetzner';
48
+ /** The hosts, in a stable order (help text / iteration / validation). */
49
+ export declare const HOST_PROVIDERS: readonly HostName[];
50
+ /**
51
+ * The per-box inputs the generator injects into the cloud-init (the values
52
+ * `make-cloud-init.sh` used to `sed` in). Everything that varies per box lives
53
+ * here; the hardened invariants are baked into the template.
54
+ */
55
+ export interface ProvisionInput {
56
+ /** Which host to provision for (v1: `hetzner`). */
57
+ host: HostName;
58
+ /** DNS A-record pointed at this box, used for the HTTPS RPC API vhost. */
59
+ apiDomain: string;
60
+ /** Optional read-only status dashboard vhost (also serves exported records). */
61
+ dashboardDomain?: string;
62
+ /** Email for Let's Encrypt (ACME) certificate issuance. */
63
+ acmeEmail: string;
64
+ /**
65
+ * The long random bearer token guarding the RPC API. Kubo enforces it via
66
+ * `API.Authorizations` even on localhost; Caddy forwards the header through.
67
+ */
68
+ bearerToken: string;
69
+ /**
70
+ * This box's role. `publisher` holds IPNS keys and signs/exports records;
71
+ * `replica` is keyless and mirrors the publisher's records. Written to the
72
+ * env file as `NODE_ROLE`, which the `pinnace node` timers self-gate on
73
+ * (republish only on publisher, mirror only on replica), so scheduling all
74
+ * timers on every box is safe.
75
+ */
76
+ role: HostRole;
77
+ /**
78
+ * REPLICA ONLY: base URL to fetch the publisher's exported signed records
79
+ * from (the publisher's dashboard vhost). Ignored for publishers.
80
+ */
81
+ publisherEndpoint?: string;
82
+ /**
83
+ * Browser origins allowed to call the RPC API (CORS). Space-separated in the
84
+ * env file. Defaults to the dashboard domain's URL when a dashboard is set.
85
+ */
86
+ corsOrigins?: string;
87
+ /**
88
+ * Public gateways to warm through, each a template containing `{cid}`.
89
+ * Defaults to a sensible dweb.link/ipfs.io set. `.eth` MFS names are ALSO
90
+ * warmed via eth.limo automatically by `pinnace node warm`.
91
+ */
92
+ gateways?: string[];
93
+ /** The Kubo version to install. Defaults to a pinned known-good release. */
94
+ kuboVersion?: string;
95
+ /** The MFS directory sites live under. Defaults to `/sites`. */
96
+ sitesDir?: string;
97
+ }
98
+ /** A single emitted file: where it goes + what it contains. */
99
+ export interface EmittedFile {
100
+ /** The path to write the file at (relative; the operator pastes/uploads it). */
101
+ path: string;
102
+ /** The full file contents. */
103
+ contents: string;
104
+ }
105
+ /** What a host provider returns: the ready-to-paste cloud-init file. */
106
+ export interface ProvisionResult {
107
+ /** Which host this was provisioned for (echoed for the caller). */
108
+ host: HostName;
109
+ /** The cloud-init file (path + contents). */
110
+ cloudInit: EmittedFile;
111
+ }
112
+ /**
113
+ * The `HostProvider` seam: one method that generates a host's provisioning
114
+ * artifact (cloud-init) for a given input. v1 has a single implementation
115
+ * ({@link hetznerHostProvider}); adding DigitalOcean / others later means
116
+ * adding another provider here and an entry in {@link HOST_PROVIDERS}; deploy/
117
+ * publish/status logic is untouched (spec user story 21).
118
+ */
119
+ export interface HostProvider {
120
+ /** The host this provider provisions for. */
121
+ readonly host: HostName;
122
+ /** Generate the cloud-init (+ any host artifacts) for the given input. */
123
+ provision(input: ProvisionInput): ProvisionResult;
124
+ }
125
+ /**
126
+ * The Hetzner provider: the first implementation of {@link HostProvider}.
127
+ * Emits the hardened-node cloud-init to a conventional filename.
128
+ */
129
+ export declare const hetznerHostProvider: HostProvider;
130
+ /**
131
+ * Generate the provisioning cloud-init for the requested host: dispatches to
132
+ * the matching {@link HostProvider} and returns its cloud-init file. Throws
133
+ * LOUDLY on an unknown/unimplemented host (the seam exists so callers can add
134
+ * hosts later, but v1 only ships `hetzner`): never a silent no-op.
135
+ */
136
+ export declare function provision(input: ProvisionInput): ProvisionResult;
137
+ //# sourceMappingURL=cloud-init.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,390 @@
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
+ const DEFAULT_SITES_DIR = '/sites';
6
+ const DEFAULT_GATEWAYS = [
7
+ 'https://{cid}.ipfs.dweb.link/',
8
+ 'https://{cid}.ipfs.cf-ipfs.com/',
9
+ 'https://ipfs.io/ipfs/{cid}',
10
+ ];
11
+ /**
12
+ * The four timers, ported from the reference bash-unit cadences but pointed at
13
+ * `pinnace node <verb>` instead of the bash scripts (ADR-0002). Order is stable
14
+ * (deterministic output). Cadences mirror the reference: IPNS republish/mirror
15
+ * well under record expiry, warm/status more frequent.
16
+ */
17
+ const TIMERS = [
18
+ {
19
+ verb: 'republish',
20
+ description: 'Republish + export IPNS records (publisher role)',
21
+ onBootSec: '8min',
22
+ onUnitActiveSec: '6h',
23
+ },
24
+ {
25
+ verb: 'mirror',
26
+ description: 'Mirror publisher IPNS records (replica role)',
27
+ onBootSec: '9min',
28
+ onUnitActiveSec: '3h',
29
+ },
30
+ {
31
+ verb: 'warm',
32
+ description: 'Warm public IPFS gateway caches for our CIDs',
33
+ onBootSec: '5min',
34
+ onUnitActiveSec: '30min',
35
+ },
36
+ {
37
+ verb: 'status',
38
+ description: 'Regenerate the per-site status JSON for the dashboard',
39
+ onBootSec: '6min',
40
+ onUnitActiveSec: '15min',
41
+ },
42
+ ];
43
+ /** Render the `pinnace-<verb>` systemd service + timer unit pair. */
44
+ function renderTimerUnits(t) {
45
+ return ` - path: /etc/systemd/system/pinnace-${t.verb}.service
46
+ permissions: "0644"
47
+ owner: root:root
48
+ content: |
49
+ [Unit]
50
+ Description=${t.description}
51
+ After=ipfs.service
52
+
53
+ [Service]
54
+ Type=oneshot
55
+ User=ipfs
56
+ Group=ipfs
57
+ EnvironmentFile=/etc/pinnace-node.env
58
+ Environment=IPFS_PATH=/var/lib/ipfs/.ipfs
59
+ ExecStart=/usr/local/bin/pinnace node ${t.verb}
60
+
61
+ - path: /etc/systemd/system/pinnace-${t.verb}.timer
62
+ permissions: "0644"
63
+ owner: root:root
64
+ content: |
65
+ [Unit]
66
+ Description=Schedule pinnace node ${t.verb}
67
+
68
+ [Timer]
69
+ OnBootSec=${t.onBootSec}
70
+ OnUnitActiveSec=${t.onUnitActiveSec}
71
+ Persistent=true
72
+
73
+ [Install]
74
+ WantedBy=timers.target
75
+ `;
76
+ }
77
+ /**
78
+ * Render the full Hetzner cloud-init. Deterministic: same input -> byte-
79
+ * identical output (snapshot-locked). Encodes the hardened-node invariants
80
+ * (ufw 4001 TCP+UDP + 443, never 5001 raw; localhost-bound API/gateway;
81
+ * AcceleratedDHTClient + reprovide + `Routing.Type auto`; hardened systemd
82
+ * Kubo unit; Caddy HTTPS + bearer proxy) and schedules the `pinnace node`
83
+ * timers (NOT bash).
84
+ */
85
+ function renderHetznerCloudInit(input) {
86
+ const dashboardDomain = input.dashboardDomain ?? '';
87
+ const corsOrigins = input.corsOrigins ?? (dashboardDomain ? `https://${dashboardDomain}` : '');
88
+ const publisherEndpoint = input.role === 'replica' ? (input.publisherEndpoint ?? '') : '';
89
+ const gateways = input.gateways ?? DEFAULT_GATEWAYS;
90
+ const kuboVersion = input.kuboVersion ?? DEFAULT_KUBO_VERSION;
91
+ const sitesDir = input.sitesDir ?? DEFAULT_SITES_DIR;
92
+ const warmGateways = gateways.join(' ');
93
+ const timerUnits = TIMERS.map(renderTimerUnits).join('\n');
94
+ const enableTimers = TIMERS.map((t) => ` - systemctl enable --now pinnace-${t.verb}.timer`).join('\n');
95
+ return `#cloud-config
96
+ # =============================================================================
97
+ # ${input.host} Cloud -> self-hosted IPFS (Kubo) node for a static website.
98
+ #
99
+ # Generated by \`pinnace provision --host ${input.host}\`. This is the programmatic
100
+ # successor to the shell \`sed\`-template prototype (superseded).
101
+ #
102
+ # What this gives you:
103
+ # - Kubo running as a hardened systemd service (user: ipfs)
104
+ # - Swarm port 4001 (TCP+UDP) open so public gateways can dial you
105
+ # - RPC API (5001) bound to localhost, exposed ONLY via Caddy HTTPS + bearer
106
+ # - AcceleratedDHTClient + reprovide tuned so gateways can DISCOVER your node
107
+ # - The \`pinnace\` binary installed + its on-box subcommands
108
+ # (\`pinnace node republish|mirror|warm|status\`) scheduled on role-gated
109
+ # systemd timers. Kubo owns pinning + reprovide; pinnace owns IPNS
110
+ # republish/export, replica mirror/fallback, gateway warm, and status.
111
+ # (See docs/adr/0002-on-box-agent-boundary.md.)
112
+ # =============================================================================
113
+
114
+ package_update: true
115
+ package_upgrade: true
116
+
117
+ packages:
118
+ - curl
119
+ - ca-certificates
120
+ - ufw
121
+ - jq
122
+ - debian-keyring
123
+ - debian-archive-keyring
124
+ - apt-transport-https
125
+
126
+ # ---------------------------------------------------------------------------
127
+ # On-box environment consumed by the pinnace node timers + the setup scripts.
128
+ # ---------------------------------------------------------------------------
129
+ write_files:
130
+ - path: /etc/pinnace-node.env
131
+ permissions: "0600"
132
+ owner: root:root
133
+ content: |
134
+ API_DOMAIN="${input.apiDomain}"
135
+ DASH_DOMAIN="${dashboardDomain}"
136
+ API_CORS_ORIGINS="${corsOrigins}"
137
+ ACME_EMAIL="${input.acmeEmail}"
138
+ RPC_BEARER_TOKEN="${input.bearerToken}"
139
+
140
+ # Gateways to warm through. {cid} is replaced with each site's current CID
141
+ # by \`pinnace node warm\`, which AUTO-DISCOVERS sites from MFS (\${SITES_DIR}).
142
+ # Any MFS entry whose name ends in .eth is ALSO warmed via eth.limo.
143
+ WARM_GATEWAYS="${warmGateways}"
144
+
145
+ # MFS directory that holds your sites (one entry per site).
146
+ SITES_DIR="${sitesDir}"
147
+
148
+ # Role: "publisher" holds IPNS keys and signs/exports records; "replica"
149
+ # holds NO key and MIRRORS the publisher's signed records. The pinnace node
150
+ # timers self-gate on this, so scheduling all timers on every box is safe.
151
+ NODE_ROLE="${input.role}"
152
+
153
+ # REPLICA ONLY: where to fetch the publisher's exported signed records.
154
+ PUBLISHER_ENDPOINT="${publisherEndpoint}"
155
+
156
+ KUBO_VERSION="${kuboVersion}"
157
+
158
+ # -- Kubo installer / initializer -----------------------------------------
159
+ - path: /usr/local/sbin/ipfs-setup.sh
160
+ permissions: "0755"
161
+ owner: root:root
162
+ content: |
163
+ #!/usr/bin/env bash
164
+ set -euo pipefail
165
+ source /etc/pinnace-node.env
166
+
167
+ ARCH="$(dpkg --print-architecture)" # amd64 / arm64
168
+ TARBALL="kubo_\${KUBO_VERSION}_linux-\${ARCH}.tar.gz"
169
+ URL="https://dist.ipfs.tech/kubo/\${KUBO_VERSION}/\${TARBALL}"
170
+
171
+ cd /tmp
172
+ curl -fsSL "$URL" -o "$TARBALL"
173
+ tar -xzf "$TARBALL"
174
+ bash kubo/install.sh
175
+ ipfs --version
176
+
177
+ if ! id ipfs >/dev/null 2>&1; then
178
+ useradd --system --create-home --home-dir /var/lib/ipfs --shell /usr/sbin/nologin ipfs
179
+ fi
180
+ install -d -o ipfs -g ipfs /var/lib/ipfs
181
+
182
+ export IPFS_PATH=/var/lib/ipfs/.ipfs
183
+ if [ ! -f "$IPFS_PATH/config" ]; then
184
+ # 'server' profile disables local-network announce (good for a datacenter box)
185
+ sudo -u ipfs env IPFS_PATH="$IPFS_PATH" ipfs init --profile server
186
+ fi
187
+
188
+ cfg() { sudo -u ipfs env IPFS_PATH="$IPFS_PATH" ipfs config "$@"; }
189
+
190
+ # --- Reachability: bind API + gateway to localhost only (NEVER public) ---
191
+ cfg Addresses.API "/ip4/127.0.0.1/tcp/5001"
192
+ cfg Addresses.Gateway "/ip4/127.0.0.1/tcp/8080"
193
+
194
+ # --- Discoverability: keep provider records fresh so gateways find us ---
195
+ cfg --json Routing.AcceleratedDHTClient true
196
+ cfg Routing.Type "auto"
197
+ # Re-announce everything we serve; interval well under the record expiry.
198
+ # (Kubo 0.38 uses Provide.*; the pre-0.38 keys FATAL the daemon at boot.)
199
+ cfg Provide.Interval "12h"
200
+ cfg Provide.Strategy "all"
201
+
202
+ # --- Resource hygiene for a small box ---
203
+ cfg Datastore.StorageMax "40GB"
204
+ cfg --json Swarm.ConnMgr.HighWater 200
205
+ cfg --json Swarm.ConnMgr.LowWater 100
206
+
207
+ # --- RPC API auth: require a bearer token even on localhost ---
208
+ cfg --json API.Authorizations "{
209
+ \\"uploader\\": {
210
+ \\"AuthSecret\\": \\"bearer:\${RPC_BEARER_TOKEN}\\",
211
+ \\"AllowedPaths\\": [\\"/api/v0\\"]
212
+ }
213
+ }"
214
+
215
+ cfg --json API.HTTPHeaders.Access-Control-Allow-Origin "[\\"*\\"]"
216
+ cfg --json API.HTTPHeaders.Access-Control-Allow-Methods "[\\"POST\\"]"
217
+ cfg --json API.HTTPHeaders.Access-Control-Allow-Headers "[\\"Authorization\\"]"
218
+
219
+ chown -R ipfs:ipfs /var/lib/ipfs
220
+
221
+ # -- systemd unit for the daemon (hardened) -------------------------------
222
+ - path: /etc/systemd/system/ipfs.service
223
+ permissions: "0644"
224
+ owner: root:root
225
+ content: |
226
+ [Unit]
227
+ Description=IPFS Kubo daemon
228
+ After=network-online.target
229
+ Wants=network-online.target
230
+
231
+ [Service]
232
+ User=ipfs
233
+ Group=ipfs
234
+ Environment=IPFS_PATH=/var/lib/ipfs/.ipfs
235
+ ExecStart=/usr/local/bin/ipfs daemon --migrate=true --enable-gc
236
+ Restart=on-failure
237
+ RestartSec=5
238
+ LimitNOFILE=65536
239
+ # hardening
240
+ NoNewPrivileges=true
241
+ ProtectSystem=strict
242
+ ProtectHome=true
243
+ ReadWritePaths=/var/lib/ipfs
244
+ PrivateTmp=true
245
+
246
+ [Install]
247
+ WantedBy=multi-user.target
248
+
249
+ # -- pinnace installer: the box runs the SAME binary as the client --------
250
+ - path: /usr/local/sbin/pinnace-setup.sh
251
+ permissions: "0755"
252
+ owner: root:root
253
+ content: |
254
+ #!/usr/bin/env bash
255
+ 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.
258
+ if ! command -v npm >/dev/null 2>&1; then
259
+ curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
260
+ apt-get install -y nodejs
261
+ fi
262
+ npm install -g pinnace
263
+ pinnace version
264
+
265
+ # -- Caddy reverse proxy for the HTTPS API (auto TLS) ---------------------
266
+ - path: /usr/local/sbin/write-caddyfile.sh
267
+ permissions: "0755"
268
+ owner: root:root
269
+ content: |
270
+ #!/usr/bin/env bash
271
+ set -euo pipefail
272
+ source /etc/pinnace-node.env
273
+ if [ -z "\${API_DOMAIN}" ]; then
274
+ echo "API_DOMAIN not set; skipping Caddy config (API stays localhost-only)."
275
+ exit 0
276
+ fi
277
+ cat > /etc/caddy/Caddyfile <<EOF
278
+ {
279
+ email \${ACME_EMAIL}
280
+ }
281
+
282
+ \${API_DOMAIN} {
283
+ @cors_preflight method OPTIONS
284
+ header {
285
+ Access-Control-Allow-Origin "\${API_CORS_ORIGINS}"
286
+ Access-Control-Allow-Methods "POST, OPTIONS"
287
+ Access-Control-Allow-Headers "Authorization, Content-Type"
288
+ Vary Origin
289
+ }
290
+ respond @cors_preflight 204
291
+
292
+ # Only the Kubo RPC API. Kubo enforces the bearer token via
293
+ # API.Authorizations; Caddy just forwards the header.
294
+ reverse_proxy 127.0.0.1:5001
295
+ }
296
+ EOF
297
+
298
+ if [ -n "\${DASH_DOMAIN}" ]; then
299
+ cat >> /etc/caddy/Caddyfile <<EOF
300
+
301
+ \${DASH_DOMAIN} {
302
+ root * /var/www/ipfs-dash
303
+ file_server
304
+ }
305
+ EOF
306
+ fi
307
+ systemctl restart caddy
308
+
309
+ ${timerUnits}
310
+ # ---------------------------------------------------------------------------
311
+ # Boot sequence
312
+ # ---------------------------------------------------------------------------
313
+ runcmd:
314
+ # Install Caddy (official repo)
315
+ - curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
316
+ - curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
317
+ - apt-get update
318
+ - apt-get install -y caddy
319
+
320
+ # Firewall: SSH + IPFS swarm + HTTP/HTTPS (for the API). NOT 5001 raw, NOT 8080.
321
+ - ufw default deny incoming
322
+ - ufw default allow outgoing
323
+ - ufw allow 22/tcp
324
+ - ufw allow 4001/tcp
325
+ - ufw allow 4001/udp
326
+ - ufw allow 80/tcp
327
+ - ufw allow 443/tcp
328
+ - ufw --force enable
329
+
330
+ # Install + start Kubo
331
+ - /usr/local/sbin/ipfs-setup.sh
332
+ - systemctl daemon-reload
333
+ - systemctl enable --now ipfs.service
334
+
335
+ # Configure the HTTPS API proxy (no-op if API_DOMAIN unset)
336
+ - /usr/local/sbin/write-caddyfile.sh
337
+
338
+ # Install the pinnace binary (the box runs the same CLI as the client).
339
+ - /usr/local/sbin/pinnace-setup.sh
340
+
341
+ # Enable the pinnace node timers. republish self-gates to publisher, mirror to
342
+ # 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
+ ${enableTimers}
345
+
346
+ # =============================================================================
347
+ # NOTES
348
+ # -----
349
+ # 1. DNS: point an A record (API_DOMAIN) at this box's IPv4 BEFORE first boot,
350
+ # or re-run /usr/local/sbin/write-caddyfile.sh after DNS propagates.
351
+ # 2. Verify discoverability from OUTSIDE the box after ~15 min:
352
+ # https://delegated-ipfs.dev/routing/v1/providers/<CID>
353
+ # Your PeerID should appear in the providers list.
354
+ # =============================================================================
355
+ `;
356
+ }
357
+ /**
358
+ * The Hetzner provider: the first implementation of {@link HostProvider}.
359
+ * Emits the hardened-node cloud-init to a conventional filename.
360
+ */
361
+ export const hetznerHostProvider = {
362
+ host: 'hetzner',
363
+ provision(input) {
364
+ return {
365
+ host: 'hetzner',
366
+ cloudInit: {
367
+ path: 'cloud-init.yaml',
368
+ contents: renderHetznerCloudInit(input),
369
+ },
370
+ };
371
+ },
372
+ };
373
+ /** The provider registry (host -> provider). v1 has a single entry. */
374
+ const PROVIDERS = {
375
+ hetzner: hetznerHostProvider,
376
+ };
377
+ /**
378
+ * Generate the provisioning cloud-init for the requested host: dispatches to
379
+ * the matching {@link HostProvider} and returns its cloud-init file. Throws
380
+ * LOUDLY on an unknown/unimplemented host (the seam exists so callers can add
381
+ * hosts later, but v1 only ships `hetzner`): never a silent no-op.
382
+ */
383
+ export function provision(input) {
384
+ const provider = PROVIDERS[input.host];
385
+ if (!provider) {
386
+ throw new Error(`unsupported host '${input.host}'; v1 provisions only ${HOST_PROVIDERS.join(', ')} (other hosts are Out of Scope but the HostProvider seam exists)`);
387
+ }
388
+ return provider.provision(input);
389
+ }
390
+ //# 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"}