@swarp/cli 0.0.1
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/bin/swarp.mjs +183 -0
- package/package.json +45 -0
- package/proto/swarp/v1/swarp.proto +157 -0
- package/src/certs/generate.mjs +467 -0
- package/src/certs/generate.test.mjs +265 -0
- package/src/config.mjs +11 -0
- package/src/generator/audit.mjs +164 -0
- package/src/generator/generate.mjs +105 -0
- package/src/generator/schema.mjs +105 -0
- package/src/init/index.mjs +189 -0
- package/src/init/index.test.mjs +168 -0
- package/src/init/wizard.mjs +38 -0
- package/src/mcp-server/dispatch.mjs +118 -0
- package/src/mcp-server/index.mjs +259 -0
- package/src/runtimes/fly-sprites.mjs +74 -0
- package/src/runtimes/interface.mjs +20 -0
- package/src/skill/generate.mjs +141 -0
- package/src/templates/agent.yaml +132 -0
- package/src/templates/agent.yaml.example.hbs +137 -0
- package/src/templates/agent.yaml.hbs +40 -0
- package/src/templates/router.yaml.hbs +11 -0
- package/src/templates/workflow.yml +20 -0
- package/src/templates/workflow.yml.hbs +16 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mTLS certificate generation for SWARP.
|
|
3
|
+
*
|
|
4
|
+
* Generates a self-signed CA and signs router + agent leaf certs.
|
|
5
|
+
* Uses node:crypto for key generation and signing — no openssl binary required.
|
|
6
|
+
* X.509 DER encoding is done in pure JavaScript using ASN.1 primitives.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
generateKeyPairSync,
|
|
11
|
+
createSign,
|
|
12
|
+
createPrivateKey,
|
|
13
|
+
createHash,
|
|
14
|
+
randomBytes,
|
|
15
|
+
X509Certificate,
|
|
16
|
+
} from 'node:crypto';
|
|
17
|
+
import { writeFileSync, readFileSync, mkdirSync, existsSync, appendFileSync } from 'node:fs';
|
|
18
|
+
import { join, resolve, basename } from 'node:path';
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// ASN.1 / DER encoder
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
const TAG = {
|
|
25
|
+
BOOLEAN: 0x01,
|
|
26
|
+
INTEGER: 0x02,
|
|
27
|
+
BIT_STRING: 0x03,
|
|
28
|
+
OCTET_STRING: 0x04,
|
|
29
|
+
SEQUENCE: 0x30,
|
|
30
|
+
SET: 0x31,
|
|
31
|
+
UTF8_STRING: 0x0c,
|
|
32
|
+
UTC_TIME: 0x17,
|
|
33
|
+
OID: 0x06,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function derLength(len) {
|
|
37
|
+
if (len < 0x80) return Buffer.from([len]);
|
|
38
|
+
if (len <= 0xff) return Buffer.from([0x81, len]);
|
|
39
|
+
return Buffer.from([0x82, (len >> 8) & 0xff, len & 0xff]);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function tlv(tag, value) {
|
|
43
|
+
const v = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
44
|
+
return Buffer.concat([Buffer.from([tag]), derLength(v.length), v]);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function sequence(...items) {
|
|
48
|
+
return tlv(TAG.SEQUENCE, Buffer.concat(items));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function set_(...items) {
|
|
52
|
+
return tlv(TAG.SET, Buffer.concat(items));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function integerFromBuffer(buf) {
|
|
56
|
+
const b = Buffer.isBuffer(buf) ? buf : Buffer.from(buf);
|
|
57
|
+
const prefixed = b[0] & 0x80 ? Buffer.concat([Buffer.from([0x00]), b]) : b;
|
|
58
|
+
return tlv(TAG.INTEGER, prefixed);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function integerFromNumber(n) {
|
|
62
|
+
let hex = BigInt(n).toString(16);
|
|
63
|
+
if (hex.length % 2 !== 0) hex = '0' + hex;
|
|
64
|
+
return integerFromBuffer(Buffer.from(hex, 'hex'));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function oid(dotted) {
|
|
68
|
+
const parts = dotted.split('.').map(Number);
|
|
69
|
+
const bytes = [40 * parts[0] + parts[1]];
|
|
70
|
+
for (let i = 2; i < parts.length; i++) {
|
|
71
|
+
let n = parts[i];
|
|
72
|
+
const enc = [n & 0x7f];
|
|
73
|
+
n >>= 7;
|
|
74
|
+
while (n > 0) {
|
|
75
|
+
enc.unshift((n & 0x7f) | 0x80);
|
|
76
|
+
n >>= 7;
|
|
77
|
+
}
|
|
78
|
+
bytes.push(...enc);
|
|
79
|
+
}
|
|
80
|
+
return tlv(TAG.OID, Buffer.from(bytes));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function utf8String(s) {
|
|
84
|
+
return tlv(TAG.UTF8_STRING, Buffer.from(s, 'utf8'));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function utcTime(date) {
|
|
88
|
+
const d = new Date(date);
|
|
89
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
90
|
+
const s =
|
|
91
|
+
String(d.getUTCFullYear()).slice(-2) +
|
|
92
|
+
pad(d.getUTCMonth() + 1) +
|
|
93
|
+
pad(d.getUTCDate()) +
|
|
94
|
+
pad(d.getUTCHours()) +
|
|
95
|
+
pad(d.getUTCMinutes()) +
|
|
96
|
+
pad(d.getUTCSeconds()) +
|
|
97
|
+
'Z';
|
|
98
|
+
return tlv(TAG.UTC_TIME, Buffer.from(s, 'ascii'));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function bitString(buf) {
|
|
102
|
+
return tlv(TAG.BIT_STRING, Buffer.concat([Buffer.from([0x00]), buf]));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function boolean_(val) {
|
|
106
|
+
return tlv(TAG.BOOLEAN, Buffer.from([val ? 0xff : 0x00]));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function contextExplicit(n, content) {
|
|
110
|
+
const tag = 0xa0 | n;
|
|
111
|
+
const v = Buffer.isBuffer(content) ? content : Buffer.from(content);
|
|
112
|
+
return Buffer.concat([Buffer.from([tag]), derLength(v.length), v]);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function contextImplicit(n, content) {
|
|
116
|
+
const tag = 0x80 | n;
|
|
117
|
+
const v = Buffer.isBuffer(content) ? content : Buffer.from(content);
|
|
118
|
+
return Buffer.concat([Buffer.from([tag]), derLength(v.length), v]);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function octetString(content) {
|
|
122
|
+
return tlv(TAG.OCTET_STRING, content);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// OID constants
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
const OID = {
|
|
130
|
+
commonName: '2.5.4.3',
|
|
131
|
+
ecdsaWithSHA256: '1.2.840.10045.4.3.2',
|
|
132
|
+
basicConstraints: '2.5.29.19',
|
|
133
|
+
subjectKeyIdentifier: '2.5.29.14',
|
|
134
|
+
authorityKeyIdentifier: '2.5.29.35',
|
|
135
|
+
keyUsage: '2.5.29.15',
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
// Name
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
function rdnSequence(cn) {
|
|
143
|
+
return sequence(
|
|
144
|
+
set_(
|
|
145
|
+
sequence(
|
|
146
|
+
oid(OID.commonName),
|
|
147
|
+
utf8String(cn),
|
|
148
|
+
),
|
|
149
|
+
),
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// Key identifier
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
function computeKeyId(spkiDer) {
|
|
158
|
+
return createHash('sha1').update(spkiDer).digest().slice(0, 20);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// Extensions
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
function buildExtensions({ isCA, subjectSpki, authoritySpki }) {
|
|
166
|
+
const exts = [];
|
|
167
|
+
|
|
168
|
+
if (isCA) {
|
|
169
|
+
const bcValue = sequence(boolean_(true));
|
|
170
|
+
exts.push(
|
|
171
|
+
sequence(
|
|
172
|
+
oid(OID.basicConstraints),
|
|
173
|
+
boolean_(true),
|
|
174
|
+
octetString(bcValue),
|
|
175
|
+
),
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const skid = computeKeyId(subjectSpki);
|
|
180
|
+
exts.push(
|
|
181
|
+
sequence(
|
|
182
|
+
oid(OID.subjectKeyIdentifier),
|
|
183
|
+
octetString(octetString(skid)),
|
|
184
|
+
),
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
if (authoritySpki) {
|
|
188
|
+
const akid = computeKeyId(authoritySpki);
|
|
189
|
+
const akidContent = sequence(contextImplicit(0, akid));
|
|
190
|
+
exts.push(
|
|
191
|
+
sequence(
|
|
192
|
+
oid(OID.authorityKeyIdentifier),
|
|
193
|
+
octetString(akidContent),
|
|
194
|
+
),
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (!isCA) {
|
|
199
|
+
// digitalSignature + keyEncipherment
|
|
200
|
+
exts.push(
|
|
201
|
+
sequence(
|
|
202
|
+
oid(OID.keyUsage),
|
|
203
|
+
boolean_(true),
|
|
204
|
+
octetString(bitString(Buffer.from([0xa0]))),
|
|
205
|
+
),
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return sequence(...exts);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
// TBSCertificate
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* @param {object} opts
|
|
218
|
+
* @param {Buffer} opts.serial
|
|
219
|
+
* @param {string} opts.issuerCN
|
|
220
|
+
* @param {string} opts.subjectCN
|
|
221
|
+
* @param {Date} opts.notBefore
|
|
222
|
+
* @param {Date} opts.notAfter
|
|
223
|
+
* @param {Buffer} opts.subjectSpki - Subject SubjectPublicKeyInfo DER
|
|
224
|
+
* @param {Buffer|null} opts.authoritySpki - Issuer SubjectPublicKeyInfo DER (null for self-signed)
|
|
225
|
+
* @param {boolean} opts.isCA
|
|
226
|
+
*/
|
|
227
|
+
function buildTbs(opts) {
|
|
228
|
+
const { serial, issuerCN, subjectCN, notBefore, notAfter, subjectSpki, authoritySpki, isCA } = opts;
|
|
229
|
+
|
|
230
|
+
const version = contextExplicit(0, integerFromNumber(2));
|
|
231
|
+
const exts = buildExtensions({ isCA, subjectSpki, authoritySpki });
|
|
232
|
+
const extsWrapped = contextExplicit(3, exts);
|
|
233
|
+
|
|
234
|
+
return sequence(
|
|
235
|
+
version,
|
|
236
|
+
integerFromBuffer(serial),
|
|
237
|
+
sequence(oid(OID.ecdsaWithSHA256)),
|
|
238
|
+
rdnSequence(issuerCN),
|
|
239
|
+
sequence(utcTime(notBefore), utcTime(notAfter)),
|
|
240
|
+
rdnSequence(subjectCN),
|
|
241
|
+
subjectSpki,
|
|
242
|
+
extsWrapped,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
// Sign + wrap
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
|
|
250
|
+
function signTbs(tbsDer, privateKeyPem) {
|
|
251
|
+
const key = createPrivateKey({ key: privateKeyPem, format: 'pem', type: 'pkcs8' });
|
|
252
|
+
const signer = createSign('SHA256');
|
|
253
|
+
signer.update(tbsDer);
|
|
254
|
+
const sigDer = signer.sign(key);
|
|
255
|
+
|
|
256
|
+
return sequence(
|
|
257
|
+
tbsDer,
|
|
258
|
+
sequence(oid(OID.ecdsaWithSHA256)),
|
|
259
|
+
bitString(sigDer),
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
// PEM helpers
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
|
|
267
|
+
function toPem(label, der) {
|
|
268
|
+
const b64 = (Buffer.isBuffer(der) ? der : Buffer.from(der)).toString('base64');
|
|
269
|
+
const lines = b64.match(/.{1,64}/g).join('\n');
|
|
270
|
+
return `-----BEGIN ${label}-----\n${lines}\n-----END ${label}-----\n`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
// Key generation
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
const VALIDITY_DAYS = 365 * 3;
|
|
278
|
+
|
|
279
|
+
function daysFromNow(days) {
|
|
280
|
+
return new Date(Date.now() + days * 24 * 60 * 60 * 1000);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function genKeyPair() {
|
|
284
|
+
const { privateKey: privatePem, publicKey: spkiDer } = generateKeyPairSync('ec', {
|
|
285
|
+
namedCurve: 'P-256',
|
|
286
|
+
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
|
|
287
|
+
publicKeyEncoding: { type: 'spki', format: 'der' },
|
|
288
|
+
});
|
|
289
|
+
return { privatePem, spkiDer: Buffer.from(spkiDer) };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function randomSerialBytes() {
|
|
293
|
+
const b = randomBytes(8);
|
|
294
|
+
b[0] &= 0x7f;
|
|
295
|
+
return b;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
// Certificate builders
|
|
300
|
+
// ---------------------------------------------------------------------------
|
|
301
|
+
|
|
302
|
+
function buildCaCert(caKey) {
|
|
303
|
+
const now = new Date();
|
|
304
|
+
const tbs = buildTbs({
|
|
305
|
+
serial: randomSerialBytes(),
|
|
306
|
+
issuerCN: 'swarp-ca',
|
|
307
|
+
subjectCN: 'swarp-ca',
|
|
308
|
+
notBefore: now,
|
|
309
|
+
notAfter: daysFromNow(VALIDITY_DAYS),
|
|
310
|
+
subjectSpki: caKey.spkiDer,
|
|
311
|
+
authoritySpki: null,
|
|
312
|
+
isCA: true,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const certDer = signTbs(tbs, caKey.privatePem);
|
|
316
|
+
return toPem('CERTIFICATE', certDer);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function buildLeafCert(cn, leafKey, caOpts) {
|
|
320
|
+
const { caCertPem, caKeyPem } = caOpts;
|
|
321
|
+
|
|
322
|
+
const caCert = new X509Certificate(caCertPem);
|
|
323
|
+
const caSpkiDer = Buffer.from(caCert.publicKey.export({ type: 'spki', format: 'der' }));
|
|
324
|
+
|
|
325
|
+
const now = new Date();
|
|
326
|
+
const tbs = buildTbs({
|
|
327
|
+
serial: randomSerialBytes(),
|
|
328
|
+
issuerCN: 'swarp-ca',
|
|
329
|
+
subjectCN: cn,
|
|
330
|
+
notBefore: now,
|
|
331
|
+
notAfter: daysFromNow(VALIDITY_DAYS),
|
|
332
|
+
subjectSpki: leafKey.spkiDer,
|
|
333
|
+
authoritySpki: caSpkiDer,
|
|
334
|
+
isCA: false,
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const certDer = signTbs(tbs, caKeyPem);
|
|
338
|
+
return toPem('CERTIFICATE', certDer);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ---------------------------------------------------------------------------
|
|
342
|
+
// .gitignore helper
|
|
343
|
+
// ---------------------------------------------------------------------------
|
|
344
|
+
|
|
345
|
+
function ensureGitignoreEntry(outDir, fsImpl) {
|
|
346
|
+
const gitignorePath = resolve('.gitignore');
|
|
347
|
+
// Normalise to a simple dirname token (e.g. 'certs') so absolute test paths
|
|
348
|
+
// don't end up written verbatim into .gitignore.
|
|
349
|
+
const dirName = basename(resolve(outDir));
|
|
350
|
+
const entry = `${dirName}/`;
|
|
351
|
+
|
|
352
|
+
if (!fsImpl.existsSync(gitignorePath)) return;
|
|
353
|
+
|
|
354
|
+
const existing = fsImpl.readFileSync(gitignorePath, 'utf8');
|
|
355
|
+
if (existing.includes(entry)) return;
|
|
356
|
+
|
|
357
|
+
fsImpl.appendFileSync(
|
|
358
|
+
gitignorePath,
|
|
359
|
+
`\n# SWARP mTLS certificates (never commit private keys)\n${entry}\n`,
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ---------------------------------------------------------------------------
|
|
364
|
+
// Exported API
|
|
365
|
+
// ---------------------------------------------------------------------------
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Generates a full mTLS certificate bundle and writes it to `outDir`.
|
|
369
|
+
*
|
|
370
|
+
* Output files:
|
|
371
|
+
* ca.crt — CA certificate (public; safe to commit)
|
|
372
|
+
* ca.key — CA private key (put in GitHub Secrets; never commit)
|
|
373
|
+
* router.crt/.key — Router mTLS keypair
|
|
374
|
+
* agent-example.crt/.key — Sample agent mTLS keypair
|
|
375
|
+
*
|
|
376
|
+
* Adds `outDir/` to .gitignore if the file exists and the entry is absent.
|
|
377
|
+
*
|
|
378
|
+
* @param {string} [outDir='certs'] - Output directory (created if missing)
|
|
379
|
+
* @param {object} [opts]
|
|
380
|
+
* @param {object} [opts.fs] - Filesystem shims for testing
|
|
381
|
+
*/
|
|
382
|
+
export async function generateCerts(outDir = 'certs', { fs: fsOverride } = {}) {
|
|
383
|
+
const fs = {
|
|
384
|
+
writeFileSync,
|
|
385
|
+
mkdirSync,
|
|
386
|
+
existsSync,
|
|
387
|
+
appendFileSync,
|
|
388
|
+
readFileSync,
|
|
389
|
+
...fsOverride,
|
|
390
|
+
};
|
|
391
|
+
const out = resolve(outDir);
|
|
392
|
+
|
|
393
|
+
if (!fs.existsSync(out)) {
|
|
394
|
+
fs.mkdirSync(out, { recursive: true });
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const caKey = genKeyPair();
|
|
398
|
+
const caCertPem = buildCaCert(caKey);
|
|
399
|
+
|
|
400
|
+
fs.writeFileSync(join(out, 'ca.crt'), caCertPem, 'utf8');
|
|
401
|
+
fs.writeFileSync(join(out, 'ca.key'), caKey.privatePem, 'utf8');
|
|
402
|
+
|
|
403
|
+
const caOpts = { caCertPem, caKeyPem: caKey.privatePem };
|
|
404
|
+
|
|
405
|
+
const routerKey = genKeyPair();
|
|
406
|
+
const routerCertPem = buildLeafCert('swarp-router', routerKey, caOpts);
|
|
407
|
+
fs.writeFileSync(join(out, 'router.crt'), routerCertPem, 'utf8');
|
|
408
|
+
fs.writeFileSync(join(out, 'router.key'), routerKey.privatePem, 'utf8');
|
|
409
|
+
|
|
410
|
+
const agentKey = genKeyPair();
|
|
411
|
+
const agentCertPem = buildLeafCert('example', agentKey, caOpts);
|
|
412
|
+
fs.writeFileSync(join(out, 'agent-example.crt'), agentCertPem, 'utf8');
|
|
413
|
+
fs.writeFileSync(join(out, 'agent-example.key'), agentKey.privatePem, 'utf8');
|
|
414
|
+
|
|
415
|
+
ensureGitignoreEntry(outDir, fs);
|
|
416
|
+
|
|
417
|
+
console.log(`\nCertificates written to ${out}/`);
|
|
418
|
+
console.log(' ca.crt CA certificate (safe to commit)');
|
|
419
|
+
console.log(' ca.key CA private key');
|
|
420
|
+
console.log(' router.crt / router.key Router mTLS keypair');
|
|
421
|
+
console.log(' agent-example.crt / .key Sample agent mTLS keypair');
|
|
422
|
+
console.log('\n Put SWARP_MTLS_CA_KEY in GitHub Secrets. Never commit it.\n');
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Rotates the certificate for a named agent using the existing CA.
|
|
427
|
+
* Reads ca.crt and ca.key from `outDir`, writes new agent-<name>.crt/.key.
|
|
428
|
+
*
|
|
429
|
+
* @param {string} name - Agent name used as CN and filename prefix
|
|
430
|
+
* @param {string} [outDir='certs'] - Directory containing ca.crt and ca.key
|
|
431
|
+
* @param {object} [opts]
|
|
432
|
+
* @param {object} [opts.fs] - Filesystem shims for testing
|
|
433
|
+
*/
|
|
434
|
+
export async function rotateCert(name, outDir = 'certs', { fs: fsOverride } = {}) {
|
|
435
|
+
const fs = {
|
|
436
|
+
writeFileSync,
|
|
437
|
+
mkdirSync,
|
|
438
|
+
existsSync,
|
|
439
|
+
appendFileSync,
|
|
440
|
+
readFileSync,
|
|
441
|
+
...fsOverride,
|
|
442
|
+
};
|
|
443
|
+
const out = resolve(outDir);
|
|
444
|
+
|
|
445
|
+
const caKeyPath = join(out, 'ca.key');
|
|
446
|
+
const caCrtPath = join(out, 'ca.crt');
|
|
447
|
+
|
|
448
|
+
if (!fs.existsSync(caKeyPath) || !fs.existsSync(caCrtPath)) {
|
|
449
|
+
throw new Error(
|
|
450
|
+
`CA files not found in ${out}/. Run 'swarp certs generate' first.`,
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const caKeyPem = fs.readFileSync(caKeyPath, 'utf8');
|
|
455
|
+
const caCertPem = fs.readFileSync(caCrtPath, 'utf8');
|
|
456
|
+
|
|
457
|
+
const leafKey = genKeyPair();
|
|
458
|
+
const leafCertPem = buildLeafCert(name, leafKey, { caCertPem, caKeyPem });
|
|
459
|
+
|
|
460
|
+
fs.writeFileSync(join(out, `agent-${name}.crt`), leafCertPem, 'utf8');
|
|
461
|
+
fs.writeFileSync(join(out, `agent-${name}.key`), leafKey.privatePem, 'utf8');
|
|
462
|
+
|
|
463
|
+
console.log(`\nRotated certificate for agent '${name}':`);
|
|
464
|
+
console.log(` agent-${name}.crt`);
|
|
465
|
+
console.log(` agent-${name}.key`);
|
|
466
|
+
console.log(`\n Update SWARP_AGENT_${name.toUpperCase()}_CERT in GitHub Secrets.\n`);
|
|
467
|
+
}
|