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,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A typed, per-node Kubo RPC client. It wraps `POST {baseUrl}/api/v0/<path>`
|
|
3
|
+
* with an `Authorization: Bearer <token>` header on EVERY call. No Kubo binary
|
|
4
|
+
* runs on the client — all interaction is HTTP.
|
|
5
|
+
*
|
|
6
|
+
* This is the seam every host-agnostic pinnace operation (deploy, publish,
|
|
7
|
+
* status, site management) speaks. It is deliberately PER-NODE (one base URL +
|
|
8
|
+
* one token); multi-target fan-out lives in the deploy task, not here.
|
|
9
|
+
*
|
|
10
|
+
* Behaviour ported (not copied) from the reference prototype
|
|
11
|
+
* `~/searches/ipfs-hetzner/deploy-car.mjs`: the exact query params
|
|
12
|
+
* (`files/mkdir?arg=/sites&parents=true`, `dag/import?pin-roots=true`, ...) and
|
|
13
|
+
* the throw-on-`!ok` error handling.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* A loud error carrying the endpoint + HTTP status of a non-2xx Kubo response,
|
|
17
|
+
* so a caller (and a log) can see exactly which call failed and how.
|
|
18
|
+
*/
|
|
19
|
+
export class KuboRpcError extends Error {
|
|
20
|
+
endpoint;
|
|
21
|
+
status;
|
|
22
|
+
bodyText;
|
|
23
|
+
constructor(
|
|
24
|
+
/** The `/api/v0/<endpoint>` path that failed. */
|
|
25
|
+
endpoint,
|
|
26
|
+
/** The HTTP status returned. */
|
|
27
|
+
status,
|
|
28
|
+
/** The response body text (for diagnostics). */
|
|
29
|
+
bodyText) {
|
|
30
|
+
super(`Kubo RPC ${endpoint} failed: ${status} ${bodyText}`);
|
|
31
|
+
this.endpoint = endpoint;
|
|
32
|
+
this.status = status;
|
|
33
|
+
this.bodyText = bodyText;
|
|
34
|
+
this.name = 'KuboRpcError';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export class KuboRpcClient {
|
|
38
|
+
baseUrl;
|
|
39
|
+
token;
|
|
40
|
+
fetchImpl;
|
|
41
|
+
constructor(options) {
|
|
42
|
+
// Normalise: strip trailing slashes so path joining is unambiguous.
|
|
43
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/, '');
|
|
44
|
+
this.token = options.token;
|
|
45
|
+
this.fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The core request primitive: POST `/api/v0/<endpoint>?<query>` with the
|
|
49
|
+
* bearer token. Throws {@link KuboRpcError} on any non-2xx. Returns the raw
|
|
50
|
+
* {@link Response} so callers can decode JSON or bytes as they need.
|
|
51
|
+
*/
|
|
52
|
+
async request(endpoint, query, body, extraHeaders) {
|
|
53
|
+
const qs = query && [...query.keys()].length > 0 ? `?${query.toString()}` : '';
|
|
54
|
+
const url = `${this.baseUrl}/api/v0/${endpoint}${qs}`;
|
|
55
|
+
const res = await this.fetchImpl(url, {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: { authorization: `Bearer ${this.token}`, ...(extraHeaders ?? {}) },
|
|
58
|
+
body,
|
|
59
|
+
});
|
|
60
|
+
if (!res.ok) {
|
|
61
|
+
const text = await safeText(res);
|
|
62
|
+
throw new KuboRpcError(endpoint, res.status, text);
|
|
63
|
+
}
|
|
64
|
+
return res;
|
|
65
|
+
}
|
|
66
|
+
/** POST an endpoint and parse the JSON response body. */
|
|
67
|
+
async requestJson(endpoint, query, body, extraHeaders) {
|
|
68
|
+
const res = await this.request(endpoint, query, body, extraHeaders);
|
|
69
|
+
return (await res.json());
|
|
70
|
+
}
|
|
71
|
+
/** `id` — the node's identity (PeerID etc.). */
|
|
72
|
+
id() {
|
|
73
|
+
return this.requestJson('id');
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* `add` — add raw bytes (returned shape is Kubo-defined). Kubo requires the
|
|
77
|
+
* payload as a `multipart/form-data` `file` part, NOT a raw body, so we send
|
|
78
|
+
* a {@link FormData} and let `fetch` set the multipart boundary itself (see
|
|
79
|
+
* {@link fileUpload}).
|
|
80
|
+
*/
|
|
81
|
+
add(data) {
|
|
82
|
+
return this.fileUpload('add', undefined, data);
|
|
83
|
+
}
|
|
84
|
+
/** `dag/import?pin-roots=true` — import a CAR and pin its roots. */
|
|
85
|
+
dagImport(car) {
|
|
86
|
+
const q = new URLSearchParams({ 'pin-roots': 'true' });
|
|
87
|
+
return this.fileUpload('dag/import', q, car);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* `pin/rm?arg=<cid>` — UNPIN content so Kubo can garbage-collect it and
|
|
91
|
+
* reclaim storage. The counterpart to `dag/import?pin-roots=true`: removing a
|
|
92
|
+
* site unpins its CID here so it stops being served/announced. `arg` takes a
|
|
93
|
+
* bare CID (or an `/ipfs/<cid>` path); callers pass the CID from `files/stat`.
|
|
94
|
+
*/
|
|
95
|
+
pinRm(cid) {
|
|
96
|
+
return this.requestJson('pin/rm', new URLSearchParams({ arg: cid }));
|
|
97
|
+
}
|
|
98
|
+
/** `files/mkdir?arg=<path>[&parents=true]`. */
|
|
99
|
+
async filesMkdir(path, options = {}) {
|
|
100
|
+
const q = new URLSearchParams({ arg: path });
|
|
101
|
+
if (options.parents)
|
|
102
|
+
q.set('parents', 'true');
|
|
103
|
+
await this.request('files/mkdir', q);
|
|
104
|
+
}
|
|
105
|
+
/** `files/rm?arg=<path>[&recursive=true][&force=true]`. */
|
|
106
|
+
async filesRm(path, options = {}) {
|
|
107
|
+
const q = new URLSearchParams({ arg: path });
|
|
108
|
+
if (options.recursive)
|
|
109
|
+
q.set('recursive', 'true');
|
|
110
|
+
if (options.force)
|
|
111
|
+
q.set('force', 'true');
|
|
112
|
+
await this.request('files/rm', q);
|
|
113
|
+
}
|
|
114
|
+
/** `files/cp?arg=<from>&arg=<to>`. */
|
|
115
|
+
async filesCp(from, to) {
|
|
116
|
+
const q = new URLSearchParams();
|
|
117
|
+
q.append('arg', from);
|
|
118
|
+
q.append('arg', to);
|
|
119
|
+
await this.request('files/cp', q);
|
|
120
|
+
}
|
|
121
|
+
/** `files/stat?arg=<path>` — returns the MFS entry stat (Hash, Type, ...). */
|
|
122
|
+
filesStat(path) {
|
|
123
|
+
return this.requestJson('files/stat', new URLSearchParams({ arg: path }));
|
|
124
|
+
}
|
|
125
|
+
/** `files/ls?arg=<path>[&l=true]` — list an MFS directory. */
|
|
126
|
+
filesLs(path, long = true) {
|
|
127
|
+
const q = new URLSearchParams({ arg: path });
|
|
128
|
+
if (long)
|
|
129
|
+
q.set('long', 'true');
|
|
130
|
+
return this.requestJson('files/ls', q);
|
|
131
|
+
}
|
|
132
|
+
/** `key/list?l=true` — list keystore keys with their IPNS ids. */
|
|
133
|
+
keyList() {
|
|
134
|
+
return this.requestJson('key/list', new URLSearchParams({ l: 'true' }));
|
|
135
|
+
}
|
|
136
|
+
/** `key/gen?arg=<name>&type=ed25519` — generate a new keystore key. */
|
|
137
|
+
keyGen(name) {
|
|
138
|
+
const q = new URLSearchParams({ arg: name, type: 'ed25519' });
|
|
139
|
+
return this.requestJson('key/gen', q);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* `key/import?arg=<name>` — import key MATERIAL (libp2p/protobuf bytes) into
|
|
143
|
+
* the keystore. The node, not the client, signs records with it.
|
|
144
|
+
*/
|
|
145
|
+
keyImport(name, keyBytes) {
|
|
146
|
+
const q = new URLSearchParams({ arg: name });
|
|
147
|
+
return this.fileUpload('key/import', q, keyBytes);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Kubo's file-upload endpoints require the payload as a `multipart/form-data`
|
|
151
|
+
* body with the bytes as a named file part (confirmed by the Kubo RPC docs'
|
|
152
|
+
* `-F <field>=@…` cURL examples and a live daemon). A raw
|
|
153
|
+
* `application/octet-stream` body is rejected with
|
|
154
|
+
* `400 file argument '<field>' is required`.
|
|
155
|
+
*
|
|
156
|
+
* The field name is endpoint-specific: `add`, `dag/import` and `key/import`
|
|
157
|
+
* expect `file` (the default), while `routing/put` expects `value-file`.
|
|
158
|
+
*
|
|
159
|
+
* We build a {@link FormData} and pass it as the body WITHOUT a hand-set
|
|
160
|
+
* `content-type`: `fetch` serialises the FormData and sets
|
|
161
|
+
* `content-type: multipart/form-data; boundary=…` itself. Setting it manually
|
|
162
|
+
* would omit/override the boundary and break the request.
|
|
163
|
+
*/
|
|
164
|
+
fileUpload(endpoint, query, bytes, field = 'file') {
|
|
165
|
+
const form = new FormData();
|
|
166
|
+
form.append(field, new Blob([bytes]), field);
|
|
167
|
+
return this.requestJson(endpoint, query, form);
|
|
168
|
+
}
|
|
169
|
+
/** `name/publish?arg=<cidPath>&key=<key>&lifetime=..&ttl=..` — sign+publish IPNS. */
|
|
170
|
+
namePublish(options) {
|
|
171
|
+
const q = new URLSearchParams({ arg: options.cidPath, key: options.key });
|
|
172
|
+
if (options.lifetime)
|
|
173
|
+
q.set('lifetime', options.lifetime);
|
|
174
|
+
if (options.ttl)
|
|
175
|
+
q.set('ttl', options.ttl);
|
|
176
|
+
if (options.allowOffline)
|
|
177
|
+
q.set('allow-offline', 'true');
|
|
178
|
+
return this.requestJson('name/publish', q);
|
|
179
|
+
}
|
|
180
|
+
/** `routing/get?arg=<ipnsPath>` — fetch the raw signed record for a name. */
|
|
181
|
+
async routingGet(ipnsPath) {
|
|
182
|
+
const res = await this.request('routing/get', new URLSearchParams({ arg: ipnsPath }));
|
|
183
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* `routing/put?arg=<ipnsPath>` — (re-)announce a signed record body. Kubo's
|
|
187
|
+
* `routing/put` takes the record as a `multipart/form-data` file part named
|
|
188
|
+
* **`value-file`** (NOT the generic `file` the other upload endpoints use); a
|
|
189
|
+
* raw `application/octet-stream` body is rejected with
|
|
190
|
+
* `400 file argument 'value-file' is required`. Goes through {@link fileUpload}
|
|
191
|
+
* so `fetch` owns the multipart boundary (no hand-set content-type).
|
|
192
|
+
*/
|
|
193
|
+
async routingPut(ipnsPath, record) {
|
|
194
|
+
const q = new URLSearchParams({ arg: ipnsPath });
|
|
195
|
+
await this.fileUpload('routing/put', q, record, 'value-file');
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/** Read a response body as text without throwing (best-effort for errors). */
|
|
199
|
+
async function safeText(res) {
|
|
200
|
+
try {
|
|
201
|
+
return await res.text();
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return '';
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
//# sourceMappingURL=kubo-rpc-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kubo-rpc-client.js","sourceRoot":"","sources":["../../src/rpc/kubo-rpc-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A mock Kubo HTTP API for tests: it RECORDS every incoming request (method,
|
|
3
|
+
* path, query, headers, body) and returns canned responses, so a test can
|
|
4
|
+
* assert the exact call shape a `KuboRpcClient` produced WITHOUT a live daemon.
|
|
5
|
+
*
|
|
6
|
+
* This is the primary test boundary for every host-agnostic pinnace operation
|
|
7
|
+
* (deploy, publish, status, site management). It is exposed from the package's
|
|
8
|
+
* test surface so sibling tasks (deploy-multi-target, status-report, ...) reuse
|
|
9
|
+
* it rather than re-inventing a Kubo fake.
|
|
10
|
+
*/
|
|
11
|
+
/** One multipart file part the mock extracted from a `FormData` body. */
|
|
12
|
+
export interface RecordedFilePart {
|
|
13
|
+
/** The multipart field name (Kubo's file-upload endpoints expect `file`). */
|
|
14
|
+
field: string;
|
|
15
|
+
/** The optional filename the part was appended with. */
|
|
16
|
+
filename?: string;
|
|
17
|
+
/** The part's bytes. */
|
|
18
|
+
bytes: Uint8Array;
|
|
19
|
+
}
|
|
20
|
+
/** One recorded RPC request as the mock observed it. */
|
|
21
|
+
export interface RecordedRequest {
|
|
22
|
+
/** The HTTP method (Kubo RPC is always POST). */
|
|
23
|
+
method: string;
|
|
24
|
+
/** The `/api/v0/...` path, WITHOUT the base URL or query string. */
|
|
25
|
+
path: string;
|
|
26
|
+
/** Parsed query parameters (multi-valued: `arg` can repeat). */
|
|
27
|
+
query: URLSearchParams;
|
|
28
|
+
/** Lower-cased header name → value, as fetch delivered them. */
|
|
29
|
+
headers: Record<string, string>;
|
|
30
|
+
/** The raw request body as text (empty string if none). */
|
|
31
|
+
bodyText: string;
|
|
32
|
+
/**
|
|
33
|
+
* The effective request `content-type`. For a `FormData` body this is
|
|
34
|
+
* `multipart/form-data` (real `fetch` sets it, WITH a boundary, when it
|
|
35
|
+
* serialises FormData) even though the caller must NOT hand-set it — the
|
|
36
|
+
* mock derives it here so tests can assert the multipart contract Kubo
|
|
37
|
+
* requires for its file-upload endpoints.
|
|
38
|
+
*/
|
|
39
|
+
contentType?: string;
|
|
40
|
+
/**
|
|
41
|
+
* When the body was a `FormData` (a `multipart/form-data` upload), the file
|
|
42
|
+
* parts it carried. Empty/undefined for non-multipart bodies. Kubo's
|
|
43
|
+
* `add`, `dag/import` and `key/import` require at least one file part under
|
|
44
|
+
* the field name `file`.
|
|
45
|
+
*/
|
|
46
|
+
fileParts?: RecordedFilePart[];
|
|
47
|
+
/** The full URL that was fetched. */
|
|
48
|
+
url: string;
|
|
49
|
+
}
|
|
50
|
+
/** A canned response for a given `/api/v0/<path>` (path without query). */
|
|
51
|
+
export interface MockResponseSpec {
|
|
52
|
+
/** HTTP status to return (default 200). */
|
|
53
|
+
status?: number;
|
|
54
|
+
/** JSON body to return; serialised and sent with content-type application/json. */
|
|
55
|
+
json?: unknown;
|
|
56
|
+
/** Raw text body to return (takes precedence over `json` if both set). */
|
|
57
|
+
text?: string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* A recording mock Kubo API. Install it with {@link fetchImpl} as the `fetch`
|
|
61
|
+
* a {@link KuboRpcClient} uses; inspect {@link requests} afterwards.
|
|
62
|
+
*/
|
|
63
|
+
export declare class MockKuboApi {
|
|
64
|
+
readonly baseUrl: string;
|
|
65
|
+
/** Every request the mock received, in order. */
|
|
66
|
+
readonly requests: RecordedRequest[];
|
|
67
|
+
/** path (no query) → canned response. */
|
|
68
|
+
private readonly responses;
|
|
69
|
+
/** The base URL this mock pretends to be (only its path/query are used). */
|
|
70
|
+
constructor(baseUrl?: string);
|
|
71
|
+
/** Register a canned response for a Kubo `/api/v0/<path>` (path without query). */
|
|
72
|
+
on(path: string, spec: MockResponseSpec): this;
|
|
73
|
+
/** The most recent recorded request, or undefined if none. */
|
|
74
|
+
get lastRequest(): RecordedRequest | undefined;
|
|
75
|
+
/** All recorded requests whose path equals `path`. */
|
|
76
|
+
requestsFor(path: string): RecordedRequest[];
|
|
77
|
+
/**
|
|
78
|
+
* A `fetch`-compatible function that records the request and returns the
|
|
79
|
+
* canned (or a default empty-JSON 200) response. Pass this to the client.
|
|
80
|
+
*/
|
|
81
|
+
readonly fetchImpl: (input: string | URL, init?: {
|
|
82
|
+
method?: string;
|
|
83
|
+
headers?: Record<string, string>;
|
|
84
|
+
body?: unknown;
|
|
85
|
+
}) => Promise<Response>;
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=mock-kubo.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mock-kubo.d.ts","sourceRoot":"","sources":["../../src/rpc/mock-kubo.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,yEAAyE;AACzE,MAAM,WAAW,gBAAgB;IAChC,6EAA6E;IAC7E,KAAK,EAAE,MAAM,CAAC;IACd,wDAAwD;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wBAAwB;IACxB,KAAK,EAAE,UAAU,CAAC;CAClB;AAED,wDAAwD;AACxD,MAAM,WAAW,eAAe;IAC/B,iDAAiD;IACjD,MAAM,EAAE,MAAM,CAAC;IACf,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,KAAK,EAAE,eAAe,CAAC;IACvB,gEAAgE;IAChE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,2DAA2D;IAC3D,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAC/B,qCAAqC;IACrC,GAAG,EAAE,MAAM,CAAC;CACZ;AAED,2EAA2E;AAC3E,MAAM,WAAW,gBAAgB;IAChC,2CAA2C;IAC3C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,mFAAmF;IACnF,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,0EAA0E;IAC1E,IAAI,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;GAGG;AACH,qBAAa,WAAW;IAQX,QAAQ,CAAC,OAAO,EAAE,MAAM;IAPpC,iDAAiD;IACjD,QAAQ,CAAC,QAAQ,EAAE,eAAe,EAAE,CAAM;IAE1C,yCAAyC;IACzC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAuC;IAEjE,4EAA4E;gBACvD,OAAO,GAAE,MAAoC;IAElE,mFAAmF;IACnF,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB,GAAG,IAAI;IAK9C,8DAA8D;IAC9D,IAAI,WAAW,IAAI,eAAe,GAAG,SAAS,CAE7C;IAED,sDAAsD;IACtD,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,EAAE;IAI5C;;;OAGG;IACH,QAAQ,CAAC,SAAS,GACjB,OAAO,MAAM,GAAG,GAAG,EACnB,OAAO;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAC,KACxE,OAAO,CAAC,QAAQ,CAAC,CAgClB;CACF"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A mock Kubo HTTP API for tests: it RECORDS every incoming request (method,
|
|
3
|
+
* path, query, headers, body) and returns canned responses, so a test can
|
|
4
|
+
* assert the exact call shape a `KuboRpcClient` produced WITHOUT a live daemon.
|
|
5
|
+
*
|
|
6
|
+
* This is the primary test boundary for every host-agnostic pinnace operation
|
|
7
|
+
* (deploy, publish, status, site management). It is exposed from the package's
|
|
8
|
+
* test surface so sibling tasks (deploy-multi-target, status-report, ...) reuse
|
|
9
|
+
* it rather than re-inventing a Kubo fake.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* A recording mock Kubo API. Install it with {@link fetchImpl} as the `fetch`
|
|
13
|
+
* a {@link KuboRpcClient} uses; inspect {@link requests} afterwards.
|
|
14
|
+
*/
|
|
15
|
+
export class MockKuboApi {
|
|
16
|
+
baseUrl;
|
|
17
|
+
/** Every request the mock received, in order. */
|
|
18
|
+
requests = [];
|
|
19
|
+
/** path (no query) → canned response. */
|
|
20
|
+
responses = new Map();
|
|
21
|
+
/** The base URL this mock pretends to be (only its path/query are used). */
|
|
22
|
+
constructor(baseUrl = 'https://node.example.test') {
|
|
23
|
+
this.baseUrl = baseUrl;
|
|
24
|
+
}
|
|
25
|
+
/** Register a canned response for a Kubo `/api/v0/<path>` (path without query). */
|
|
26
|
+
on(path, spec) {
|
|
27
|
+
this.responses.set(path, spec);
|
|
28
|
+
return this;
|
|
29
|
+
}
|
|
30
|
+
/** The most recent recorded request, or undefined if none. */
|
|
31
|
+
get lastRequest() {
|
|
32
|
+
return this.requests[this.requests.length - 1];
|
|
33
|
+
}
|
|
34
|
+
/** All recorded requests whose path equals `path`. */
|
|
35
|
+
requestsFor(path) {
|
|
36
|
+
return this.requests.filter((r) => r.path === path);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A `fetch`-compatible function that records the request and returns the
|
|
40
|
+
* canned (or a default empty-JSON 200) response. Pass this to the client.
|
|
41
|
+
*/
|
|
42
|
+
fetchImpl = async (input, init) => {
|
|
43
|
+
const url = new URL(typeof input === 'string' ? input : input.toString());
|
|
44
|
+
const path = url.pathname.replace(/^\/api\/v0\//, '');
|
|
45
|
+
const headers = {};
|
|
46
|
+
for (const [k, v] of Object.entries(init?.headers ?? {})) {
|
|
47
|
+
headers[k.toLowerCase()] = v;
|
|
48
|
+
}
|
|
49
|
+
const { bodyText, contentType, fileParts } = await inspectBody(init?.body, headers);
|
|
50
|
+
this.requests.push({
|
|
51
|
+
method: init?.method ?? 'GET',
|
|
52
|
+
path,
|
|
53
|
+
query: new URLSearchParams(url.search),
|
|
54
|
+
headers,
|
|
55
|
+
bodyText,
|
|
56
|
+
contentType,
|
|
57
|
+
fileParts,
|
|
58
|
+
url: url.toString(),
|
|
59
|
+
});
|
|
60
|
+
const spec = this.responses.get(path);
|
|
61
|
+
const status = spec?.status ?? 200;
|
|
62
|
+
if (spec?.text !== undefined) {
|
|
63
|
+
return new Response(spec.text, { status });
|
|
64
|
+
}
|
|
65
|
+
const payload = spec?.json ?? {};
|
|
66
|
+
return new Response(JSON.stringify(payload), {
|
|
67
|
+
status,
|
|
68
|
+
headers: { 'content-type': 'application/json' },
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Inspect a fetch body the way a real `fetch` would surface it to Kubo:
|
|
74
|
+
* derive the effective `content-type` and, for a `multipart/form-data`
|
|
75
|
+
* (`FormData`) body, extract the file parts. This is what lets the mock
|
|
76
|
+
* ENFORCE Kubo's real upload contract (multipart + a `file` part) instead of
|
|
77
|
+
* merely recording raw bytes — the fidelity gap that let raw octet-stream
|
|
78
|
+
* uploads ship and then fail against a live daemon.
|
|
79
|
+
*/
|
|
80
|
+
async function inspectBody(body, headers) {
|
|
81
|
+
if (body instanceof FormData) {
|
|
82
|
+
// Real `fetch` serialises a FormData as `multipart/form-data; boundary=…`
|
|
83
|
+
// and IGNORES any hand-set content-type. Model that here.
|
|
84
|
+
const fileParts = [];
|
|
85
|
+
const textLines = [];
|
|
86
|
+
// This TS `lib` (dom) types FormData without `entries()`/an iterator, so
|
|
87
|
+
// collect the parts via `forEach` (which IS typed) then process them.
|
|
88
|
+
const entries = [];
|
|
89
|
+
body.forEach((value, field) => {
|
|
90
|
+
entries.push([field, value]);
|
|
91
|
+
});
|
|
92
|
+
for (const [field, value] of entries) {
|
|
93
|
+
if (value instanceof Blob) {
|
|
94
|
+
const bytes = new Uint8Array(await value.arrayBuffer());
|
|
95
|
+
const filename = typeof value.name === 'string'
|
|
96
|
+
? value.name
|
|
97
|
+
: undefined;
|
|
98
|
+
fileParts.push({ field, filename, bytes });
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
textLines.push(`${field}=${String(value)}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
bodyText: textLines.join('&'),
|
|
106
|
+
contentType: 'multipart/form-data',
|
|
107
|
+
fileParts,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
bodyText: await bodyToText(body),
|
|
112
|
+
contentType: headers['content-type'],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/** Best-effort stringify of a non-multipart fetch body for recording. */
|
|
116
|
+
async function bodyToText(body) {
|
|
117
|
+
if (body === undefined || body === null)
|
|
118
|
+
return '';
|
|
119
|
+
if (typeof body === 'string')
|
|
120
|
+
return body;
|
|
121
|
+
if (body instanceof Blob)
|
|
122
|
+
return await body.text();
|
|
123
|
+
if (body instanceof URLSearchParams)
|
|
124
|
+
return body.toString();
|
|
125
|
+
if (body instanceof Uint8Array)
|
|
126
|
+
return Buffer.from(body).toString('utf8');
|
|
127
|
+
return String(body);
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=mock-kubo.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mock-kubo.js","sourceRoot":"","sources":["../../src/rpc/mock-kubo.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-class **site management** — the `site` namespace (spec user stories 4,
|
|
3
|
+
* 15; CONTEXT.md `gateway warming`). Sites are auto-discovered from MFS
|
|
4
|
+
* `/sites/*` (that is how `gateway warming`, IPNS republish, and `status` find
|
|
5
|
+
* them), so managing the sites a node serves = managing those MFS entries + the
|
|
6
|
+
* pins that back them. This module gives the operator explicit verbs for that
|
|
7
|
+
* lifecycle instead of leaving it an implicit side-effect of `deploy`:
|
|
8
|
+
*
|
|
9
|
+
* - **list** — enumerate `/sites/*` with each site's current CID and, when a
|
|
10
|
+
* same-`id` keystore key exists, its IPNS id.
|
|
11
|
+
* - **remove** — `files/rm /sites/<id>` (the entry drops out of
|
|
12
|
+
* warm/republish/status auto-discovery) AND `pin/rm <cid>` so
|
|
13
|
+
* the content stops being served/announced and its storage is
|
|
14
|
+
* reclaimed.
|
|
15
|
+
* - **add** — place an already-imported `/ipfs/<cid>` into MFS
|
|
16
|
+
* `/sites/<id>` (mkdir parents / rm old / cp). See the
|
|
17
|
+
* DESIGN NOTE below on its relationship to `deploy`.
|
|
18
|
+
*
|
|
19
|
+
* Every verb speaks ONLY the Kubo RPC seam (MFS + pin endpoints), so the same
|
|
20
|
+
* core is usable both as a TypeScript API and behind the thin `pinnace site
|
|
21
|
+
* <verb>` CLI (CONTEXT.md `core vs cli`). Tests drive it through the recording
|
|
22
|
+
* `MockKuboApi`, never a live daemon (spec Testing Decisions).
|
|
23
|
+
*
|
|
24
|
+
* DESIGN NOTE (add vs deploy) — recorded per the task's "decide during build".
|
|
25
|
+
* `add` is a DISTINCT verb, not an alias over `deploy`. `deploy` (see
|
|
26
|
+
* `deploy-multi-target`) builds a fresh CAR, imports+pins it on every node, and
|
|
27
|
+
* THEN performs this MFS-placement step. `add` is exactly that final placement
|
|
28
|
+
* step in isolation: it takes an EXISTING CID (already imported/pinned, e.g. a
|
|
29
|
+
* known-good historical CID, or one imported out of band) and makes the node
|
|
30
|
+
* serve it under a site name, WITHOUT building or importing a CAR. Adding a site
|
|
31
|
+
* from an existing CID is a meaningful operation deploy does not cover (no fresh
|
|
32
|
+
* artifact), so it earns its own verb. Deploy is expected to REUSE
|
|
33
|
+
* {@link placeInMfs} for its placement step when it lands, keeping a single
|
|
34
|
+
* implementation of the mkdir/rm/cp sequence rather than forking it. This is
|
|
35
|
+
* NOT a new domain concept: `add` sits at the same layer as the other explicit
|
|
36
|
+
* client verbs and reuses the existing `/sites/<name>` MFS convention.
|
|
37
|
+
*/
|
|
38
|
+
import type { KuboRpcClient } from '../rpc/kubo-rpc-client.js';
|
|
39
|
+
/** The three site-management verbs, under the `site` namespace. */
|
|
40
|
+
export type SiteVerb = 'list' | 'remove' | 'add';
|
|
41
|
+
/** The verbs, in a stable order (for help text / iteration). */
|
|
42
|
+
export declare const SITE_VERBS: readonly SiteVerb[];
|
|
43
|
+
/** One site as listed: its MFS `id`, current CID, and IPNS id if a key exists. */
|
|
44
|
+
export interface SiteListing {
|
|
45
|
+
/** The site's single `id` (its MFS entry under `/sites/`). */
|
|
46
|
+
id: string;
|
|
47
|
+
/** The current content root CID (`files/stat --hash`). */
|
|
48
|
+
cid: string;
|
|
49
|
+
/** The IPNS id, if a same-`id` keystore key exists (ipfs-mode sites lack one). */
|
|
50
|
+
ipns?: string;
|
|
51
|
+
}
|
|
52
|
+
/** Inputs shared by every site-management verb. */
|
|
53
|
+
interface SiteBaseInput {
|
|
54
|
+
/** The Kubo RPC client for the node whose sites are being managed. */
|
|
55
|
+
client: KuboRpcClient;
|
|
56
|
+
/** The MFS directory sites live under (default `/sites`). */
|
|
57
|
+
sitesDir?: string;
|
|
58
|
+
}
|
|
59
|
+
/** Inputs to {@link listSites}. */
|
|
60
|
+
export type ListSitesInput = SiteBaseInput;
|
|
61
|
+
/** Inputs to {@link removeSite}. */
|
|
62
|
+
export interface RemoveSiteInput extends SiteBaseInput {
|
|
63
|
+
/** The site `id` (its MFS entry `/sites/<id>`) to remove. */
|
|
64
|
+
id: string;
|
|
65
|
+
}
|
|
66
|
+
/** The outcome of {@link removeSite}. */
|
|
67
|
+
export interface RemoveSiteResult {
|
|
68
|
+
/** The site `id` that was removed. */
|
|
69
|
+
id: string;
|
|
70
|
+
/** The CID that backed it (undefined if it had no resolvable content). */
|
|
71
|
+
cid?: string;
|
|
72
|
+
/** True when the content was unpinned (storage reclaimed); false if unpin failed. */
|
|
73
|
+
unpinned: boolean;
|
|
74
|
+
}
|
|
75
|
+
/** Inputs to {@link addSite}. */
|
|
76
|
+
export interface AddSiteInput extends SiteBaseInput {
|
|
77
|
+
/** The site `id` to expose it under (its MFS entry `/sites/<id>`). */
|
|
78
|
+
id: string;
|
|
79
|
+
/** The already-imported content root CID to place into MFS. */
|
|
80
|
+
cid: string;
|
|
81
|
+
}
|
|
82
|
+
/** The outcome of {@link addSite}. */
|
|
83
|
+
export interface AddSiteResult {
|
|
84
|
+
/** The site `id` that was placed. */
|
|
85
|
+
id: string;
|
|
86
|
+
/** The CID that now backs it in MFS. */
|
|
87
|
+
cid: string;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* **list** — enumerate the sites the node currently serves (MFS `/sites/*`),
|
|
91
|
+
* annotating each with its current CID (from `files/stat`) and, when a keystore
|
|
92
|
+
* key of the same name exists, its IPNS id (from `key/list -l`). A fresh box
|
|
93
|
+
* with no `/sites` dir yields an empty list, not an error.
|
|
94
|
+
*/
|
|
95
|
+
export declare function listSites(input: ListSitesInput): Promise<SiteListing[]>;
|
|
96
|
+
/**
|
|
97
|
+
* **remove** — delete a site: `files/rm /sites/<name>` FIRST (so it immediately
|
|
98
|
+
* drops out of MFS auto-discovery and stops being served/announced/warmed),
|
|
99
|
+
* THEN `pin/rm <cid>` to unpin the content so its storage is reclaimed. The CID
|
|
100
|
+
* is resolved up-front with `files/stat` so we know what to unpin after the
|
|
101
|
+
* entry is gone.
|
|
102
|
+
*
|
|
103
|
+
* The MFS removal is the load-bearing step and is always attempted. The unpin
|
|
104
|
+
* is best-effort: if the content was never pinned (or is pinned indirectly),
|
|
105
|
+
* `pin/rm` errors, which is REPORTED (`unpinned: false`) rather than thrown, so
|
|
106
|
+
* a partially-pinned site still removes cleanly.
|
|
107
|
+
*/
|
|
108
|
+
export declare function removeSite(input: RemoveSiteInput): Promise<RemoveSiteResult>;
|
|
109
|
+
/**
|
|
110
|
+
* **add** — expose an existing CID as a served site by placing it into MFS at
|
|
111
|
+
* `/sites/<name>` (the discoverable location warm/republish/status read). This
|
|
112
|
+
* is deploy's MFS-placement step in isolation (see the module DESIGN NOTE): it
|
|
113
|
+
* does NOT build or import a CAR and does NOT pin (the CID is assumed already
|
|
114
|
+
* imported/pinned on the node).
|
|
115
|
+
*/
|
|
116
|
+
export declare function addSite(input: AddSiteInput): Promise<AddSiteResult>;
|
|
117
|
+
/**
|
|
118
|
+
* The MFS-placement step: `files/mkdir /sites --parents`, `files/rm
|
|
119
|
+
* /sites/<id> --recursive --force` (clear any prior content), then `files/cp
|
|
120
|
+
* /ipfs/<cid> /sites/<id>`. Ported from the reference prototype's deploy MFS
|
|
121
|
+
* placement (`~/searches/ipfs-hetzner/deploy-car.mjs`). Exported so `deploy`
|
|
122
|
+
* can REUSE this exact sequence rather than forking it.
|
|
123
|
+
*/
|
|
124
|
+
export declare function placeInMfs(client: KuboRpcClient, sitesDir: string, id: string, cid: string): Promise<void>;
|
|
125
|
+
export {};
|
|
126
|
+
//# sourceMappingURL=site-management.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"site-management.d.ts","sourceRoot":"","sources":["../../src/site/site-management.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,2BAA2B,CAAC;AAE7D,mEAAmE;AACnE,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;AAEjD,gEAAgE;AAChE,eAAO,MAAM,UAAU,EAAE,SAAS,QAAQ,EAA8B,CAAC;AAKzE,kFAAkF;AAClF,MAAM,WAAW,WAAW;IAC3B,8DAA8D;IAC9D,EAAE,EAAE,MAAM,CAAC;IACX,0DAA0D;IAC1D,GAAG,EAAE,MAAM,CAAC;IACZ,kFAAkF;IAClF,IAAI,CAAC,EAAE,MAAM,CAAC;CACd;AAED,mDAAmD;AACnD,UAAU,aAAa;IACtB,sEAAsE;IACtE,MAAM,EAAE,aAAa,CAAC;IACtB,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,mCAAmC;AACnC,MAAM,MAAM,cAAc,GAAG,aAAa,CAAC;AAE3C,oCAAoC;AACpC,MAAM,WAAW,eAAgB,SAAQ,aAAa;IACrD,6DAA6D;IAC7D,EAAE,EAAE,MAAM,CAAC;CACX;AAED,yCAAyC;AACzC,MAAM,WAAW,gBAAgB;IAChC,sCAAsC;IACtC,EAAE,EAAE,MAAM,CAAC;IACX,0EAA0E;IAC1E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,qFAAqF;IACrF,QAAQ,EAAE,OAAO,CAAC;CAClB;AAED,iCAAiC;AACjC,MAAM,WAAW,YAAa,SAAQ,aAAa;IAClD,sEAAsE;IACtE,EAAE,EAAE,MAAM,CAAC;IACX,+DAA+D;IAC/D,GAAG,EAAE,MAAM,CAAC;CACZ;AAED,sCAAsC;AACtC,MAAM,WAAW,aAAa;IAC7B,qCAAqC;IACrC,EAAE,EAAE,MAAM,CAAC;IACX,wCAAwC;IACxC,GAAG,EAAE,MAAM,CAAC;CACZ;AAED;;;;;GAKG;AACH,wBAAsB,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAa7E;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,UAAU,CAC/B,KAAK,EAAE,eAAe,GACpB,OAAO,CAAC,gBAAgB,CAAC,CAuB3B;AAED;;;;;;GAMG;AACH,wBAAsB,OAAO,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,aAAa,CAAC,CAIzE;AAED;;;;;;GAMG;AACH,wBAAsB,UAAU,CAC/B,MAAM,EAAE,aAAa,EACrB,QAAQ,EAAE,MAAM,EAChB,EAAE,EAAE,MAAM,EACV,GAAG,EAAE,MAAM,GACT,OAAO,CAAC,IAAI,CAAC,CAIf"}
|