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,130 @@
1
+ /**
2
+ * Master-key -> per-site IPNS key derivation. **THIS IS A FROZEN CONTRACT.**
3
+ *
4
+ * Every `ipns`-mode site's key is derived deterministically from one operator
5
+ * master secret, so names are recoverable from the master alone and
6
+ * provisioning is stateless (CONTEXT.md `master key`, `keyId`). The scheme,
7
+ * pinned FOREVER by docs/adr/0001-frozen-ipns-key-derivation.md:
8
+ *
9
+ * seed = HKDF-SHA256(ikm = master, salt = "", info = "pinnace:ipns:v1:" + keyId, length = 32)
10
+ * -> the 32 bytes ARE the ed25519 private seed
11
+ * -> its public key IS the IPNS name, rendered as a CIDv1 `k51...` id.
12
+ *
13
+ * Frozen invariants (changing ANY of these moves every live name irreversibly):
14
+ * - the KDF is HKDF-SHA256;
15
+ * - the `info` prefix is exactly `pinnace:ipns:v1:` (the version `v1` lives in
16
+ * the `info` string, NOT in a separate parameter — a v2 scheme would use a
17
+ * new prefix and never re-derive existing names);
18
+ * - `keyId` is the SOLE per-site input, appended verbatim (UTF-8) to the info
19
+ * prefix. The ENS name is mutable and MUST NEVER enter derivation, so this
20
+ * surface has no `ensName` parameter at all;
21
+ * - the HKDF `salt` is the RFC 5869 default (empty / zero-length). The spec
22
+ * pins only ikm/info/length; an empty salt is the standard "no salt" case
23
+ * and is pinned here so the contract is total (see ADR-0001);
24
+ * - the 32 HKDF output bytes are used directly as the ed25519 seed (RFC 8032
25
+ * private key), NOT hashed or expanded again.
26
+ *
27
+ * Key IMPORT into a node keystore and the "no client signing" boundary are a
28
+ * SEPARATE concern (the `key-import-publisher` task): this module derives the
29
+ * key + id only, with no node, network, or deploy.
30
+ */
31
+ import { createPrivateKey, createPublicKey, hkdfSync } from 'node:crypto';
32
+ /** The frozen `info`-string prefix. The version (`v1`) is encoded HERE. */
33
+ export const IPNS_INFO_PREFIX = 'pinnace:ipns:v1:';
34
+ /** PKCS#8 DER prefix wrapping a raw 32-byte ed25519 seed (RFC 8410). */
35
+ const ED25519_PKCS8_PREFIX = Buffer.from('302e020100300506032b657004220420', 'hex');
36
+ /** base36 lowercase alphabet (multibase `k`), per the multiformats spec. */
37
+ const BASE36_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz';
38
+ function toBytes(master) {
39
+ return typeof master === 'string' ? new TextEncoder().encode(master) : master;
40
+ }
41
+ /**
42
+ * HKDF-SHA256(master, salt = "", info = IPNS_INFO_PREFIX + keyId, 32).
43
+ * Returns the 32-byte ed25519 seed. Frozen — see the module doc.
44
+ */
45
+ function deriveSeed(master, keyId) {
46
+ const info = new TextEncoder().encode(IPNS_INFO_PREFIX + keyId);
47
+ const seed = hkdfSync('sha256', toBytes(master), new Uint8Array(0), // RFC 5869 default (empty) salt — pinned by ADR-0001.
48
+ info, 32);
49
+ return new Uint8Array(seed);
50
+ }
51
+ /** Derive the raw 32-byte ed25519 public key from a 32-byte private seed. */
52
+ function ed25519PublicKeyFromSeed(seed) {
53
+ const pkcs8 = Buffer.concat([ED25519_PKCS8_PREFIX, Buffer.from(seed)]);
54
+ const priv = createPrivateKey({ key: pkcs8, format: 'der', type: 'pkcs8' });
55
+ const spki = createPublicKey(priv).export({ format: 'der', type: 'spki' });
56
+ // An ed25519 SPKI DER is a fixed 12-byte header + the 32 raw public bytes.
57
+ return new Uint8Array(spki.subarray(spki.length - 32));
58
+ }
59
+ /** Encode bytes as multibase-less base36 (lowercase), preserving leading zeros. */
60
+ function base36Encode(bytes) {
61
+ const digits = [0];
62
+ for (const byte of bytes) {
63
+ let carry = byte;
64
+ for (let i = 0; i < digits.length; i++) {
65
+ const value = digits[i] * 256 + carry;
66
+ digits[i] = value % 36;
67
+ carry = (value / 36) | 0;
68
+ }
69
+ while (carry > 0) {
70
+ digits.push(carry % 36);
71
+ carry = (carry / 36) | 0;
72
+ }
73
+ }
74
+ let out = '';
75
+ for (let i = 0; i < bytes.length && bytes[i] === 0; i++)
76
+ out += '0';
77
+ for (let i = digits.length - 1; i >= 0; i--)
78
+ out += BASE36_ALPHABET[digits[i]];
79
+ return out;
80
+ }
81
+ /**
82
+ * Encode a raw 32-byte ed25519 public key as an IPNS name (`k51...`).
83
+ *
84
+ * The name is a CIDv1 with the libp2p-key multicodec, whose multihash inlines
85
+ * the libp2p PublicKey protobuf via the identity hash (the default for
86
+ * ed25519), rendered in case-insensitive base36 with the `k` multibase prefix:
87
+ *
88
+ * 0x01 (CIDv1) 0x72 (libp2p-key)
89
+ * 0x00 (identity mh) 0x24 (len 36)
90
+ * 0x08 0x01 (protobuf field1 Type = Ed25519) 0x12 0x20 (field2 Data, len 32)
91
+ * <32 raw public key bytes>
92
+ *
93
+ * @see https://specs.ipfs.tech/ipns/ipns-record/
94
+ */
95
+ function ipnsIdFromPublicKey(publicKey) {
96
+ const pbHeader = Uint8Array.from([0x08, 0x01, 0x12, 0x20]);
97
+ const protobuf = new Uint8Array(pbHeader.length + publicKey.length);
98
+ protobuf.set(pbHeader, 0);
99
+ protobuf.set(publicKey, pbHeader.length);
100
+ const multihash = new Uint8Array(2 + protobuf.length);
101
+ multihash[0] = 0x00; // identity multihash function
102
+ multihash[1] = protobuf.length; // digest length (36)
103
+ multihash.set(protobuf, 2);
104
+ const cid = new Uint8Array(2 + multihash.length);
105
+ cid[0] = 0x01; // CIDv1
106
+ cid[1] = 0x72; // libp2p-key multicodec
107
+ cid.set(multihash, 2);
108
+ return 'k' + base36Encode(cid); // multibase base36 (lowercase) prefix.
109
+ }
110
+ /**
111
+ * Derive the full per-site IPNS key (seed + public key + `k51...` id) from the
112
+ * master secret and the frozen `keyId`. Pure and deterministic — no node,
113
+ * network, or deploy. This is the FROZEN CONTRACT; see the module doc.
114
+ */
115
+ export function deriveIpnsKey(input) {
116
+ const seed = deriveSeed(input.master, input.keyId);
117
+ const publicKey = ed25519PublicKeyFromSeed(seed);
118
+ const ipnsId = ipnsIdFromPublicKey(publicKey);
119
+ return { seed, publicKey, ipnsId };
120
+ }
121
+ /**
122
+ * Derive-and-print path (user story 22): return a site's `k51...` IPNS id from
123
+ * the master + keyId with NO deploy and NO network, so an operator can set the
124
+ * ENS contenthash to `ipns://<id>` before the first deploy. The id depends ONLY
125
+ * on (master, keyId) — never the ENS name.
126
+ */
127
+ export function deriveIpnsId(input) {
128
+ return deriveIpnsKey(input).ipnsId;
129
+ }
130
+ //# sourceMappingURL=ipns-key-derivation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ipns-key-derivation.js","sourceRoot":"","sources":["../../src/derive/ipns-key-derivation.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * pinnace library core — the public TypeScript API.
3
+ *
4
+ * All logic lives in the core (see CONTEXT.md `core vs cli`); the `pinnace`
5
+ * CLI bin (src/cli/bin.ts) is a thin wrapper that imports from here, so every
6
+ * operation is equally usable as a TypeScript API. Subsequent tracer-bullet
7
+ * tasks (Kubo RPC client, CAR build, key derivation, cloud-init, CI emitter,
8
+ * status, config, deploy, site management, CLI) hang off this entrypoint.
9
+ */
10
+ /** The package name, exposed so the CLI and API can report a consistent identity. */
11
+ export declare const PINNACE = "pinnace";
12
+ /** Returns the pinnace package name (a trivial seam proving the toolchain is wired). */
13
+ export declare function name(): string;
14
+ export { KuboRpcClient, KuboRpcError, type KuboRpcClientOptions, type FetchLike, type FilesMkdirOptions, type FilesRmOptions, type NamePublishOptions, } from './rpc/kubo-rpc-client.js';
15
+ export { MockKuboApi, type RecordedRequest, type MockResponseSpec, } from './rpc/mock-kubo.js';
16
+ export { buildCar, writeCar, type BuiltCar } from './car/car-build.js';
17
+ export { deriveIpnsKey, deriveIpnsId, IPNS_INFO_PREFIX, type Master, type DeriveIpnsInput, type DerivedIpnsKey, } from './derive/ipns-key-derivation.js';
18
+ export { resolveConfig, resolveMasterSecret, resolveHostToken, hostTokenEnvVar, MissingHostTokenError, type PinnaceConfigFile, type ResolvedConfig, type HostConfig, type SiteConfig, type HostRole, type SiteMode, type EnvRecord, type CliOverrides, type ResolveConfigInput, type ResolveMasterInput, type ResolveHostTokenInput, } from './config/config-resolution.js';
19
+ export { emitCi, githubCiProvider, CI_SYSTEMS, type CiSystem, type CIProvider, type EmitCiInput, type EmittedCi, type EmittedFile, type RequiredCiSetting, } from './ci/ci-emit.js';
20
+ export { provision, hetznerHostProvider, HOST_PROVIDERS, type HostName, type HostProvider, type ProvisionInput, type ProvisionResult, } from './provision/cloud-init.js';
21
+ export { serializeIpnsKeyForImport, importIpnsKeyIntoPublisher, KeyImportRoleError, LIBP2P_ED25519_PRIVATE_KEY_PREFIX, type ImportIpnsKeyInput, type KeyImportResult, } from './publisher/key-import.js';
22
+ export { republishAndExport, mirrorAndReannounce, makeRepublishOp, makeMirrorOp, promoteReplicaToPublisher, RECORD_LIFETIME, RECORD_TTL, type PromoteReplicaInput, type PromoteReplicaResult, } from './publisher/record-sequence.js';
23
+ export { runNodeCommand, discoverSites, NODE_VERBS, type NodeVerb, type NodeCommandContext, type NodeCommandOps, type NodeCommandResult, type NodeOpResult, type DiscoveredSite, type SiteOutcome, type PublisherFetch, type GatewayFetch, } from './node/node-commands.js';
24
+ export { listSites, removeSite, addSite, placeInMfs, SITE_VERBS, type SiteVerb, type SiteListing, type ListSitesInput, type RemoveSiteInput, type RemoveSiteResult, type AddSiteInput, type AddSiteResult, } from './site/site-management.js';
25
+ export { deploy, type DeployInput, type DeployTarget, type DeployResult, type DeployNodeOk, type DeployNodeFailure, } from './deploy/deploy.js';
26
+ export { statusReport, makeStatusOp, defaultProvidersLookup, defaultGatewayProbe, type StatusReport, type SiteStatus, type StatusReportInput, type ProvidersLookup, type ProvidersResponse, type GatewayProbe, } from './status/status-report.js';
27
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * pinnace library core — the public TypeScript API.
3
+ *
4
+ * All logic lives in the core (see CONTEXT.md `core vs cli`); the `pinnace`
5
+ * CLI bin (src/cli/bin.ts) is a thin wrapper that imports from here, so every
6
+ * operation is equally usable as a TypeScript API. Subsequent tracer-bullet
7
+ * tasks (Kubo RPC client, CAR build, key derivation, cloud-init, CI emitter,
8
+ * status, config, deploy, site management, CLI) hang off this entrypoint.
9
+ */
10
+ /** The package name, exposed so the CLI and API can report a consistent identity. */
11
+ export const PINNACE = 'pinnace';
12
+ /** Returns the pinnace package name (a trivial seam proving the toolchain is wired). */
13
+ export function name() {
14
+ return PINNACE;
15
+ }
16
+ export { KuboRpcClient, KuboRpcError, } from './rpc/kubo-rpc-client.js';
17
+ export { MockKuboApi, } from './rpc/mock-kubo.js';
18
+ export { buildCar, writeCar } from './car/car-build.js';
19
+ export { deriveIpnsKey, deriveIpnsId, IPNS_INFO_PREFIX, } from './derive/ipns-key-derivation.js';
20
+ export { resolveConfig, resolveMasterSecret, resolveHostToken, hostTokenEnvVar, MissingHostTokenError, } from './config/config-resolution.js';
21
+ export { emitCi, githubCiProvider, CI_SYSTEMS, } from './ci/ci-emit.js';
22
+ export { provision, hetznerHostProvider, HOST_PROVIDERS, } from './provision/cloud-init.js';
23
+ export { serializeIpnsKeyForImport, importIpnsKeyIntoPublisher, KeyImportRoleError, LIBP2P_ED25519_PRIVATE_KEY_PREFIX, } from './publisher/key-import.js';
24
+ export { republishAndExport, mirrorAndReannounce, makeRepublishOp, makeMirrorOp, promoteReplicaToPublisher, RECORD_LIFETIME, RECORD_TTL, } from './publisher/record-sequence.js';
25
+ export { runNodeCommand, discoverSites, NODE_VERBS, } from './node/node-commands.js';
26
+ export { listSites, removeSite, addSite, placeInMfs, SITE_VERBS, } from './site/site-management.js';
27
+ export { deploy, } from './deploy/deploy.js';
28
+ export { statusReport, makeStatusOp, defaultProvidersLookup, defaultGatewayProbe, } from './status/status-report.js';
29
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,qFAAqF;AACrF,MAAM,CAAC,MAAM,OAAO,GAAG,SAAS,CAAC;AAEjC,wFAAwF;AACxF,MAAM,UAAU,IAAI;IACnB,OAAO,OAAO,CAAC;AAChB,CAAC;AAED,OAAO,EACN,aAAa,EACb,YAAY,GAMZ,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACN,WAAW,GAGX,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAC,QAAQ,EAAE,QAAQ,EAAgB,MAAM,oBAAoB,CAAC;AACrE,OAAO,EACN,aAAa,EACb,YAAY,EACZ,gBAAgB,GAIhB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EACN,aAAa,EACb,mBAAmB,EACnB,gBAAgB,EAChB,eAAe,EACf,qBAAqB,GAYrB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACN,MAAM,EACN,gBAAgB,EAChB,UAAU,GAOV,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACN,SAAS,EACT,mBAAmB,EACnB,cAAc,GAKd,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACN,yBAAyB,EACzB,0BAA0B,EAC1B,kBAAkB,EAClB,iCAAiC,GAGjC,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACN,kBAAkB,EAClB,mBAAmB,EACnB,eAAe,EACf,YAAY,EACZ,yBAAyB,EACzB,eAAe,EACf,UAAU,GAGV,MAAM,gCAAgC,CAAC;AACxC,OAAO,EACN,cAAc,EACd,aAAa,EACb,UAAU,GAUV,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACN,SAAS,EACT,UAAU,EACV,OAAO,EACP,UAAU,EACV,UAAU,GAQV,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACN,MAAM,GAMN,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACN,YAAY,EACZ,YAAY,EACZ,sBAAsB,EACtB,mBAAmB,GAOnB,MAAM,2BAA2B,CAAC"}
@@ -0,0 +1,115 @@
1
+ import type { HostRole } from '../config/config-resolution.js';
2
+ import type { KuboRpcClient } from '../rpc/kubo-rpc-client.js';
3
+ /** The four on-box verbs, under the `node` namespace. */
4
+ export type NodeVerb = 'republish' | 'mirror' | 'warm' | 'status';
5
+ /** The verbs, in a stable order (for help text / iteration). */
6
+ export declare const NODE_VERBS: readonly NodeVerb[];
7
+ /** One site as auto-discovered from MFS `/sites/*`: its `id` and current CID. */
8
+ export interface DiscoveredSite {
9
+ /** The MFS entry `id` under `/sites/` (the site's single identifier). */
10
+ id: string;
11
+ /** The current content root CID (`files/stat --hash`). */
12
+ cid: string;
13
+ }
14
+ /** Per-site outcome line a verb reports (shape is verb-specific but uniform). */
15
+ export interface SiteOutcome {
16
+ /** The site's single `id` (its MFS entry / KDF input). */
17
+ id: string;
18
+ cid?: string;
19
+ ipns?: string;
20
+ /** A short machine-readable status token (e.g. `re-announced`, `no-record`). */
21
+ status?: string;
22
+ /**
23
+ * `status`-verb only: whether the network announces this node for the CID
24
+ * (the delegated-routing providers list contains our PeerID). Owned + filled
25
+ * by the `status-report` core op; absent for other verbs.
26
+ */
27
+ announced?: boolean;
28
+ /** `status`-verb only: whether a cold public gateway served the CID (2xx/206). */
29
+ gatewayServes?: boolean;
30
+ /** `status`-verb only: the raw HTTP status the cold-gateway probe returned. */
31
+ gatewayHttp?: number;
32
+ }
33
+ /** The uniform result an op returns: the per-site outcomes it produced. */
34
+ export interface NodeOpResult {
35
+ sites: SiteOutcome[];
36
+ }
37
+ /** The full result {@link runNodeCommand} returns for one verb invocation. */
38
+ export interface NodeCommandResult extends NodeOpResult {
39
+ /** The verb that ran. */
40
+ verb: NodeVerb;
41
+ /** True when role-gating (or a missing precondition) skipped the op. */
42
+ skipped?: boolean;
43
+ /** Why it was skipped (e.g. `role publisher required, this box is replica`). */
44
+ skippedReason?: string;
45
+ }
46
+ /**
47
+ * An injectable HTTP fetch for a single URL, returning the body text. Used for
48
+ * fetching the publisher's exported record on a replica. Tests inject a fake;
49
+ * production passes a `fetch`-backed implementation. Throwing signals the
50
+ * endpoint is unreachable (the replica then falls back to its cache).
51
+ */
52
+ export type PublisherFetch = (url: string) => Promise<string>;
53
+ /**
54
+ * An injectable HTTP fetch for gateway WARMING: fetch the URL (a small range is
55
+ * enough to seat the cache) and return the HTTP status. Tests inject a fake so
56
+ * no live gateway is hit.
57
+ */
58
+ export type GatewayFetch = (url: string) => Promise<number>;
59
+ /**
60
+ * The four core operations, injectable so the parallel `publisher-replica-model`
61
+ * (republish/mirror) and `status-report` (status) tasks can supply the owned,
62
+ * fully-tested implementations behind this SAME seam. Any op left unset uses
63
+ * this module's thin default (the direct Kubo wiring the reference describes).
64
+ */
65
+ export interface NodeCommandOps {
66
+ republish: (ctx: NodeCommandContext, sites: DiscoveredSite[]) => Promise<NodeOpResult>;
67
+ mirror: (ctx: NodeCommandContext, sites: DiscoveredSite[]) => Promise<NodeOpResult>;
68
+ warm: (ctx: NodeCommandContext, sites: DiscoveredSite[]) => Promise<NodeOpResult>;
69
+ status: (ctx: NodeCommandContext, sites: DiscoveredSite[]) => Promise<NodeOpResult>;
70
+ }
71
+ /**
72
+ * Everything a verb needs to run on the box. On-box PATHS (records/cache/
73
+ * dashboard) are explicit fields so tests isolate them to temp fixtures and
74
+ * production points them at the real box locations (`/var/www/ipfs-dash/...`,
75
+ * `/var/lib/ipfs/records`) — no global default location is baked into the
76
+ * logic.
77
+ */
78
+ export interface NodeCommandContext {
79
+ /** The LOCAL Kubo RPC client (this box's daemon). */
80
+ client: KuboRpcClient;
81
+ /** This box's role; gates `republish` (publisher) and `mirror` (replica). */
82
+ role: HostRole;
83
+ /** The MFS directory sites live under (default `/sites`). */
84
+ sitesDir?: string;
85
+ /** Gateway URL templates with a `{cid}` placeholder to warm through. */
86
+ gateways?: string[];
87
+ /** For replicas: base URL to fetch the publisher's exported records from. */
88
+ publisherEndpoint?: string;
89
+ /** Where the publisher EXPORTS signed records (replicas fetch these). */
90
+ recordsDir?: string;
91
+ /** Where a replica CACHES the last good record for fallback. */
92
+ cacheDir?: string;
93
+ /** Where `status` writes its dashboard JSON. */
94
+ dashboardDir?: string;
95
+ /** Injected publisher-record fetch (replica); defaults to a `fetch` call. */
96
+ publisherFetch?: PublisherFetch;
97
+ /** Injected gateway warm fetch; defaults to a `fetch` range request. */
98
+ gatewayFetch?: GatewayFetch;
99
+ /** Override any of the four core ops (parallel tasks supply the owned impl). */
100
+ ops?: Partial<NodeCommandOps>;
101
+ }
102
+ /**
103
+ * Auto-discover sites from MFS `/sites/*`: list the directory, then stat each
104
+ * entry for its current CID. Entries without a resolvable CID are skipped.
105
+ * Shared by every verb (the reference scripts all begin with this same walk).
106
+ */
107
+ export declare function discoverSites(client: KuboRpcClient, sitesDir?: string): Promise<DiscoveredSite[]>;
108
+ /**
109
+ * Run one on-box verb. Resolves the role gate first (a wrong-role verb is a
110
+ * SKIP — a clean no-op that touches no Kubo RPC, so scheduling all timers on
111
+ * every box is safe), then discovers sites and dispatches to the (possibly
112
+ * injected) core op. Throws on an unknown verb (loud, never a silent no-op).
113
+ */
114
+ export declare function runNodeCommand(verb: NodeVerb, ctx: NodeCommandContext): Promise<NodeCommandResult>;
115
+ //# sourceMappingURL=node-commands.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node-commands.d.ts","sourceRoot":"","sources":["../../src/node/node-commands.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,227 @@
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`. Reuses the `status-report` core logic (injected via
166
+ * {@link NodeCommandContext.ops}.status; this default is a thin stand-in until
167
+ * that task lands) and writes the resulting report as `status.json` under the
168
+ * box's dashboard directory ONLY. Reporting per-site CID / IPNS id happens in
169
+ * the reused core; this wrapper just persists it where the dashboard reads.
170
+ */
171
+ async function defaultStatus(ctx, sites) {
172
+ // Thin stand-in report until `status-report` supplies the owned op: current
173
+ // CID from discovery + IPNS id from the keystore. The external announce /
174
+ // gateway-serves checks are that task's concern (injected here). The
175
+ // dashboard write is done by the command layer ({@link runNodeCommand}), not
176
+ // here, so an injected op gets persisted the same way.
177
+ const keys = await listKeys(ctx.client);
178
+ const outcomes = sites.map((s) => ({
179
+ id: s.id,
180
+ cid: s.cid,
181
+ ipns: keys.get(s.id) ?? '',
182
+ }));
183
+ return { sites: outcomes };
184
+ }
185
+ /** Serialise a status result to `status.json` under the dashboard dir only. */
186
+ async function writeStatusReport(ctx, result) {
187
+ if (!ctx.dashboardDir)
188
+ return;
189
+ await mkdir(ctx.dashboardDir, { recursive: true });
190
+ const payload = { generated: new Date().toISOString(), sites: result.sites };
191
+ await writeFile(join(ctx.dashboardDir, 'status.json'), JSON.stringify(payload, null, 2));
192
+ }
193
+ /** The default op table (verb -> thin Kubo wiring). */
194
+ const DEFAULT_OPS = {
195
+ republish: defaultRepublish,
196
+ mirror: defaultMirror,
197
+ warm: defaultWarm,
198
+ status: defaultStatus,
199
+ };
200
+ // ---------------------------------------------------------------------------
201
+ // Small shared helpers.
202
+ // ---------------------------------------------------------------------------
203
+ /** Map site/key name -> IPNS id from `key/list -l`. */
204
+ async function listKeys(client) {
205
+ const res = await client.keyList();
206
+ const map = new Map();
207
+ for (const k of res.Keys ?? []) {
208
+ if (k?.Name && k?.Id)
209
+ map.set(k.Name, k.Id);
210
+ }
211
+ return map;
212
+ }
213
+ /** Warm one URL, swallowing any error (a cold gateway must not fail the run). */
214
+ async function safeWarm(warm, url) {
215
+ try {
216
+ await warm(url);
217
+ }
218
+ catch {
219
+ // Intentionally ignored: warming is best-effort.
220
+ }
221
+ }
222
+ /** Production gateway warm: range-request the URL and return the HTTP status. */
223
+ async function httpFetchStatus(url) {
224
+ const res = await fetch(url, { headers: { range: 'bytes=0-0' } });
225
+ return res.status;
226
+ }
227
+ //# 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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"}