@camstack/system 1.2.437 → 1.2.439
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/dist/addon-runner.js +64 -12
- package/dist/addon-runner.mjs +65 -13
- package/dist/{agent-https-BZIwS4xF.js → agent-https-DlpTMDqA.js} +252 -22
- package/dist/{agent-https-BPfHrI4c.mjs → agent-https-U0d4uvn2.mjs} +211 -23
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +7 -1
- package/dist/builtins/alerts/alerts.addon.mjs +7 -1
- package/dist/builtins/autotrack/index.js +1 -1
- package/dist/builtins/autotrack/index.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2 -2
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
- package/dist/builtins/calendars/calendars.addon.js +1 -1
- package/dist/builtins/calendars/calendars.addon.mjs +1 -1
- package/dist/builtins/camera-grid/index.js +9 -1
- package/dist/builtins/camera-grid/index.mjs +9 -1
- package/dist/builtins/composer/composer.addon.js +2 -2
- package/dist/builtins/composer/composer.addon.mjs +2 -2
- package/dist/builtins/console-logging/index.js +12 -5
- package/dist/builtins/console-logging/index.mjs +12 -5
- package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
- package/dist/builtins/device-manager/device-manager.addon.d.ts +5 -0
- package/dist/builtins/device-manager/device-manager.addon.js +717 -28
- package/dist/builtins/device-manager/device-manager.addon.mjs +717 -28
- package/dist/builtins/device-manager/device-meta-types.d.ts +4 -0
- package/dist/builtins/device-manager/device-provider-context.d.ts +9 -0
- package/dist/builtins/device-manager/device-removal-hooks.d.ts +103 -0
- package/dist/builtins/device-manager/device-row-store.d.ts +1 -0
- package/dist/builtins/device-manager/device-tombstone-store.d.ts +68 -0
- package/dist/builtins/device-manager/removal-cleanup.d.ts +50 -0
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/keychain/index.js +37 -4
- package/dist/builtins/keychain/index.mjs +37 -4
- package/dist/builtins/keychain/keychain-service.d.ts +14 -0
- package/dist/builtins/keychain/keychain-store.d.ts +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +12 -12
- package/dist/builtins/local-auth/local-auth.addon.mjs +12 -12
- package/dist/builtins/local-network/local-network.addon.js +16 -2
- package/dist/builtins/local-network/local-network.addon.mjs +16 -2
- package/dist/builtins/loki-logging/index.js +28 -5
- package/dist/builtins/loki-logging/index.mjs +28 -5
- package/dist/builtins/native-metrics/native-metrics.addon.js +11 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +11 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +11 -1
- package/dist/builtins/snapshot/index.mjs +11 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +4 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +4 -1
- package/dist/builtins/sqlite-storage/index.js +1 -1
- package/dist/builtins/sqlite-storage/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +3 -3
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +3 -3
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
- package/dist/builtins/system-config/system-config.addon.js +22 -4
- package/dist/builtins/system-config/system-config.addon.mjs +22 -4
- package/dist/builtins/winston-logging/index.js +14 -5
- package/dist/builtins/winston-logging/index.mjs +14 -5
- package/dist/{composition-sources-BIT8IUKm.js → composition-sources-B4jC2FC7.js} +1 -1
- package/dist/{composition-sources-tpYR0f1x.mjs → composition-sources-BGOSCGWa.mjs} +1 -1
- package/dist/{constants-Bc6trhoW.mjs → constants-B92BjObl.mjs} +1 -1
- package/dist/{constants-CwRY32Nx.js → constants-BRsjQwi-.js} +1 -1
- package/dist/{device-ca-BhdBByGD.mjs → device-ca-46BUsWD1.mjs} +2 -2
- package/dist/{device-ca-DImD8IzR.js → device-ca-DHG9OW2m.js} +2 -2
- package/dist/{dist-CEAyXW3Q.js → dist-BOWdLjoK.js} +341 -13
- package/dist/{dist-m5JkT8UN.mjs → dist-yI6kF6r6.mjs} +306 -14
- package/dist/file-host-worker.js +1 -1
- package/dist/file-host-worker.mjs +1 -1
- package/dist/{hub-mdns-ciao-CBLvq2oj.js → hub-mdns-ciao-D7S5AJRU.js} +377 -69
- package/dist/{hub-mdns-ciao-DMlQTdPU.mjs → hub-mdns-ciao-DGwVc2uh.mjs} +344 -72
- package/dist/i18n/catalog-registry.d.ts +35 -0
- package/dist/i18n/index.d.ts +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +698 -22
- package/dist/index.mjs +681 -24
- package/dist/kernel/gc-census.d.ts +18 -1
- package/dist/kernel/settings-view-types.d.ts +2 -0
- package/dist/kernel/transport/local-transport.d.ts +3 -0
- package/dist/kernel/transport/peer-answered-error.d.ts +5 -1
- package/dist/{lib-NNxHSOIE.js → lib-8oZrELLm.js} +1 -1
- package/dist/{lib-BeGzVQcd.mjs → lib-DK38ompL.mjs} +1 -1
- package/dist/{restore-archive-DNxAStrJ.mjs → restore-archive-CpdUO-uS.mjs} +1 -1
- package/dist/{restore-archive-C75srVbG.js → restore-archive-DiGiI6Jp.js} +1 -1
- package/dist/{retired-settings-keys-DNfV28R_.js → retired-settings-keys-CyQ7uvta.js} +1 -1
- package/dist/{retired-settings-keys-C5qP9QIf.mjs → retired-settings-keys-UsVtLOT0.mjs} +1 -1
- package/dist/tls/agent-https.d.ts +2 -2
- package/dist/tls/boot-tls.d.ts +6 -0
- package/dist/tls/cert-evaluation.d.ts +1 -1
- package/dist/tls/cert-issue.d.ts +5 -1
- package/dist/tls/cert-manager.d.ts +18 -0
- package/dist/tls/cert-name-constraints.d.ts +53 -0
- package/dist/tls/index.d.ts +5 -4
- package/dist/tls/tls-actions.d.ts +27 -7
- package/dist/tls/tls-status.d.ts +22 -0
- package/dist/tls/uploaded-cert.d.ts +36 -4
- package/dist/{wal-maintenance-Ch79gy9S.js → wal-maintenance-DEL_jeG0.js} +0 -0
- package/dist/{wal-maintenance-DvnVO5IK.mjs → wal-maintenance-xsvdT5Go.mjs} +0 -0
- package/dist/{x509.es-r7Ht1thA.js → x509.es-C4cOo48M.js} +36 -0
- package/dist/{x509.es-CDvj67p1.mjs → x509.es-DK__ETIe.mjs} +1 -1
- package/package.json +3 -1
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { o as __toESM } from "./chunk-CNf5ZN-e.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { vi as userError } from "./dist-yI6kF6r6.mjs";
|
|
3
|
+
import { f as X509Certificate$1 } from "./x509.es-DK__ETIe.mjs";
|
|
4
|
+
import { A as isConstrainedCa, E as reasonRequiresNewCa, M as permitsIdentity, P as readNameConstraints, T as evaluateExistingCert, _ as issueLeaf, g as generateCa, v as loadCa, y as collectCertIdentity } from "./agent-https-U0d4uvn2.mjs";
|
|
3
5
|
import { X509Certificate, createPrivateKey } from "node:crypto";
|
|
4
6
|
import { writeOwnerOnlyFileSync } from "@camstack/types/node";
|
|
5
7
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
6
|
-
import { join } from "node:path";
|
|
7
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { rootCertificates } from "node:tls";
|
|
8
11
|
import { createServer } from "node:http";
|
|
9
12
|
//#region src/http/lan-http-bind.ts
|
|
10
13
|
/**
|
|
@@ -188,24 +191,26 @@ async function ensureTlsCert(dataDir, options) {
|
|
|
188
191
|
};
|
|
189
192
|
const commonName = options?.commonName ?? DEFAULT_COMMON_NAME;
|
|
190
193
|
const validDays = Math.min(options?.validDays ?? 397, 397);
|
|
191
|
-
const
|
|
194
|
+
const rawIdentity = collectCertIdentity({
|
|
192
195
|
commonName,
|
|
193
196
|
extraSans: options?.extraSans ?? [],
|
|
194
197
|
...options?.hostFacts === void 0 ? {} : { hostFacts: options.hostFacts }
|
|
195
198
|
});
|
|
196
|
-
const
|
|
197
|
-
|
|
199
|
+
const onDisk = constrainToDiskCa(paths, rawIdentity);
|
|
200
|
+
const reason = decideRegeneration(paths, onDisk.identity);
|
|
201
|
+
if (reason === null) return describe(paths, false, null, false, null, null, onDisk.dropped);
|
|
198
202
|
const previousFingerprint = readFingerprint(paths.certPath);
|
|
199
203
|
const caRotated = reasonRequiresNewCa(reason);
|
|
200
204
|
mkdirSync(tlsDir, { recursive: true });
|
|
205
|
+
let dropped = [];
|
|
201
206
|
try {
|
|
202
|
-
await regenerate(paths,
|
|
207
|
+
dropped = await regenerate(paths, rawIdentity, commonName, validDays, caRotated, reason);
|
|
203
208
|
} catch (err) {
|
|
204
209
|
const message = err instanceof Error ? err.message : String(err);
|
|
205
210
|
if (!canKeepServing(paths)) throw err;
|
|
206
|
-
return describe(paths, false, null, false, null, message);
|
|
211
|
+
return describe(paths, false, null, false, null, message, onDisk.dropped);
|
|
207
212
|
}
|
|
208
|
-
return describe(paths, true, reason, caRotated, previousFingerprint, null);
|
|
213
|
+
return describe(paths, true, reason, caRotated, previousFingerprint, null, dropped);
|
|
209
214
|
}
|
|
210
215
|
/**
|
|
211
216
|
* Operator "Regenerate": issue a new leaf under the existing local CA.
|
|
@@ -229,8 +234,39 @@ async function reissueTlsLeaf(dataDir, options) {
|
|
|
229
234
|
});
|
|
230
235
|
const previousFingerprint = readFingerprint(paths.certPath);
|
|
231
236
|
mkdirSync(tlsDir, { recursive: true });
|
|
232
|
-
await regenerate(paths, identity, commonName, validDays, false);
|
|
233
|
-
|
|
237
|
+
return describe(paths, true, "san-coverage-gap", false, previousFingerprint, null, await regenerate(paths, identity, commonName, validDays, false, null));
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Uploaded mode keeps a generated CA beside the upload (agents' leaves are still signed
|
|
241
|
+
* by it). When that CA is unconstrained, replace it alone (D873): the served pair is the
|
|
242
|
+
* operator's and is never touched. Returns whether the CA was replaced.
|
|
243
|
+
*/
|
|
244
|
+
async function replaceUnconstrainedLocalCa(dataDir, options) {
|
|
245
|
+
const tlsDir = join(dataDir, "tls");
|
|
246
|
+
const paths = {
|
|
247
|
+
certPath: join(tlsDir, "camstack.crt"),
|
|
248
|
+
keyPath: join(tlsDir, "camstack.key"),
|
|
249
|
+
caCertPath: join(tlsDir, "camstack-ca.crt"),
|
|
250
|
+
caKeyPath: join(tlsDir, "camstack-ca.key")
|
|
251
|
+
};
|
|
252
|
+
if (!existsSync(paths.caCertPath) || !existsSync(paths.caKeyPath)) return false;
|
|
253
|
+
try {
|
|
254
|
+
if (isConstrainedCa(new X509Certificate$1(readFileSync(paths.caCertPath, "utf-8")))) return false;
|
|
255
|
+
} catch {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
const identity = collectCertIdentity({
|
|
259
|
+
commonName: options?.commonName ?? DEFAULT_COMMON_NAME,
|
|
260
|
+
extraSans: options?.extraSans ?? [],
|
|
261
|
+
...options?.hostFacts === void 0 ? {} : { hostFacts: options.hostFacts }
|
|
262
|
+
});
|
|
263
|
+
const ca = await generateCa(/* @__PURE__ */ new Date(), identity.dnsNames);
|
|
264
|
+
archivePreviousCa(paths, ca);
|
|
265
|
+
await writeFile(`${paths.caCertPath}.new`, ca.certPem, "utf-8");
|
|
266
|
+
writeOwnerOnlyFileSync(`${paths.caKeyPath}.new`, ca.keyPem);
|
|
267
|
+
renameSync(`${paths.caCertPath}.new`, paths.caCertPath);
|
|
268
|
+
renameSync(`${paths.caKeyPath}.new`, paths.caKeyPath);
|
|
269
|
+
return true;
|
|
234
270
|
}
|
|
235
271
|
/** True when the material already on disk can still terminate TLS. */
|
|
236
272
|
function canKeepServing(paths) {
|
|
@@ -267,12 +303,18 @@ function decideRegeneration(paths, identity) {
|
|
|
267
303
|
* moves leaves a key/certificate mismatch, which the evaluator reports as
|
|
268
304
|
* `key-mismatch` and heals on the next boot.
|
|
269
305
|
*/
|
|
270
|
-
async function regenerate(paths,
|
|
306
|
+
async function regenerate(paths, rawIdentity, commonName, validDays, newCa, reason) {
|
|
271
307
|
const now = /* @__PURE__ */ new Date();
|
|
272
|
-
const ca = newCa ? await generateCa(now) : await loadCa(readFileSync(paths.caCertPath, "utf-8"), readFileSync(paths.caKeyPath, "utf-8"));
|
|
308
|
+
const ca = newCa ? await generateCa(now, rawIdentity.dnsNames) : await loadCa(readFileSync(paths.caCertPath, "utf-8"), readFileSync(paths.caKeyPath, "utf-8"));
|
|
309
|
+
const constraints = readNameConstraints(ca.cert);
|
|
310
|
+
const { identity, dropped } = constraints === null ? {
|
|
311
|
+
identity: rawIdentity,
|
|
312
|
+
dropped: []
|
|
313
|
+
} : permitsIdentity(constraints, rawIdentity);
|
|
273
314
|
const leaf = await issueLeaf(ca, identity, commonName, validDays, now);
|
|
274
315
|
const chainPem = `${leaf.certPem.trimEnd()}\n${ca.certPem.trimEnd()}\n`;
|
|
275
316
|
if (newCa) {
|
|
317
|
+
if (reason === "ca-unconstrained") archivePreviousCa(paths, ca);
|
|
276
318
|
await writeFile(`${paths.caCertPath}.new`, ca.certPem, "utf-8");
|
|
277
319
|
writeOwnerOnlyFileSync(`${paths.caKeyPath}.new`, ca.keyPem);
|
|
278
320
|
renameSync(`${paths.caCertPath}.new`, paths.caCertPath);
|
|
@@ -282,8 +324,56 @@ async function regenerate(paths, identity, commonName, validDays, newCa) {
|
|
|
282
324
|
await writeFile(`${paths.certPath}.new`, chainPem, "utf-8");
|
|
283
325
|
renameSync(`${paths.keyPath}.new`, paths.keyPath);
|
|
284
326
|
renameSync(`${paths.certPath}.new`, paths.certPath);
|
|
327
|
+
return dropped;
|
|
285
328
|
}
|
|
286
|
-
|
|
329
|
+
/** The identity as far as the CA on disk may certify it; unchanged while that CA is absent or unconstrained. */
|
|
330
|
+
function constrainToDiskCa(paths, identity) {
|
|
331
|
+
try {
|
|
332
|
+
const constraints = readNameConstraints(new X509Certificate$1(readFileSync(paths.caCertPath, "utf-8")));
|
|
333
|
+
return constraints === null ? {
|
|
334
|
+
identity,
|
|
335
|
+
dropped: []
|
|
336
|
+
} : permitsIdentity(constraints, identity);
|
|
337
|
+
} catch {
|
|
338
|
+
return {
|
|
339
|
+
identity,
|
|
340
|
+
dropped: []
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* One-time replacement of an UNCONSTRAINED generated CA (D873): keep the old material
|
|
346
|
+
* beside the new one until the new leaf is serving (`retirePreviousLocalCa`), and write the
|
|
347
|
+
* marker the admin UI reads to tell the operator that clients must re-trust the CA.
|
|
348
|
+
*/
|
|
349
|
+
function archivePreviousCa(paths, replacement) {
|
|
350
|
+
if (!existsSync(paths.caCertPath)) return;
|
|
351
|
+
const previousCert = `${paths.caCertPath}.previous`;
|
|
352
|
+
const previousKey = `${paths.caKeyPath}.previous`;
|
|
353
|
+
copyFileSync(paths.caCertPath, previousCert);
|
|
354
|
+
if (existsSync(paths.caKeyPath)) copyFileSync(paths.caKeyPath, previousKey);
|
|
355
|
+
const previousFingerprint = readFingerprint(paths.caCertPath);
|
|
356
|
+
writeOwnerOnlyFileSync(join(dirname(paths.caCertPath), "ca-replaced.json"), `${JSON.stringify({
|
|
357
|
+
replacedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
358
|
+
previousFingerprintSha256: previousFingerprint,
|
|
359
|
+
fingerprintSha256: new X509Certificate(replacement.certPem).fingerprint256
|
|
360
|
+
})}\n`);
|
|
361
|
+
}
|
|
362
|
+
/** Marker path for the status reader and the retire step. */
|
|
363
|
+
function caReplacementMarkerFile(dataDir) {
|
|
364
|
+
return join(dataDir, "tls", "ca-replaced.json");
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Called once the HTTPS listener serves the leaf of the replacement CA: drop the retired
|
|
368
|
+
* unconstrained CA, key first. Idempotent; returns whether anything was removed.
|
|
369
|
+
*/
|
|
370
|
+
function retirePreviousLocalCa(dataDir) {
|
|
371
|
+
const base = join(dataDir, "tls", "camstack-ca");
|
|
372
|
+
const present = [`${base}.key.previous`, `${base}.crt.previous`].filter((file) => existsSync(file));
|
|
373
|
+
for (const file of present) rmSync(file, { force: true });
|
|
374
|
+
return present.length > 0;
|
|
375
|
+
}
|
|
376
|
+
function describe(paths, generated, reason, caRotated, previousFingerprintSha256, reissueError, droppedNames) {
|
|
287
377
|
const leaf = new X509Certificate(readFileSync(paths.certPath));
|
|
288
378
|
const ca = new X509Certificate(readFileSync(paths.caCertPath));
|
|
289
379
|
const san = leaf.subjectAltName ?? "";
|
|
@@ -297,6 +387,7 @@ function describe(paths, generated, reason, caRotated, previousFingerprintSha256
|
|
|
297
387
|
caFingerprintSha256: ca.fingerprint256,
|
|
298
388
|
validTo: new Date(leaf.validTo).toISOString(),
|
|
299
389
|
sans: san === "" ? [] : san.split(", "),
|
|
390
|
+
droppedNames,
|
|
300
391
|
reissueError
|
|
301
392
|
};
|
|
302
393
|
}
|
|
@@ -312,6 +403,140 @@ function loadTlsCert(certPath, keyPath) {
|
|
|
312
403
|
};
|
|
313
404
|
}
|
|
314
405
|
//#endregion
|
|
406
|
+
//#region src/tls/uploaded-cert.ts
|
|
407
|
+
var MESSAGES = {
|
|
408
|
+
"tls.cert-unparsable": "Certificate PEM could not be parsed.",
|
|
409
|
+
"tls.key-unparsable": "Private key PEM could not be parsed.",
|
|
410
|
+
"tls.key-mismatch": "Private key does not match the certificate.",
|
|
411
|
+
"tls.missing-server-auth": "Certificate is missing extendedKeyUsage = serverAuth.",
|
|
412
|
+
"tls.ca-required": "This certificate was not issued by a public certificate authority, so its CA certificate is required: devices need it to trust the hub. Paste the CA certificate (for mkcert, the rootCA.pem in the folder `mkcert -CAROOT` prints).",
|
|
413
|
+
"tls.ca-unparsable": "The CA certificate could not be parsed.",
|
|
414
|
+
"tls.ca-not-issuer": "The certificate was not issued by the CA certificate you supplied.",
|
|
415
|
+
"tls.ca-not-a-ca": "The supplied CA certificate is not a certificate authority (CA:TRUE is missing).",
|
|
416
|
+
"tls.ca-expired": "The supplied CA certificate has expired or is not valid yet."
|
|
417
|
+
};
|
|
418
|
+
function failure(code) {
|
|
419
|
+
return {
|
|
420
|
+
ok: false,
|
|
421
|
+
code,
|
|
422
|
+
error: MESSAGES[code]
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Operator-supplied material must parse, match its key, and carry
|
|
427
|
+
* `extendedKeyUsage = serverAuth` — the same leaf defects D227 refused
|
|
428
|
+
* to keep generating.
|
|
429
|
+
*/
|
|
430
|
+
function validateUploadedTls(certPem, keyPem) {
|
|
431
|
+
let leaf;
|
|
432
|
+
try {
|
|
433
|
+
leaf = new X509Certificate(certPem);
|
|
434
|
+
} catch {
|
|
435
|
+
return failure("tls.cert-unparsable");
|
|
436
|
+
}
|
|
437
|
+
try {
|
|
438
|
+
if (!leaf.checkPrivateKey(createPrivateKey(keyPem))) return failure("tls.key-mismatch");
|
|
439
|
+
} catch {
|
|
440
|
+
return failure("tls.key-unparsable");
|
|
441
|
+
}
|
|
442
|
+
const eku = leaf.keyUsage;
|
|
443
|
+
if (eku === void 0 || !eku.includes("1.3.6.1.5.5.7.3.1")) return failure("tls.missing-server-auth");
|
|
444
|
+
return {
|
|
445
|
+
ok: true,
|
|
446
|
+
fingerprintSha256: leaf.fingerprint256,
|
|
447
|
+
validTo: new Date(leaf.validTo).toISOString()
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
/** Every certificate in a PEM bundle, in order. */
|
|
451
|
+
function pemCertificates(pem) {
|
|
452
|
+
const blocks = pem.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g);
|
|
453
|
+
return blocks === null ? [] : blocks.map((block) => `${block}\n`);
|
|
454
|
+
}
|
|
455
|
+
function parseAll(pems) {
|
|
456
|
+
const parsed = [];
|
|
457
|
+
for (const pem of pems) try {
|
|
458
|
+
parsed.push(new X509Certificate(pem));
|
|
459
|
+
} catch {}
|
|
460
|
+
return parsed;
|
|
461
|
+
}
|
|
462
|
+
function issuedBy(child, issuer) {
|
|
463
|
+
try {
|
|
464
|
+
return child.checkIssued(issuer) && child.verify(issuer.publicKey);
|
|
465
|
+
} catch {
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
var defaultPublicRoots;
|
|
470
|
+
/** The trust anchors of public CAs that ship with Node. */
|
|
471
|
+
function publicRootCertificates() {
|
|
472
|
+
defaultPublicRoots ??= parseAll(rootCertificates);
|
|
473
|
+
return defaultPublicRoots;
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* True when the served chain leads to a public CA that clients already trust:
|
|
477
|
+
* the leaf, or one of the chain's own certificates, is signed by a public root.
|
|
478
|
+
* Such a certificate needs nothing installed on a device, so no CA is required.
|
|
479
|
+
*/
|
|
480
|
+
function chainsToPublicRoot(chainPem, roots = publicRootCertificates()) {
|
|
481
|
+
const [leaf, ...rest] = parseAll(pemCertificates(chainPem));
|
|
482
|
+
if (leaf === void 0) return false;
|
|
483
|
+
let current = leaf;
|
|
484
|
+
const pool = [...rest];
|
|
485
|
+
for (let depth = 0; depth < 8; depth += 1) {
|
|
486
|
+
if (roots.some((root) => issuedBy(current, root))) return true;
|
|
487
|
+
const next = pool.find((candidate) => candidate !== current && issuedBy(current, candidate));
|
|
488
|
+
if (next === void 0) return false;
|
|
489
|
+
pool.splice(pool.indexOf(next), 1);
|
|
490
|
+
current = next;
|
|
491
|
+
}
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
/** Is `cert` a usable CA at `now`? */
|
|
495
|
+
function caProblem(cert, now) {
|
|
496
|
+
if (!cert.ca) return "tls.ca-not-a-ca";
|
|
497
|
+
if (now < new Date(cert.validFrom) || now > new Date(cert.validTo)) return "tls.ca-expired";
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* The CA rule for an upload (D873). A CA the operator supplies must parse, be a CA, be in
|
|
502
|
+
* date and actually have signed the leaf. Without one, the upload is accepted only when
|
|
503
|
+
* its own chain carries the issuer or leads to a public root; anything else is refused,
|
|
504
|
+
* because a device cannot be told to trust a certificate nobody can hand it the CA of.
|
|
505
|
+
*/
|
|
506
|
+
function checkUploadedCa(chainPem, caPem, options) {
|
|
507
|
+
const now = options?.now ?? /* @__PURE__ */ new Date();
|
|
508
|
+
const [leaf, ...chainRest] = parseAll(pemCertificates(chainPem));
|
|
509
|
+
if (leaf === void 0) return failure("tls.cert-unparsable");
|
|
510
|
+
if (caPem !== void 0 && caPem.trim() !== "") {
|
|
511
|
+
const candidates = parseAll(pemCertificates(caPem));
|
|
512
|
+
if (candidates.length === 0) return failure("tls.ca-unparsable");
|
|
513
|
+
return pickIssuer(leaf, candidates, now, chainPem, options?.publicRoots);
|
|
514
|
+
}
|
|
515
|
+
const fromChain = chainRest.filter((cert) => issuedBy(leaf, cert));
|
|
516
|
+
if (fromChain.length > 0) return pickIssuer(leaf, fromChain, now, chainPem, options?.publicRoots);
|
|
517
|
+
if (chainsToPublicRoot(chainPem, options?.publicRoots)) return {
|
|
518
|
+
ok: true,
|
|
519
|
+
issuerPem: null,
|
|
520
|
+
publicChain: true
|
|
521
|
+
};
|
|
522
|
+
return failure("tls.ca-required");
|
|
523
|
+
}
|
|
524
|
+
function pickIssuer(leaf, candidates, now, chainPem, publicRoots) {
|
|
525
|
+
const signers = candidates.filter((candidate) => issuedBy(leaf, candidate));
|
|
526
|
+
if (signers.length === 0) return failure("tls.ca-not-issuer");
|
|
527
|
+
let firstProblem = null;
|
|
528
|
+
for (const signer of signers) {
|
|
529
|
+
const problem = caProblem(signer, now);
|
|
530
|
+
if (problem === null) return {
|
|
531
|
+
ok: true,
|
|
532
|
+
issuerPem: `${signer.toString().trimEnd()}\n`,
|
|
533
|
+
publicChain: chainsToPublicRoot(chainPem, publicRoots)
|
|
534
|
+
};
|
|
535
|
+
firstProblem ??= problem;
|
|
536
|
+
}
|
|
537
|
+
return failure(firstProblem ?? "tls.ca-not-issuer");
|
|
538
|
+
}
|
|
539
|
+
//#endregion
|
|
315
540
|
//#region src/tls/tls-status.ts
|
|
316
541
|
function tlsDir(dataDir) {
|
|
317
542
|
return join(dataDir, "tls");
|
|
@@ -344,6 +569,44 @@ function writeExtraSans(dataDir, sans) {
|
|
|
344
569
|
mkdirSync(tlsDir(dataDir), { recursive: true });
|
|
345
570
|
writeFileSync(join(tlsDir(dataDir), "extra-sans.json"), `${JSON.stringify(sans)}\n`, "utf-8");
|
|
346
571
|
}
|
|
572
|
+
/** The kept copy of the last upload: the chain and key as the operator supplied them. */
|
|
573
|
+
var STORED_UPLOAD_CERT_FILE = "uploaded.crt";
|
|
574
|
+
var STORED_UPLOAD_KEY_FILE = "uploaded.key";
|
|
575
|
+
var CA_REPLACED_MARKER_FILE = "ca-replaced.json";
|
|
576
|
+
function readStoredUpload(dir) {
|
|
577
|
+
const certPath = join(dir, STORED_UPLOAD_CERT_FILE);
|
|
578
|
+
if (!existsSync(certPath) || !existsSync(join(dir, "uploaded.key"))) return null;
|
|
579
|
+
try {
|
|
580
|
+
const leaf = new X509Certificate(readFileSync(certPath, "utf-8"));
|
|
581
|
+
return {
|
|
582
|
+
leafFingerprintSha256: leaf.fingerprint256,
|
|
583
|
+
validTo: new Date(leaf.validTo).toISOString(),
|
|
584
|
+
hasCa: existsSync(join(dir, UPLOADED_CA_FILE))
|
|
585
|
+
};
|
|
586
|
+
} catch {
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
function readLocalCaConstrained(dir) {
|
|
591
|
+
const caPath = join(dir, "camstack-ca.crt");
|
|
592
|
+
if (!existsSync(caPath)) return null;
|
|
593
|
+
try {
|
|
594
|
+
return isConstrainedCa(new X509Certificate$1(readFileSync(caPath, "utf-8")));
|
|
595
|
+
} catch {
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
function readCaReplacedAt(dir) {
|
|
600
|
+
const path = join(dir, CA_REPLACED_MARKER_FILE);
|
|
601
|
+
if (!existsSync(path)) return null;
|
|
602
|
+
try {
|
|
603
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
604
|
+
const at = parsed !== null && typeof parsed === "object" ? Reflect.get(parsed, "replacedAt") : null;
|
|
605
|
+
return typeof at === "string" ? at : null;
|
|
606
|
+
} catch {
|
|
607
|
+
return null;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
347
610
|
/** Where an upload's own CA is kept — never over the local CA's `camstack-ca.crt`. */
|
|
348
611
|
var UPLOADED_CA_FILE = "uploaded-ca.crt";
|
|
349
612
|
/** Every certificate in a PEM bundle, in order. */
|
|
@@ -398,7 +661,11 @@ function readTlsAccessStatus(dataDir, servedLeafFingerprint) {
|
|
|
398
661
|
sans: [],
|
|
399
662
|
caCertPem: (mode === "uploaded" ? null : generatedIssuer(dir))?.pem ?? null,
|
|
400
663
|
reissueError: null,
|
|
401
|
-
restartRequired: false
|
|
664
|
+
restartRequired: false,
|
|
665
|
+
caMissing: false,
|
|
666
|
+
localCaConstrained: readLocalCaConstrained(dir),
|
|
667
|
+
caReplacedAt: readCaReplacedAt(dir),
|
|
668
|
+
storedUpload: readStoredUpload(dir)
|
|
402
669
|
};
|
|
403
670
|
try {
|
|
404
671
|
const chainPem = readFileSync(certPath, "utf-8");
|
|
@@ -413,7 +680,11 @@ function readTlsAccessStatus(dataDir, servedLeafFingerprint) {
|
|
|
413
680
|
sans: san === "" ? [] : san.split(", "),
|
|
414
681
|
caCertPem: ca?.pem ?? null,
|
|
415
682
|
reissueError: null,
|
|
416
|
-
restartRequired: servedLeafFingerprint !== null && servedLeafFingerprint !== leaf.fingerprint256
|
|
683
|
+
restartRequired: servedLeafFingerprint !== null && servedLeafFingerprint !== leaf.fingerprint256,
|
|
684
|
+
caMissing: mode === "uploaded" && ca === null && !chainsToPublicRoot(chainPem),
|
|
685
|
+
localCaConstrained: readLocalCaConstrained(dir),
|
|
686
|
+
caReplacedAt: readCaReplacedAt(dir),
|
|
687
|
+
storedUpload: readStoredUpload(dir)
|
|
417
688
|
};
|
|
418
689
|
} catch {
|
|
419
690
|
return {
|
|
@@ -424,7 +695,11 @@ function readTlsAccessStatus(dataDir, servedLeafFingerprint) {
|
|
|
424
695
|
sans: [],
|
|
425
696
|
caCertPem: null,
|
|
426
697
|
reissueError: "Certificate on disk could not be parsed.",
|
|
427
|
-
restartRequired: false
|
|
698
|
+
restartRequired: false,
|
|
699
|
+
caMissing: false,
|
|
700
|
+
localCaConstrained: readLocalCaConstrained(dir),
|
|
701
|
+
caReplacedAt: readCaReplacedAt(dir),
|
|
702
|
+
storedUpload: readStoredUpload(dir)
|
|
428
703
|
};
|
|
429
704
|
}
|
|
430
705
|
}
|
|
@@ -446,7 +721,8 @@ async function prepareBootTls(dataDir, options) {
|
|
|
446
721
|
mode: "uploaded",
|
|
447
722
|
pair: loadTlsCert(certPath, keyPath),
|
|
448
723
|
ensured: void 0,
|
|
449
|
-
uploadedMissing: false
|
|
724
|
+
uploadedMissing: false,
|
|
725
|
+
dormantCaReplaced: await replaceUnconstrainedLocalCa(dataDir, options)
|
|
450
726
|
};
|
|
451
727
|
const uploadedMissing = marked === "uploaded";
|
|
452
728
|
if (uploadedMissing) writeTlsMode(dataDir, "generated");
|
|
@@ -455,7 +731,8 @@ async function prepareBootTls(dataDir, options) {
|
|
|
455
731
|
mode: "generated",
|
|
456
732
|
pair: loadTlsCert(ensured.certPath, ensured.keyPath),
|
|
457
733
|
ensured,
|
|
458
|
-
uploadedMissing
|
|
734
|
+
uploadedMissing,
|
|
735
|
+
dormantCaReplaced: false
|
|
459
736
|
};
|
|
460
737
|
}
|
|
461
738
|
//#endregion
|
|
@@ -533,66 +810,51 @@ function reloadHttpsListener(pair) {
|
|
|
533
810
|
}
|
|
534
811
|
}
|
|
535
812
|
//#endregion
|
|
536
|
-
//#region src/tls/uploaded-cert.ts
|
|
537
|
-
/**
|
|
538
|
-
* Operator-supplied material must parse, match its key, and carry
|
|
539
|
-
* `extendedKeyUsage = serverAuth` — the same leaf defects D227 refused
|
|
540
|
-
* to keep generating.
|
|
541
|
-
*/
|
|
542
|
-
function validateUploadedTls(certPem, keyPem) {
|
|
543
|
-
let leaf;
|
|
544
|
-
try {
|
|
545
|
-
leaf = new X509Certificate(certPem);
|
|
546
|
-
} catch {
|
|
547
|
-
return {
|
|
548
|
-
ok: false,
|
|
549
|
-
error: "Certificate PEM could not be parsed."
|
|
550
|
-
};
|
|
551
|
-
}
|
|
552
|
-
try {
|
|
553
|
-
if (!leaf.checkPrivateKey(createPrivateKey(keyPem))) return {
|
|
554
|
-
ok: false,
|
|
555
|
-
error: "Private key does not match the certificate."
|
|
556
|
-
};
|
|
557
|
-
} catch {
|
|
558
|
-
return {
|
|
559
|
-
ok: false,
|
|
560
|
-
error: "Private key PEM could not be parsed."
|
|
561
|
-
};
|
|
562
|
-
}
|
|
563
|
-
const eku = leaf.keyUsage;
|
|
564
|
-
if (eku === void 0 || !eku.includes("1.3.6.1.5.5.7.3.1")) return {
|
|
565
|
-
ok: false,
|
|
566
|
-
error: "Certificate is missing extendedKeyUsage = serverAuth."
|
|
567
|
-
};
|
|
568
|
-
return {
|
|
569
|
-
ok: true,
|
|
570
|
-
fingerprintSha256: leaf.fingerprint256,
|
|
571
|
-
validTo: new Date(leaf.validTo).toISOString()
|
|
572
|
-
};
|
|
573
|
-
}
|
|
574
|
-
//#endregion
|
|
575
813
|
//#region src/tls/tls-actions.ts
|
|
576
814
|
function withNewline(pem) {
|
|
577
815
|
return pem.endsWith("\n") ? pem : `${pem}\n`;
|
|
578
816
|
}
|
|
579
817
|
/**
|
|
580
|
-
* Store an operator's certificate. The
|
|
581
|
-
*
|
|
582
|
-
* (
|
|
583
|
-
*
|
|
584
|
-
*
|
|
818
|
+
* Store an operator's certificate (D873). The material is validated first, and a
|
|
819
|
+
* certificate that does not lead to a public CA must come with the CA that signed it
|
|
820
|
+
* (validated: it is a CA, in date, and issued the leaf) - otherwise "Download CA" would
|
|
821
|
+
* have nothing to hand out. Nothing is written when anything is refused.
|
|
822
|
+
*
|
|
823
|
+
* The leaf (or chain) and key are written twice: as the served pair, and as a kept copy
|
|
824
|
+
* (`uploaded.crt` / `uploaded.key`, with the issuing CA in `uploaded-ca.crt`) that
|
|
825
|
+
* "Revert to generated" leaves in place so the upload can be selected again. The local
|
|
826
|
+
* CA (`camstack-ca.crt` + key) is never touched - it did not sign the upload, and it is
|
|
827
|
+
* what every device that trusts it still needs.
|
|
585
828
|
*/
|
|
586
|
-
async function storeUploadedCertificate(dataDir, input) {
|
|
829
|
+
async function storeUploadedCertificate(dataDir, input, options) {
|
|
587
830
|
const valid = validateUploadedTls(input.certPem, input.keyPem);
|
|
588
|
-
if (!valid.ok) throw
|
|
831
|
+
if (!valid.ok) throw userError(valid.code, {}, valid.error);
|
|
832
|
+
const ca = checkUploadedCa(input.certPem, input.caPem, options);
|
|
833
|
+
if (!ca.ok) throw userError(ca.code, {}, ca.error);
|
|
589
834
|
const dir = join(dataDir, "tls");
|
|
590
835
|
await mkdir(dir, { recursive: true });
|
|
591
|
-
|
|
592
|
-
|
|
836
|
+
const certText = withNewline(input.certPem);
|
|
837
|
+
const keyText = withNewline(input.keyPem);
|
|
838
|
+
writeOwnerOnlyFileSync(join(dir, STORED_UPLOAD_KEY_FILE), keyText);
|
|
839
|
+
await writeFile(join(dir, STORED_UPLOAD_CERT_FILE), certText);
|
|
593
840
|
const caPath = join(dir, UPLOADED_CA_FILE);
|
|
594
|
-
if (
|
|
841
|
+
if (ca.issuerPem !== null) await writeFile(caPath, ca.issuerPem);
|
|
595
842
|
else rmSync(caPath, { force: true });
|
|
843
|
+
await writeFile(join(dir, "camstack.crt"), certText);
|
|
844
|
+
writeOwnerOnlyFileSync(join(dir, "camstack.key"), keyText);
|
|
845
|
+
writeTlsMode(dataDir, "uploaded");
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Serve the kept upload again after a revert (D873). Refused with a clear error when none
|
|
849
|
+
* is kept; the generated material stays on disk and is untouched.
|
|
850
|
+
*/
|
|
851
|
+
async function useStoredUploadedCertificate(dataDir) {
|
|
852
|
+
const dir = join(dataDir, "tls");
|
|
853
|
+
const certPath = join(dir, STORED_UPLOAD_CERT_FILE);
|
|
854
|
+
const keyPath = join(dir, STORED_UPLOAD_KEY_FILE);
|
|
855
|
+
if (!existsSync(certPath) || !existsSync(keyPath)) throw userError("tls.no-stored-upload", {}, "No uploaded certificate is kept on this node.");
|
|
856
|
+
copyFileSync(certPath, join(dir, "camstack.crt"));
|
|
857
|
+
copyFileSync(keyPath, join(dir, "camstack.key"));
|
|
596
858
|
writeTlsMode(dataDir, "uploaded");
|
|
597
859
|
}
|
|
598
860
|
/** Issue a new generated leaf under the local CA. Refused while an upload is served. */
|
|
@@ -600,10 +862,20 @@ async function regenerateCertificate(dataDir, options) {
|
|
|
600
862
|
if (readTlsMode(dataDir) === "uploaded") throw new Error("Cannot regenerate an uploaded certificate — revert to generated first.");
|
|
601
863
|
await reissueTlsLeaf(dataDir, options);
|
|
602
864
|
}
|
|
603
|
-
/**
|
|
865
|
+
/**
|
|
866
|
+
* Serve the generated local-CA leaf again (D227). The uploaded certificate and its CA stay
|
|
867
|
+
* on disk (D873) so it can be selected again; an upload made before they were kept is
|
|
868
|
+
* copied aside first, from the pair being served.
|
|
869
|
+
*/
|
|
604
870
|
async function revertToGeneratedCertificate(dataDir, options) {
|
|
871
|
+
const dir = join(dataDir, "tls");
|
|
872
|
+
const keptCert = join(dir, STORED_UPLOAD_CERT_FILE);
|
|
873
|
+
const keptKey = join(dir, STORED_UPLOAD_KEY_FILE);
|
|
874
|
+
if (readTlsMode(dataDir) === "uploaded" && !existsSync(keptCert) && existsSync(join(dir, "camstack.crt")) && existsSync(join(dir, "camstack.key"))) {
|
|
875
|
+
copyFileSync(join(dir, "camstack.crt"), keptCert);
|
|
876
|
+
copyFileSync(join(dir, "camstack.key"), keptKey);
|
|
877
|
+
}
|
|
605
878
|
writeTlsMode(dataDir, "generated");
|
|
606
|
-
rmSync(join(dataDir, "tls", UPLOADED_CA_FILE), { force: true });
|
|
607
879
|
await ensureTlsCert(dataDir, options);
|
|
608
880
|
}
|
|
609
881
|
/**
|
|
@@ -623,7 +895,7 @@ function applyCertificateToListener(dataDir) {
|
|
|
623
895
|
//#region src/builtins/local-network/hub-mdns-ciao.ts
|
|
624
896
|
function createCiaoPublisher() {
|
|
625
897
|
return { async publish(announcement) {
|
|
626
|
-
const { getResponder } = await import("./lib-
|
|
898
|
+
const { getResponder } = await import("./lib-DK38ompL.mjs").then((m) => /* @__PURE__ */ __toESM(m.default));
|
|
627
899
|
const responder = getResponder();
|
|
628
900
|
const service = responder.createService({
|
|
629
901
|
name: announcement.name,
|
|
@@ -645,4 +917,4 @@ function createCiaoPublisher() {
|
|
|
645
917
|
} };
|
|
646
918
|
}
|
|
647
919
|
//#endregion
|
|
648
|
-
export {
|
|
920
|
+
export { allFamiliesListenHost as A, caReplacementMarkerFile as C, replaceUnconstrainedLocalCa as D, reissueTlsLeaf as E, registerLanHttpHandler as F, bindPendingLanHttp as M, closeLanHttp as N, retirePreviousLocalCa as O, readLanHttpState as P, validateUploadedTls as S, loadTlsCert as T, splitPemCertificates as _, storeUploadedCertificate as a, chainsToPublicRoot as b, leafFingerprintOf as c, servedHttpsLeafFingerprint as d, prepareBootTls as f, readTlsMode as g, readTlsAccessStatus as h, revertToGeneratedCertificate as i, applyLanHttp as j, DEFAULT_LAN_HTTP_PORT as k, registerHttpsListener as l, readExtraSans as m, applyCertificateToListener as n, useStoredUploadedCertificate as o, UPLOADED_CA_FILE as p, regenerateCertificate as r, clearHttpsListener as s, createCiaoPublisher as t, reloadHttpsListener as u, writeExtraSans as v, ensureTlsCert as w, checkUploadedCa as x, writeTlsMode as y };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type CatalogLang, type CatalogLookup, type TranslationParams } from "@camstack/types";
|
|
2
|
+
/** The core namespace: user-facing error texts and core schema labels. */
|
|
3
|
+
export declare const SERVER_NAMESPACE = "server";
|
|
4
|
+
/** `@camstack/addon-provider-reolink` -> `provider-reolink`; `@camstack/system` -> `system`. */
|
|
5
|
+
export declare function catalogNamespaceOf(packageName: string): string;
|
|
6
|
+
export type CatalogWarn = (message: string) => void;
|
|
7
|
+
export declare class CatalogRegistry {
|
|
8
|
+
private readonly warn;
|
|
9
|
+
private readonly namespaces;
|
|
10
|
+
constructor(warn?: CatalogWarn);
|
|
11
|
+
/** Validates `raw` and installs it; returns false (after warning) when it is not a flat string map. */
|
|
12
|
+
set(ns: string, lang: CatalogLang, raw: unknown, origin: string): boolean;
|
|
13
|
+
/** Loads `<packageDir>/i18n/<lang>.json` for every language that has a file. */
|
|
14
|
+
loadAddonDir(ns: string, packageDir: string): void;
|
|
15
|
+
/** Drops a namespace (addon uninstalled or redeployed before a reload). */
|
|
16
|
+
clear(ns: string): void;
|
|
17
|
+
/** The text for `ns:key` in `lang`, or `undefined`. Never falls back across languages. */
|
|
18
|
+
lookup(lang: CatalogLang, ns: string, key: string): string | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* `t(ns, key, params, fallbackEnglish)` - the translation in `lang`, else
|
|
21
|
+
* the English fallback (also interpolated). English is never looked up: the
|
|
22
|
+
* fallback IS the English text.
|
|
23
|
+
*/
|
|
24
|
+
t(lang: CatalogLang, ns: string, key: string, params: TranslationParams | undefined, fallbackEnglish: string): string;
|
|
25
|
+
/** A lookup bound to one language, for `localizeConfigTree`. English never substitutes. */
|
|
26
|
+
lookupFor(lang: CatalogLang): CatalogLookup;
|
|
27
|
+
/** `t` for a `"ns:key"` reference. */
|
|
28
|
+
tRef(lang: CatalogLang, ref: string, params: TranslationParams | undefined, fallbackEnglish: string): string;
|
|
29
|
+
}
|
|
30
|
+
/** A fresh registry holding the core `server` namespace (specs build their own). */
|
|
31
|
+
export declare function createCoreCatalogRegistry(warn?: CatalogWarn): CatalogRegistry;
|
|
32
|
+
/** The hub's one registry, core catalogs preloaded. */
|
|
33
|
+
export declare function getCatalogRegistry(warn?: CatalogWarn): CatalogRegistry;
|
|
34
|
+
/** Spec hook: forget the shared registry. */
|
|
35
|
+
export declare function resetCatalogRegistryForTests(): void;
|
package/dist/index.d.ts
CHANGED
|
@@ -145,3 +145,4 @@ export { CAP_METHOD_NOT_IMPLEMENTED } from "./kernel/moleculer/child-cap-dispatc
|
|
|
145
145
|
export { BackupDecryptError, BackupKeyFormatError, BackupKeyMismatchError, generateBackupKeyPair, readBackupEncryptionHeader } from "./builtins/system-backup/backup-encryption.js";
|
|
146
146
|
export { SystemBackupService } from "./builtins/system-backup/system-backup.service.js";
|
|
147
147
|
export { BackupKeyRequiredError, prepareRestoreArchive } from "./builtins/backup-orchestrator/restore-archive.js";
|
|
148
|
+
export * from "./i18n/index.js";
|