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.
- package/LICENSE +661 -0
- package/README.md +164 -0
- package/dist/car/car-build.d.ts +26 -0
- package/dist/car/car-build.d.ts.map +1 -0
- package/dist/car/car-build.js +113 -0
- package/dist/car/car-build.js.map +1 -0
- package/dist/ci/ci-emit.d.ts +116 -0
- package/dist/ci/ci-emit.d.ts.map +1 -0
- package/dist/ci/ci-emit.js +190 -0
- package/dist/ci/ci-emit.js.map +1 -0
- package/dist/cli/bin.d.ts +3 -0
- package/dist/cli/bin.d.ts.map +1 -0
- package/dist/cli/bin.js +17 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/cli/run.d.ts +94 -0
- package/dist/cli/run.d.ts.map +1 -0
- package/dist/cli/run.js +731 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/config/config-resolution.d.ts +186 -0
- package/dist/config/config-resolution.d.ts.map +1 -0
- package/dist/config/config-resolution.js +137 -0
- package/dist/config/config-resolution.js.map +1 -0
- package/dist/deploy/deploy.d.ts +121 -0
- package/dist/deploy/deploy.d.ts.map +1 -0
- package/dist/deploy/deploy.js +150 -0
- package/dist/deploy/deploy.js.map +1 -0
- package/dist/derive/ipns-key-derivation.d.ts +37 -0
- package/dist/derive/ipns-key-derivation.d.ts.map +1 -0
- package/dist/derive/ipns-key-derivation.js +130 -0
- package/dist/derive/ipns-key-derivation.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/node/node-commands.d.ts +115 -0
- package/dist/node/node-commands.d.ts.map +1 -0
- package/dist/node/node-commands.js +231 -0
- package/dist/node/node-commands.js.map +1 -0
- package/dist/provision/cloud-init.d.ts +162 -0
- package/dist/provision/cloud-init.d.ts.map +1 -0
- package/dist/provision/cloud-init.js +460 -0
- package/dist/provision/cloud-init.js.map +1 -0
- package/dist/publisher/key-import.d.ts +105 -0
- package/dist/publisher/key-import.d.ts.map +1 -0
- package/dist/publisher/key-import.js +76 -0
- package/dist/publisher/key-import.js.map +1 -0
- package/dist/publisher/record-sequence.d.ts +90 -0
- package/dist/publisher/record-sequence.d.ts.map +1 -0
- package/dist/publisher/record-sequence.js +250 -0
- package/dist/publisher/record-sequence.js.map +1 -0
- package/dist/rpc/kubo-rpc-client.d.ts +154 -0
- package/dist/rpc/kubo-rpc-client.d.ts.map +1 -0
- package/dist/rpc/kubo-rpc-client.js +207 -0
- package/dist/rpc/kubo-rpc-client.js.map +1 -0
- package/dist/rpc/mock-kubo.d.ts +87 -0
- package/dist/rpc/mock-kubo.d.ts.map +1 -0
- package/dist/rpc/mock-kubo.js +129 -0
- package/dist/rpc/mock-kubo.js.map +1 -0
- package/dist/site/site-management.d.ts +126 -0
- package/dist/site/site-management.d.ts.map +1 -0
- package/dist/site/site-management.js +123 -0
- package/dist/site/site-management.js.map +1 -0
- package/dist/status/status-report.d.ts +145 -0
- package/dist/status/status-report.d.ts.map +1 -0
- package/dist/status/status-report.js +192 -0
- package/dist/status/status-report.js.map +1 -0
- package/package.json +45 -2
- package/src/car/car-build.ts +135 -0
- package/src/ci/ci-emit.ts +284 -0
- package/src/cli/bin.ts +20 -0
- package/src/cli/run.ts +970 -0
- package/src/config/config-resolution.ts +264 -0
- package/src/deploy/deploy.ts +250 -0
- package/src/derive/ipns-key-derivation.ts +173 -0
- package/src/index.ts +146 -0
- package/src/node/node-commands.ts +395 -0
- package/src/provision/cloud-init.ts +646 -0
- package/src/publisher/key-import.ts +141 -0
- package/src/publisher/record-sequence.ts +336 -0
- package/src/rpc/kubo-rpc-client.ts +281 -0
- package/src/rpc/mock-kubo.ts +194 -0
- package/src/site/site-management.ts +241 -0
- package/src/status/status-report.ts +291 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The **on-box** command surface of `pinnace` — the `node` namespace.
|
|
3
|
+
*
|
|
4
|
+
* The SAME `pinnace` binary the operator runs as a client also runs the
|
|
5
|
+
* recurring loop ON a Kubo node (invoked by the cloud-init systemd timers).
|
|
6
|
+
* This module is that loop's command surface: `pinnace node <verb>` with
|
|
7
|
+
* `verb` in {`republish`, `mirror`, `warm`, `status`}. Each verb is a THIN
|
|
8
|
+
* wrapper that discovers sites from the LOCAL Kubo RPC and dispatches to a
|
|
9
|
+
* core operation, exactly like the client CLI wraps core operations
|
|
10
|
+
* (CONTEXT.md `core vs cli`). One codebase, two invocation contexts, so the
|
|
11
|
+
* record/warm/mirror/status logic has a single implementation — no bash/TS
|
|
12
|
+
* behaviour drift like the reference prototype had.
|
|
13
|
+
*
|
|
14
|
+
* BOUNDARY (see docs/adr/0002-*): Kubo owns pinning (`dag/import --pin-roots`)
|
|
15
|
+
* and provider-record freshness (`Reprovider.Interval`); this recurring loop
|
|
16
|
+
* owns ONLY IPNS republish/export, replica mirror/fallback, gateway warm, and
|
|
17
|
+
* status. It does nothing recurring for pinning or reprovide.
|
|
18
|
+
*
|
|
19
|
+
* Behaviour ported (not copied as bash) from the reference cloud-init scripts
|
|
20
|
+
* `ipfs-ipns-publish.sh`, `ipfs-ipns-mirror.sh`, `ipfs-warm.sh`,
|
|
21
|
+
* `ipfs-status.sh` (`~/searches/ipfs-hetzner/cloud-init.yaml`).
|
|
22
|
+
*
|
|
23
|
+
* SEAM NOTE: the publisher/replica record SEQUENCE (export -> fetch ->
|
|
24
|
+
* routing/put -> fallback) is OWNED and tested by the `publisher-replica-model`
|
|
25
|
+
* task, and `status`'s per-site checks by the `status-report` task. Both land
|
|
26
|
+
* in parallel with this task. So the four core operations are injectable via
|
|
27
|
+
* {@link NodeCommandOps}: this module's default `republish`/`mirror` DELEGATE to
|
|
28
|
+
* the owned record-sequence core (`../publisher/record-sequence.ts`) so there is
|
|
29
|
+
* a SINGLE implementation (no bash/TS drift, per ADR-0002), and `status`
|
|
30
|
+
* delegates to the `status-report` core injected via `ctx.ops`. This task owns
|
|
31
|
+
* the command surface + role-gating + `warm` + the boundary ADR.
|
|
32
|
+
*/
|
|
33
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
34
|
+
import { join } from 'node:path';
|
|
35
|
+
import { republishAndExport, mirrorAndReannounce, } from '../publisher/record-sequence.js';
|
|
36
|
+
/** The verbs, in a stable order (for help text / iteration). */
|
|
37
|
+
export const NODE_VERBS = [
|
|
38
|
+
'republish',
|
|
39
|
+
'mirror',
|
|
40
|
+
'warm',
|
|
41
|
+
'status',
|
|
42
|
+
];
|
|
43
|
+
/** Which role each role-gated verb requires; role-agnostic verbs are absent. */
|
|
44
|
+
const VERB_ROLE_GATE = {
|
|
45
|
+
republish: 'publisher',
|
|
46
|
+
mirror: 'replica',
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Auto-discover sites from MFS `/sites/*`: list the directory, then stat each
|
|
50
|
+
* entry for its current CID. Entries without a resolvable CID are skipped.
|
|
51
|
+
* Shared by every verb (the reference scripts all begin with this same walk).
|
|
52
|
+
*/
|
|
53
|
+
export async function discoverSites(client, sitesDir = '/sites') {
|
|
54
|
+
let listing;
|
|
55
|
+
try {
|
|
56
|
+
listing = await client.filesLs(sitesDir);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// No /sites dir yet (fresh box) — nothing to do, not an error.
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
const entries = listing.Entries ?? [];
|
|
63
|
+
const sites = [];
|
|
64
|
+
for (const entry of entries) {
|
|
65
|
+
const id = entry?.Name;
|
|
66
|
+
if (!id)
|
|
67
|
+
continue;
|
|
68
|
+
try {
|
|
69
|
+
const stat = await client.filesStat(`${sitesDir}/${id}`);
|
|
70
|
+
if (stat?.Hash)
|
|
71
|
+
sites.push({ id, cid: stat.Hash });
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// A site whose stat fails is skipped, not fatal for the others.
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return sites;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Run one on-box verb. Resolves the role gate first (a wrong-role verb is a
|
|
81
|
+
* SKIP — a clean no-op that touches no Kubo RPC, so scheduling all timers on
|
|
82
|
+
* every box is safe), then discovers sites and dispatches to the (possibly
|
|
83
|
+
* injected) core op. Throws on an unknown verb (loud, never a silent no-op).
|
|
84
|
+
*/
|
|
85
|
+
export async function runNodeCommand(verb, ctx) {
|
|
86
|
+
if (!NODE_VERBS.includes(verb)) {
|
|
87
|
+
throw new Error(`unknown node verb '${verb}'; expected one of ${NODE_VERBS.join(', ')}`);
|
|
88
|
+
}
|
|
89
|
+
const requiredRole = VERB_ROLE_GATE[verb];
|
|
90
|
+
if (requiredRole && ctx.role !== requiredRole) {
|
|
91
|
+
return {
|
|
92
|
+
verb,
|
|
93
|
+
sites: [],
|
|
94
|
+
skipped: true,
|
|
95
|
+
skippedReason: `role ${requiredRole} required, this box is ${ctx.role}`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const sites = await discoverSites(ctx.client, ctx.sitesDir ?? '/sites');
|
|
99
|
+
const op = ctx.ops?.[verb] ?? DEFAULT_OPS[verb];
|
|
100
|
+
const result = await op(ctx, sites);
|
|
101
|
+
// The dashboard PERSISTENCE is on-box wiring owned by this command layer, so
|
|
102
|
+
// it happens whether `status` used the default op or the injected
|
|
103
|
+
// `status-report` core op — the reused core produces the report, this
|
|
104
|
+
// wrapper writes it where the dashboard reads (and ONLY there).
|
|
105
|
+
if (verb === 'status') {
|
|
106
|
+
await writeStatusReport(ctx, result);
|
|
107
|
+
}
|
|
108
|
+
return { verb, ...result };
|
|
109
|
+
}
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Default (thin) core-op implementations.
|
|
112
|
+
//
|
|
113
|
+
// The `warm` and `status` defaults are the direct Kubo wiring the reference
|
|
114
|
+
// cloud-init scripts describe. The `republish`/`mirror` defaults DELEGATE to the
|
|
115
|
+
// owned record-sequence core (`../publisher/record-sequence.ts`) so the record
|
|
116
|
+
// SEQUENCE (export -> fetch -> routing/put -> fallback) has a SINGLE
|
|
117
|
+
// implementation shared by client and box (ADR-0002); `status` reuses the
|
|
118
|
+
// `status-report` core injected via `ctx.ops`.
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
/**
|
|
121
|
+
* Default `republish` (publisher): DELEGATE to the owned record-sequence core
|
|
122
|
+
* ({@link republishAndExport}). It refreshes the signed record (`name/publish`,
|
|
123
|
+
* ~72h/1h) and EXPORTS it (`routing/get`) to {@link NodeCommandContext.recordsDir}
|
|
124
|
+
* where replicas fetch it. One implementation, no bash/TS drift (ADR-0002).
|
|
125
|
+
*/
|
|
126
|
+
async function defaultRepublish(ctx, sites) {
|
|
127
|
+
return republishAndExport(ctx, sites);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Default `mirror` (replica): DELEGATE to the owned record-sequence core
|
|
131
|
+
* ({@link mirrorAndReannounce}). It fetches the publisher's exported record,
|
|
132
|
+
* re-announces it (`routing/put`), and FALLS BACK to the last cached record
|
|
133
|
+
* when the publisher endpoint is unreachable — NEVER signing.
|
|
134
|
+
*/
|
|
135
|
+
async function defaultMirror(ctx, sites) {
|
|
136
|
+
return mirrorAndReannounce(ctx, sites);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Default `warm`. Re-fetch each site's current CID through every configured
|
|
140
|
+
* gateway template (`{cid}` substituted); `.eth` names are ALSO warmed via
|
|
141
|
+
* eth.limo. Warming failures are recorded, never thrown (a cold gateway must
|
|
142
|
+
* not fail the whole run).
|
|
143
|
+
*/
|
|
144
|
+
async function defaultWarm(ctx, sites) {
|
|
145
|
+
const warm = ctx.gatewayFetch ?? httpFetchStatus;
|
|
146
|
+
const gateways = ctx.gateways ?? [];
|
|
147
|
+
const outcomes = [];
|
|
148
|
+
for (const site of sites) {
|
|
149
|
+
for (const template of gateways) {
|
|
150
|
+
const url = template.replaceAll('{cid}', site.cid);
|
|
151
|
+
await safeWarm(warm, url);
|
|
152
|
+
}
|
|
153
|
+
// eth.limo warming heuristic: an `id` ending `.eth` is ALSO warmed via
|
|
154
|
+
// eth.limo (keyed off the MFS entry `id`, the value that flows through
|
|
155
|
+
// here). Making this an explicit `ensName` hint is the sibling ENS-demotion
|
|
156
|
+
// idea's job, out of scope for this config-model change.
|
|
157
|
+
if (site.id.endsWith('.eth')) {
|
|
158
|
+
await safeWarm(warm, `https://${site.id}.limo/`);
|
|
159
|
+
}
|
|
160
|
+
outcomes.push({ id: site.id, cid: site.cid, status: 'warmed' });
|
|
161
|
+
}
|
|
162
|
+
return { sites: outcomes };
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Default `status` — a thin stand-in used ONLY when no `status` op is injected.
|
|
166
|
+
* The PRODUCTION on-box path (`pinnace node status`, wired in cli/run.ts's
|
|
167
|
+
* `runNodeCli`) injects the OWNED `status` op ({@link makeStatusOp} from
|
|
168
|
+
* status-report) via {@link NodeCommandContext.ops}.status, which is the real
|
|
169
|
+
* per-site CID/IPNS/announce/gateway report. This default remains as the
|
|
170
|
+
* seam's safe fallback (e.g. a direct `runNodeCommand` call that does not want
|
|
171
|
+
* the live external checks / a hermetic test that injects its own op), so a
|
|
172
|
+
* bare call still reports current CID + IPNS id without reaching the network.
|
|
173
|
+
* The dashboard write is done by the command layer ({@link runNodeCommand}).
|
|
174
|
+
*/
|
|
175
|
+
async function defaultStatus(ctx, sites) {
|
|
176
|
+
// Thin stand-in report until `status-report` supplies the owned op: current
|
|
177
|
+
// CID from discovery + IPNS id from the keystore. The external announce /
|
|
178
|
+
// gateway-serves checks are that task's concern (injected here). The
|
|
179
|
+
// dashboard write is done by the command layer ({@link runNodeCommand}), not
|
|
180
|
+
// here, so an injected op gets persisted the same way.
|
|
181
|
+
const keys = await listKeys(ctx.client);
|
|
182
|
+
const outcomes = sites.map((s) => ({
|
|
183
|
+
id: s.id,
|
|
184
|
+
cid: s.cid,
|
|
185
|
+
ipns: keys.get(s.id) ?? '',
|
|
186
|
+
}));
|
|
187
|
+
return { sites: outcomes };
|
|
188
|
+
}
|
|
189
|
+
/** Serialise a status result to `status.json` under the dashboard dir only. */
|
|
190
|
+
async function writeStatusReport(ctx, result) {
|
|
191
|
+
if (!ctx.dashboardDir)
|
|
192
|
+
return;
|
|
193
|
+
await mkdir(ctx.dashboardDir, { recursive: true });
|
|
194
|
+
const payload = { generated: new Date().toISOString(), sites: result.sites };
|
|
195
|
+
await writeFile(join(ctx.dashboardDir, 'status.json'), JSON.stringify(payload, null, 2));
|
|
196
|
+
}
|
|
197
|
+
/** The default op table (verb -> thin Kubo wiring). */
|
|
198
|
+
const DEFAULT_OPS = {
|
|
199
|
+
republish: defaultRepublish,
|
|
200
|
+
mirror: defaultMirror,
|
|
201
|
+
warm: defaultWarm,
|
|
202
|
+
status: defaultStatus,
|
|
203
|
+
};
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
// Small shared helpers.
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
/** Map site/key name -> IPNS id from `key/list -l`. */
|
|
208
|
+
async function listKeys(client) {
|
|
209
|
+
const res = await client.keyList();
|
|
210
|
+
const map = new Map();
|
|
211
|
+
for (const k of res.Keys ?? []) {
|
|
212
|
+
if (k?.Name && k?.Id)
|
|
213
|
+
map.set(k.Name, k.Id);
|
|
214
|
+
}
|
|
215
|
+
return map;
|
|
216
|
+
}
|
|
217
|
+
/** Warm one URL, swallowing any error (a cold gateway must not fail the run). */
|
|
218
|
+
async function safeWarm(warm, url) {
|
|
219
|
+
try {
|
|
220
|
+
await warm(url);
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
// Intentionally ignored: warming is best-effort.
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/** Production gateway warm: range-request the URL and return the HTTP status. */
|
|
227
|
+
async function httpFetchStatus(url) {
|
|
228
|
+
const res = await fetch(url, { headers: { range: 'bytes=0-0' } });
|
|
229
|
+
return res.status;
|
|
230
|
+
}
|
|
231
|
+
//# sourceMappingURL=node-commands.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node-commands.js","sourceRoot":"","sources":["../../src/node/node-commands.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,162 @@
|
|
|
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
|
+
* 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).
|
|
56
|
+
*/
|
|
57
|
+
import type { HostRole } from '../config/config-resolution.js';
|
|
58
|
+
/** The hosts pinnace can provision for. v1 = Hetzner ONLY. */
|
|
59
|
+
export type HostName = 'hetzner';
|
|
60
|
+
/** The hosts, in a stable order (help text / iteration / validation). */
|
|
61
|
+
export declare const HOST_PROVIDERS: readonly HostName[];
|
|
62
|
+
/**
|
|
63
|
+
* The per-box inputs the generator injects into the cloud-init (the values
|
|
64
|
+
* `make-cloud-init.sh` used to `sed` in). Everything that varies per box lives
|
|
65
|
+
* here; the hardened invariants are baked into the template.
|
|
66
|
+
*/
|
|
67
|
+
export interface ProvisionInput {
|
|
68
|
+
/** Which host to provision for (v1: `hetzner`). */
|
|
69
|
+
host: HostName;
|
|
70
|
+
/** DNS A-record pointed at this box, used for the HTTPS RPC API vhost. */
|
|
71
|
+
apiDomain: string;
|
|
72
|
+
/** Optional read-only status dashboard vhost (also serves exported records). */
|
|
73
|
+
dashboardDomain?: string;
|
|
74
|
+
/** Email for Let's Encrypt (ACME) certificate issuance. */
|
|
75
|
+
acmeEmail: string;
|
|
76
|
+
/**
|
|
77
|
+
* The long random bearer token guarding the RPC API. Kubo enforces it via
|
|
78
|
+
* `API.Authorizations` even on localhost; Caddy forwards the header through.
|
|
79
|
+
*/
|
|
80
|
+
bearerToken: string;
|
|
81
|
+
/**
|
|
82
|
+
* This box's role. `publisher` holds IPNS keys and signs/exports records;
|
|
83
|
+
* `replica` is keyless and mirrors the publisher's records. Written to the
|
|
84
|
+
* env file as `NODE_ROLE`, which the `pinnace node` timers self-gate on
|
|
85
|
+
* (republish only on publisher, mirror only on replica), so scheduling all
|
|
86
|
+
* timers on every box is safe.
|
|
87
|
+
*/
|
|
88
|
+
role: HostRole;
|
|
89
|
+
/**
|
|
90
|
+
* REPLICA ONLY: base URL to fetch the publisher's exported signed records
|
|
91
|
+
* from (the publisher's dashboard vhost). Ignored for publishers.
|
|
92
|
+
*/
|
|
93
|
+
publisherEndpoint?: string;
|
|
94
|
+
/**
|
|
95
|
+
* Browser origins allowed to call the RPC API (CORS). Space-separated in the
|
|
96
|
+
* env file. Defaults to the dashboard domain's URL when a dashboard is set.
|
|
97
|
+
*/
|
|
98
|
+
corsOrigins?: string;
|
|
99
|
+
/**
|
|
100
|
+
* Public gateways to warm through, each a template containing `{cid}`.
|
|
101
|
+
* Defaults to a sensible dweb.link/ipfs.io set. `.eth` MFS names are ALSO
|
|
102
|
+
* warmed via eth.limo automatically by `pinnace node warm`.
|
|
103
|
+
*/
|
|
104
|
+
gateways?: string[];
|
|
105
|
+
/** The Kubo version to install. Defaults to a pinned known-good release. */
|
|
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;
|
|
120
|
+
/** The MFS directory sites live under. Defaults to `/sites`. */
|
|
121
|
+
sitesDir?: string;
|
|
122
|
+
}
|
|
123
|
+
/** A single emitted file: where it goes + what it contains. */
|
|
124
|
+
export interface EmittedFile {
|
|
125
|
+
/** The path to write the file at (relative; the operator pastes/uploads it). */
|
|
126
|
+
path: string;
|
|
127
|
+
/** The full file contents. */
|
|
128
|
+
contents: string;
|
|
129
|
+
}
|
|
130
|
+
/** What a host provider returns: the ready-to-paste cloud-init file. */
|
|
131
|
+
export interface ProvisionResult {
|
|
132
|
+
/** Which host this was provisioned for (echoed for the caller). */
|
|
133
|
+
host: HostName;
|
|
134
|
+
/** The cloud-init file (path + contents). */
|
|
135
|
+
cloudInit: EmittedFile;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The `HostProvider` seam: one method that generates a host's provisioning
|
|
139
|
+
* artifact (cloud-init) for a given input. v1 has a single implementation
|
|
140
|
+
* ({@link hetznerHostProvider}); adding DigitalOcean / others later means
|
|
141
|
+
* adding another provider here and an entry in {@link HOST_PROVIDERS}; deploy/
|
|
142
|
+
* publish/status logic is untouched (spec user story 21).
|
|
143
|
+
*/
|
|
144
|
+
export interface HostProvider {
|
|
145
|
+
/** The host this provider provisions for. */
|
|
146
|
+
readonly host: HostName;
|
|
147
|
+
/** Generate the cloud-init (+ any host artifacts) for the given input. */
|
|
148
|
+
provision(input: ProvisionInput): ProvisionResult;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* The Hetzner provider: the first implementation of {@link HostProvider}.
|
|
152
|
+
* Emits the hardened-node cloud-init to a conventional filename.
|
|
153
|
+
*/
|
|
154
|
+
export declare const hetznerHostProvider: HostProvider;
|
|
155
|
+
/**
|
|
156
|
+
* Generate the provisioning cloud-init for the requested host: dispatches to
|
|
157
|
+
* the matching {@link HostProvider} and returns its cloud-init file. Throws
|
|
158
|
+
* LOUDLY on an unknown/unimplemented host (the seam exists so callers can add
|
|
159
|
+
* hosts later, but v1 only ships `hetzner`): never a silent no-op.
|
|
160
|
+
*/
|
|
161
|
+
export declare function provision(input: ProvisionInput): ProvisionResult;
|
|
162
|
+
//# 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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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"}
|