sporades 0.9.21 → 0.9.22
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/CHANGES.md +24 -4
- package/bin/sporades-host-helper.js +443 -273
- package/bin/sporades.js +708 -659
- package/dist/cli/cli-help.d.ts +2 -2
- package/dist/cli/cli-help.d.ts.map +1 -1
- package/dist/cli/cli-help.js +2 -0
- package/dist/cli/cli-help.js.map +1 -1
- package/dist/cli/cli-version.d.ts +1 -1
- package/dist/cli/cli-version.js +1 -1
- package/dist/cli/host-domain-aliases.d.ts +5 -0
- package/dist/cli/host-domain-aliases.d.ts.map +1 -0
- package/dist/cli/host-domain-aliases.js +69 -0
- package/dist/cli/host-domain-aliases.js.map +1 -0
- package/dist/cli/host-helper-requests.d.ts +1 -0
- package/dist/cli/host-helper-requests.d.ts.map +1 -1
- package/dist/cli/host-helper-validation.d.ts.map +1 -1
- package/dist/cli/host-helper-validation.js +2 -0
- package/dist/cli/host-helper-validation.js.map +1 -1
- package/dist/cli/host-request-builders.d.ts +2 -1
- package/dist/cli/host-request-builders.d.ts.map +1 -1
- package/dist/cli/host-request-builders.js +2 -1
- package/dist/cli/host-request-builders.js.map +1 -1
- package/dist/cli/hosted-capsule-contract.d.ts +2 -0
- package/dist/cli/hosted-capsule-contract.d.ts.map +1 -1
- package/dist/cli/sporades-host-helper.js +124 -15
- package/dist/cli/sporades-host-helper.js.map +1 -1
- package/dist/cli/sporades.js +16 -2
- package/dist/cli/sporades.js.map +1 -1
- package/dist/generated-source-manifest.json +7 -2
- package/dist/http-runtime.d.ts +2 -1
- package/dist/http-runtime.d.ts.map +1 -1
- package/dist/http-runtime.js +16 -3
- package/dist/http-runtime.js.map +1 -1
- package/dist/templates/server-bundle-entry.js +1 -0
- package/dist/templates/server-bundle-entry.js.map +1 -1
- package/package.json +1 -1
|
@@ -16678,14 +16678,14 @@ var require_svgPath = __commonJS({
|
|
|
16678
16678
|
["Z", 0],
|
|
16679
16679
|
["z", 0]
|
|
16680
16680
|
]);
|
|
16681
|
-
var parse5 = function(
|
|
16681
|
+
var parse5 = function(path5) {
|
|
16682
16682
|
var cmd;
|
|
16683
16683
|
var ret = [];
|
|
16684
16684
|
var args = [];
|
|
16685
16685
|
var curArg = "";
|
|
16686
16686
|
var foundDecimal = false;
|
|
16687
16687
|
var params = 0;
|
|
16688
|
-
for (var _i = 0, path_1 =
|
|
16688
|
+
for (var _i = 0, path_1 = path5; _i < path_1.length; _i++) {
|
|
16689
16689
|
var c = path_1[_i];
|
|
16690
16690
|
if (parameters.has(c)) {
|
|
16691
16691
|
params = parameters.get(c);
|
|
@@ -16999,8 +16999,8 @@ var require_svgPath = __commonJS({
|
|
|
16999
16999
|
];
|
|
17000
17000
|
return result;
|
|
17001
17001
|
};
|
|
17002
|
-
exports.svgPathToOperators = function(
|
|
17003
|
-
return apply(parse5(
|
|
17002
|
+
exports.svgPathToOperators = function(path5) {
|
|
17003
|
+
return apply(parse5(path5));
|
|
17004
17004
|
};
|
|
17005
17005
|
}
|
|
17006
17006
|
});
|
|
@@ -17183,7 +17183,7 @@ var require_operations = __commonJS({
|
|
|
17183
17183
|
operators_1.popGraphicsState()
|
|
17184
17184
|
]).filter(Boolean);
|
|
17185
17185
|
};
|
|
17186
|
-
exports.drawSvgPath = function(
|
|
17186
|
+
exports.drawSvgPath = function(path5, options) {
|
|
17187
17187
|
var _a, _b, _c;
|
|
17188
17188
|
return tslib_1.__spreadArrays([
|
|
17189
17189
|
operators_1.pushGraphicsState(),
|
|
@@ -17197,7 +17197,7 @@ var require_operations = __commonJS({
|
|
|
17197
17197
|
options.borderWidth && operators_1.setLineWidth(options.borderWidth),
|
|
17198
17198
|
options.borderLineCap && operators_1.setLineCap(options.borderLineCap),
|
|
17199
17199
|
operators_1.setDashPattern((_b = options.borderDashArray) !== null && _b !== void 0 ? _b : [], (_c = options.borderDashPhase) !== null && _c !== void 0 ? _c : 0)
|
|
17200
|
-
], svgPath_1.svgPathToOperators(
|
|
17200
|
+
], svgPath_1.svgPathToOperators(path5), [
|
|
17201
17201
|
// prettier-ignore
|
|
17202
17202
|
options.color && options.borderWidth ? operators_1.fillAndStroke() : options.color ? operators_1.fill() : options.borderColor ? operators_1.stroke() : operators_1.closePath(),
|
|
17203
17203
|
operators_1.popGraphicsState()
|
|
@@ -21495,12 +21495,12 @@ var require_PDFPage = __commonJS({
|
|
|
21495
21495
|
graphicsState: graphicsStateKey
|
|
21496
21496
|
}));
|
|
21497
21497
|
};
|
|
21498
|
-
PDFPage2.prototype.drawSvgPath = function(
|
|
21498
|
+
PDFPage2.prototype.drawSvgPath = function(path5, options) {
|
|
21499
21499
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
21500
21500
|
if (options === void 0) {
|
|
21501
21501
|
options = {};
|
|
21502
21502
|
}
|
|
21503
|
-
utils_1.assertIs(
|
|
21503
|
+
utils_1.assertIs(path5, "path", ["string"]);
|
|
21504
21504
|
utils_1.assertOrUndefined(options.x, "options.x", ["number"]);
|
|
21505
21505
|
utils_1.assertOrUndefined(options.y, "options.y", ["number"]);
|
|
21506
21506
|
utils_1.assertOrUndefined(options.scale, "options.scale", ["number"]);
|
|
@@ -21529,7 +21529,7 @@ var require_PDFPage = __commonJS({
|
|
|
21529
21529
|
options.borderColor = colors_1.rgb(0, 0, 0);
|
|
21530
21530
|
}
|
|
21531
21531
|
var contentStream = this.getContentStream();
|
|
21532
|
-
contentStream.push.apply(contentStream, operations_1.drawSvgPath(
|
|
21532
|
+
contentStream.push.apply(contentStream, operations_1.drawSvgPath(path5, {
|
|
21533
21533
|
x: (_a = options.x) !== null && _a !== void 0 ? _a : this.x,
|
|
21534
21534
|
y: (_b = options.y) !== null && _b !== void 0 ? _b : this.y,
|
|
21535
21535
|
scale: options.scale,
|
|
@@ -23605,9 +23605,9 @@ var require_decoder = __commonJS({
|
|
|
23605
23605
|
return a < 0 ? 0 : a > 255 ? 255 : a;
|
|
23606
23606
|
}
|
|
23607
23607
|
constructor.prototype = {
|
|
23608
|
-
load: function load(
|
|
23608
|
+
load: function load(path5) {
|
|
23609
23609
|
var xhr = new XMLHttpRequest();
|
|
23610
|
-
xhr.open("GET",
|
|
23610
|
+
xhr.open("GET", path5, true);
|
|
23611
23611
|
xhr.responseType = "arraybuffer";
|
|
23612
23612
|
xhr.onload = (function() {
|
|
23613
23613
|
var data2 = new Uint8Array(xhr.response || xhr.mozResponseArrayBuffer);
|
|
@@ -26261,13 +26261,118 @@ var require_png2 = __commonJS({
|
|
|
26261
26261
|
}
|
|
26262
26262
|
});
|
|
26263
26263
|
|
|
26264
|
+
// src/cli/host-domain-aliases.ts
|
|
26265
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
26266
|
+
import path from "node:path";
|
|
26267
|
+
|
|
26268
|
+
// src/cli/cli-support.ts
|
|
26269
|
+
function errorDetails(error) {
|
|
26270
|
+
if (error === null || error === void 0) {
|
|
26271
|
+
return {};
|
|
26272
|
+
}
|
|
26273
|
+
return typeof error === "object" ? error : { message: String(error) };
|
|
26274
|
+
}
|
|
26275
|
+
function helperError(message, hint, diagnostics = null) {
|
|
26276
|
+
const error = new Error(message);
|
|
26277
|
+
error.hint = hint;
|
|
26278
|
+
if (diagnostics) {
|
|
26279
|
+
error.diagnostics = diagnostics;
|
|
26280
|
+
}
|
|
26281
|
+
return error;
|
|
26282
|
+
}
|
|
26283
|
+
function readStdin() {
|
|
26284
|
+
return new Promise((resolve, reject) => {
|
|
26285
|
+
let stdin = "";
|
|
26286
|
+
process.stdin.setEncoding("utf8");
|
|
26287
|
+
process.stdin.on("data", (chunk) => {
|
|
26288
|
+
stdin += chunk;
|
|
26289
|
+
});
|
|
26290
|
+
process.stdin.on("end", () => resolve(stdin));
|
|
26291
|
+
process.stdin.on("error", reject);
|
|
26292
|
+
});
|
|
26293
|
+
}
|
|
26294
|
+
function delay(ms) {
|
|
26295
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
26296
|
+
}
|
|
26297
|
+
function writeResult(result, failed = false) {
|
|
26298
|
+
process.stdout.write(`${JSON.stringify(result)}
|
|
26299
|
+
`);
|
|
26300
|
+
if (failed) {
|
|
26301
|
+
process.exitCode = 1;
|
|
26302
|
+
}
|
|
26303
|
+
}
|
|
26304
|
+
function writeEnvelope(result, failed = false) {
|
|
26305
|
+
writeResult(result, failed);
|
|
26306
|
+
}
|
|
26307
|
+
|
|
26308
|
+
// src/cli/host-domain-aliases.ts
|
|
26309
|
+
function validateAliasDomains(value) {
|
|
26310
|
+
if (value === void 0) return [];
|
|
26311
|
+
if (!Array.isArray(value) || value.length > 20 || value.some((hostname) => typeof hostname !== "string" || hostname.length > 253 || !hostname.includes(".") || /^[0-9.]+$/.test(hostname) || !hostname.split(".").every((label) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label)))) {
|
|
26312
|
+
throw helperError(
|
|
26313
|
+
"Invalid Hosted Capsule alias domains.",
|
|
26314
|
+
"Supply at most 20 unique lowercase DNS hostnames, without schemes, ports, paths, wildcards, or IP addresses."
|
|
26315
|
+
);
|
|
26316
|
+
}
|
|
26317
|
+
if (new Set(value).size !== value.length) {
|
|
26318
|
+
throw helperError("Duplicate Hosted Capsule alias domain.", "Supply each alias domain once.");
|
|
26319
|
+
}
|
|
26320
|
+
return [...value];
|
|
26321
|
+
}
|
|
26322
|
+
async function assertHostnamesAvailable(remoteRoot, hostnames, owner) {
|
|
26323
|
+
const wanted = new Set(hostnames.map((hostname) => hostname.replace(/:\d+$/, "")));
|
|
26324
|
+
if (wanted.size !== hostnames.length) {
|
|
26325
|
+
throw helperError("Duplicate Hosted Capsule hostname.", "An alias must differ from the Capsule subdomain.");
|
|
26326
|
+
}
|
|
26327
|
+
const assertUnclaimed = (hostname, claimant) => {
|
|
26328
|
+
if (claimant !== owner && wanted.has(hostname.replace(/:\d+$/, ""))) {
|
|
26329
|
+
throw helperError(
|
|
26330
|
+
"Hosted Capsule hostname is already reserved.",
|
|
26331
|
+
`Choose another hostname; ${hostname} belongs to ${claimant}.`
|
|
26332
|
+
);
|
|
26333
|
+
}
|
|
26334
|
+
};
|
|
26335
|
+
const entries = async (directory) => readdir(directory, { withFileTypes: true }).catch((error) => {
|
|
26336
|
+
if (error.code === "ENOENT") return [];
|
|
26337
|
+
throw error;
|
|
26338
|
+
});
|
|
26339
|
+
for (const entry of await entries(path.join(remoteRoot, "caddy", "hosts"))) {
|
|
26340
|
+
if (entry.name.endsWith(".caddy")) {
|
|
26341
|
+
const domain = entry.name.slice(0, -6);
|
|
26342
|
+
assertUnclaimed(`host.${domain}`, `health:${domain}`);
|
|
26343
|
+
}
|
|
26344
|
+
}
|
|
26345
|
+
for (const domain of await entries(path.join(remoteRoot, "hosts"))) {
|
|
26346
|
+
if (domain.isSymbolicLink()) throw helperError("Cannot inspect Hosted domain ownership.", "Replace symbolic links in the Host registry with canonical directories.");
|
|
26347
|
+
if (!domain.isDirectory()) continue;
|
|
26348
|
+
for (const collection of ["capsules", "registration-claims"]) {
|
|
26349
|
+
const directory = path.join(remoteRoot, "hosts", domain.name, "registry", collection);
|
|
26350
|
+
for (const entry of await entries(directory)) {
|
|
26351
|
+
if (!entry.name.endsWith(".json")) continue;
|
|
26352
|
+
if (!entry.isFile()) throw helperError("Cannot inspect Hosted Capsule ownership.", "Repair the Host registry before registering domains.");
|
|
26353
|
+
const record = JSON.parse(await readFile(path.join(directory, entry.name), "utf8"));
|
|
26354
|
+
if (record.domain !== domain.name || record.subname !== entry.name.slice(0, -5)) {
|
|
26355
|
+
throw helperError("Invalid Hosted Capsule registry identity.", "Repair the Host registry before registering domains.");
|
|
26356
|
+
}
|
|
26357
|
+
if (collection === "capsules" && record.status === "unregistered") continue;
|
|
26358
|
+
const claimant = `${record.domain}/${record.subname}`;
|
|
26359
|
+
const aliases = validateAliasDomains(record.aliasDomains);
|
|
26360
|
+
const previousAliases = collection === "registration-claims" ? validateAliasDomains(record.previousAliasDomains) : [];
|
|
26361
|
+
for (const hostname of [`${record.subname}.${record.domain}`, ...aliases, ...previousAliases]) {
|
|
26362
|
+
assertUnclaimed(hostname, claimant);
|
|
26363
|
+
}
|
|
26364
|
+
}
|
|
26365
|
+
}
|
|
26366
|
+
}
|
|
26367
|
+
}
|
|
26368
|
+
|
|
26264
26369
|
// src/cli/sporades-host-helper.ts
|
|
26265
26370
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
26266
26371
|
import { constants as fsConstants, createReadStream, statSync } from "node:fs";
|
|
26267
|
-
import { access, chmod, lstat, mkdir, open, opendir, readdir, readFile as
|
|
26372
|
+
import { access, chmod, lstat, mkdir, open, opendir, readdir as readdir2, readFile as readFile3, readlink, rename, rm, stat, statfs, symlink, writeFile } from "node:fs/promises";
|
|
26268
26373
|
import { createHash, generateKeyPairSync, randomBytes } from "node:crypto";
|
|
26269
26374
|
import { freemem, loadavg, totalmem } from "node:os";
|
|
26270
|
-
import
|
|
26375
|
+
import path4 from "node:path";
|
|
26271
26376
|
import { isDeepStrictEqual } from "node:util";
|
|
26272
26377
|
|
|
26273
26378
|
// src/base-image.ts
|
|
@@ -42546,48 +42651,8 @@ var serviceUserMutationAuthority = Object.freeze({ kind: "service-user-mutation-
|
|
|
42546
42651
|
var MutationExecutionStorage = process.getBuiltinModule("node:async_hooks").AsyncLocalStorage;
|
|
42547
42652
|
var mutationExecution = new MutationExecutionStorage();
|
|
42548
42653
|
|
|
42549
|
-
// src/cli/cli-support.ts
|
|
42550
|
-
function errorDetails(error) {
|
|
42551
|
-
if (error === null || error === void 0) {
|
|
42552
|
-
return {};
|
|
42553
|
-
}
|
|
42554
|
-
return typeof error === "object" ? error : { message: String(error) };
|
|
42555
|
-
}
|
|
42556
|
-
function helperError(message, hint, diagnostics = null) {
|
|
42557
|
-
const error = new Error(message);
|
|
42558
|
-
error.hint = hint;
|
|
42559
|
-
if (diagnostics) {
|
|
42560
|
-
error.diagnostics = diagnostics;
|
|
42561
|
-
}
|
|
42562
|
-
return error;
|
|
42563
|
-
}
|
|
42564
|
-
function readStdin() {
|
|
42565
|
-
return new Promise((resolve, reject) => {
|
|
42566
|
-
let stdin = "";
|
|
42567
|
-
process.stdin.setEncoding("utf8");
|
|
42568
|
-
process.stdin.on("data", (chunk) => {
|
|
42569
|
-
stdin += chunk;
|
|
42570
|
-
});
|
|
42571
|
-
process.stdin.on("end", () => resolve(stdin));
|
|
42572
|
-
process.stdin.on("error", reject);
|
|
42573
|
-
});
|
|
42574
|
-
}
|
|
42575
|
-
function delay(ms) {
|
|
42576
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
42577
|
-
}
|
|
42578
|
-
function writeResult(result, failed = false) {
|
|
42579
|
-
process.stdout.write(`${JSON.stringify(result)}
|
|
42580
|
-
`);
|
|
42581
|
-
if (failed) {
|
|
42582
|
-
process.exitCode = 1;
|
|
42583
|
-
}
|
|
42584
|
-
}
|
|
42585
|
-
function writeEnvelope(result, failed = false) {
|
|
42586
|
-
writeResult(result, failed);
|
|
42587
|
-
}
|
|
42588
|
-
|
|
42589
42654
|
// src/cli/cli-version.ts
|
|
42590
|
-
var CLI_VERSION = "0.9.
|
|
42655
|
+
var CLI_VERSION = "0.9.22";
|
|
42591
42656
|
|
|
42592
42657
|
// src/cli/schedule-inspection-envelope.ts
|
|
42593
42658
|
var SCHEDULE_DIAGNOSTIC_FIELDS = /* @__PURE__ */ new Set([
|
|
@@ -42833,8 +42898,8 @@ function validatePublicArchiveBounds(entries) {
|
|
|
42833
42898
|
}
|
|
42834
42899
|
|
|
42835
42900
|
// src/cli/host-helper-config.ts
|
|
42836
|
-
import { readFile } from "node:fs/promises";
|
|
42837
|
-
import
|
|
42901
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
42902
|
+
import path2 from "node:path";
|
|
42838
42903
|
var HOST_HELPER_CONFIG_FILE = "sporades-host-helper.json";
|
|
42839
42904
|
var DEFAULT_HOSTED_CAPSULE_DOCKER_IMAGE = SPORADES_BASE_IMAGE.image;
|
|
42840
42905
|
var DEFAULT_HOSTED_CAPSULE_DOCKER_NETWORK = "sporades-hosted-capsules";
|
|
@@ -42862,7 +42927,7 @@ async function loadHostHelperConfig(request) {
|
|
|
42862
42927
|
}
|
|
42863
42928
|
let contents;
|
|
42864
42929
|
try {
|
|
42865
|
-
contents = await
|
|
42930
|
+
contents = await readFile2(configPath, "utf8");
|
|
42866
42931
|
} catch (error) {
|
|
42867
42932
|
if (errorDetails(error).code === "ENOENT" && !process.env.SPORADES_HOST_HELPER_CONFIG) {
|
|
42868
42933
|
return loaded;
|
|
@@ -42890,7 +42955,7 @@ function hostHelperConfigPath(request) {
|
|
|
42890
42955
|
if (typeof request.host?.remoteRoot !== "string" || request.host.remoteRoot.length === 0) {
|
|
42891
42956
|
return null;
|
|
42892
42957
|
}
|
|
42893
|
-
return
|
|
42958
|
+
return path2.join(request.host.remoteRoot, HOST_HELPER_CONFIG_FILE);
|
|
42894
42959
|
}
|
|
42895
42960
|
function applyHostHelperConfig(loaded, config, configPath) {
|
|
42896
42961
|
assertPlainObject(config, "Host helper config", configPath);
|
|
@@ -42962,7 +43027,7 @@ function readConfigPositiveInteger(value, key, configPath) {
|
|
|
42962
43027
|
}
|
|
42963
43028
|
|
|
42964
43029
|
// src/cli/host-helper-validation.ts
|
|
42965
|
-
import
|
|
43030
|
+
import path3 from "node:path";
|
|
42966
43031
|
function missingCapsuleHint(request, purpose) {
|
|
42967
43032
|
if (purpose === "push") {
|
|
42968
43033
|
return `Run \`sporades host register ${request.capsule.subname} --host ${request.host.alias}\` before pushing a release.`;
|
|
@@ -43110,7 +43175,7 @@ function validateListRequest(request) {
|
|
|
43110
43175
|
}
|
|
43111
43176
|
function validateListRegistryRecord(request, record, recordPath) {
|
|
43112
43177
|
const capsuleRecord = record;
|
|
43113
|
-
const expectedSubname =
|
|
43178
|
+
const expectedSubname = path3.basename(recordPath, ".json");
|
|
43114
43179
|
const expectedRemoteCapsuleId = `${request.host.domain}/${typeof capsuleRecord?.subname === "string" ? capsuleRecord.subname : expectedSubname}`;
|
|
43115
43180
|
const valid = capsuleRecord && typeof capsuleRecord.subname === "string" && capsuleRecord.subname.length > 0 && capsuleRecord.subname === expectedSubname && capsuleRecord.domain === request.host.domain && (capsuleRecord.remoteCapsuleId ?? expectedRemoteCapsuleId) === expectedRemoteCapsuleId;
|
|
43116
43181
|
if (!valid) {
|
|
@@ -43148,6 +43213,7 @@ function validateRegisterRequest(request) {
|
|
|
43148
43213
|
throw helperError("Invalid Hosted Capsule registration request.", "Update the Sporades CLI and retry `sporades host register`.");
|
|
43149
43214
|
}
|
|
43150
43215
|
const registration = request.registration ?? {};
|
|
43216
|
+
validateAliasDomains(registration.aliasDomains);
|
|
43151
43217
|
const mismatchedIdentity = registration.subname && registration.subname !== request.capsule.subname || registration.domain && registration.domain !== request.host.domain || registration.remoteCapsuleId && registration.remoteCapsuleId !== `${request.host.domain}/${request.capsule.subname}`;
|
|
43152
43218
|
if (mismatchedIdentity) {
|
|
43153
43219
|
throw helperError(
|
|
@@ -43236,8 +43302,8 @@ function validateInstallRequest(request) {
|
|
|
43236
43302
|
if (!directories?.releases || !directories.release) {
|
|
43237
43303
|
throw helperError("Invalid Hosted Capsule release directory.", "Update the Sporades CLI and retry `sporades host push`.");
|
|
43238
43304
|
}
|
|
43239
|
-
const expectedReleaseDirectory =
|
|
43240
|
-
if (
|
|
43305
|
+
const expectedReleaseDirectory = path3.join(directories.releases, release.id);
|
|
43306
|
+
if (path3.resolve(directories.release) !== path3.resolve(expectedReleaseDirectory)) {
|
|
43241
43307
|
throw helperError("Invalid Hosted Capsule release directory.", "Update the Sporades CLI and retry `sporades host push`.");
|
|
43242
43308
|
}
|
|
43243
43309
|
}
|
|
@@ -43356,12 +43422,12 @@ async function runHostHelperInstaller() {
|
|
|
43356
43422
|
const internal = process.argv[2] === "--install-host-helper-internal";
|
|
43357
43423
|
const target = process.argv[3];
|
|
43358
43424
|
const expectedChecksum = process.argv[4];
|
|
43359
|
-
if (process.argv.length !== 5 || typeof target !== "string" || !
|
|
43425
|
+
if (process.argv.length !== 5 || typeof target !== "string" || !path4.isAbsolute(target) || path4.basename(target) !== "sporades-host-helper" || !/^[a-f0-9]{64}$/.test(expectedChecksum ?? "")) {
|
|
43360
43426
|
throw helperError("Invalid Host helper upgrade request.", "Retry with `sporades host upgrade --host <alias>`.");
|
|
43361
43427
|
}
|
|
43362
43428
|
const stage = process.argv[1];
|
|
43363
|
-
const directory =
|
|
43364
|
-
const expectedStage =
|
|
43429
|
+
const directory = path4.dirname(target);
|
|
43430
|
+
const expectedStage = path4.join(directory, `.sporades-host-helper-stage-${expectedChecksum}.mjs`);
|
|
43365
43431
|
if (stage !== expectedStage) {
|
|
43366
43432
|
throw helperError("Invalid staged Host helper path.", "Retry with `sporades host upgrade --host <alias>`.");
|
|
43367
43433
|
}
|
|
@@ -43375,7 +43441,7 @@ async function runHostHelperInstaller() {
|
|
|
43375
43441
|
"--conflict-exit-code",
|
|
43376
43442
|
"75",
|
|
43377
43443
|
"--no-fork",
|
|
43378
|
-
|
|
43444
|
+
path4.join(directory, ".sporades-host-helper.upgrade.lock"),
|
|
43379
43445
|
process.execPath,
|
|
43380
43446
|
stage,
|
|
43381
43447
|
"--install-host-helper-internal",
|
|
@@ -43416,14 +43482,14 @@ function hostHelperUpgradeTimeoutMs(name2, fallback) {
|
|
|
43416
43482
|
return value;
|
|
43417
43483
|
}
|
|
43418
43484
|
async function installHostHelperPayload(stage, target, expectedChecksum) {
|
|
43419
|
-
const directory =
|
|
43420
|
-
const pointer =
|
|
43421
|
-
const blocked =
|
|
43422
|
-
const needsDrain =
|
|
43485
|
+
const directory = path4.dirname(target);
|
|
43486
|
+
const pointer = path4.join(directory, ".sporades-host-helper.active");
|
|
43487
|
+
const blocked = path4.join(directory, ".sporades-host-helper.upgrade-blocked");
|
|
43488
|
+
const needsDrain = path4.join(directory, ".sporades-host-helper.needs-drain");
|
|
43423
43489
|
const newPayloadName = `.sporades-host-helper-payload-${expectedChecksum}.mjs`;
|
|
43424
|
-
const newPayload =
|
|
43490
|
+
const newPayload = path4.join(directory, newPayloadName);
|
|
43425
43491
|
await mkdir(directory, { recursive: true });
|
|
43426
|
-
if (createHash("sha256").update(await
|
|
43492
|
+
if (createHash("sha256").update(await readFile3(stage)).digest("hex") !== expectedChecksum) {
|
|
43427
43493
|
throw helperError("Staged Host helper checksum did not match.", "Upload the immutable Host helper again, then retry the upgrade.");
|
|
43428
43494
|
}
|
|
43429
43495
|
await publishHostHelperFile(stage, newPayload, 493);
|
|
@@ -43431,18 +43497,18 @@ async function installHostHelperPayload(stage, target, expectedChecksum) {
|
|
|
43431
43497
|
let firstCooperativeUpgrade = true;
|
|
43432
43498
|
let currentTarget;
|
|
43433
43499
|
try {
|
|
43434
|
-
currentTarget = await
|
|
43500
|
+
currentTarget = await readFile3(target);
|
|
43435
43501
|
} catch {
|
|
43436
43502
|
throw helperError("Current Host helper was not found.", "Install the current Host helper before retrying the upgrade.");
|
|
43437
43503
|
}
|
|
43438
43504
|
if (currentTarget.toString("utf8").includes(HOST_HELPER_DISPATCHER_MARKER)) {
|
|
43439
43505
|
firstCooperativeUpgrade = false;
|
|
43440
|
-
previousPayloadName = (await
|
|
43506
|
+
previousPayloadName = (await readFile3(pointer, "utf8")).trim();
|
|
43441
43507
|
await validateHostHelperPayload(directory, previousPayloadName);
|
|
43442
43508
|
} else {
|
|
43443
43509
|
const previousChecksum = createHash("sha256").update(currentTarget).digest("hex");
|
|
43444
43510
|
previousPayloadName = `.sporades-host-helper-payload-${previousChecksum}.mjs`;
|
|
43445
|
-
await publishHostHelperFile(target,
|
|
43511
|
+
await publishHostHelperFile(target, path4.join(directory, previousPayloadName), 493);
|
|
43446
43512
|
await writeHostHelperPointer(pointer, previousPayloadName);
|
|
43447
43513
|
await writeFile(needsDrain, "legacy-helper-drain-required\n", { mode: 384 });
|
|
43448
43514
|
}
|
|
@@ -43461,7 +43527,7 @@ async function installHostHelperPayload(stage, target, expectedChecksum) {
|
|
|
43461
43527
|
}
|
|
43462
43528
|
}
|
|
43463
43529
|
async function publishHostHelperFile(source, target, mode) {
|
|
43464
|
-
const contents = await
|
|
43530
|
+
const contents = await readFile3(source);
|
|
43465
43531
|
await publishHostHelperBytes(contents, target, mode);
|
|
43466
43532
|
}
|
|
43467
43533
|
async function publishHostHelperBytes(contents, target, mode) {
|
|
@@ -43475,15 +43541,15 @@ async function publishHostHelperBytes(contents, target, mode) {
|
|
|
43475
43541
|
}
|
|
43476
43542
|
}
|
|
43477
43543
|
async function writeHostHelperPointer(pointer, payloadName) {
|
|
43478
|
-
await validateHostHelperPayload(
|
|
43544
|
+
await validateHostHelperPayload(path4.dirname(pointer), payloadName);
|
|
43479
43545
|
await publishHostHelperBytes(Buffer.from(`${payloadName}
|
|
43480
43546
|
`, "utf8"), pointer, 384);
|
|
43481
43547
|
}
|
|
43482
43548
|
async function validateHostHelperPayload(directory, payloadName) {
|
|
43483
43549
|
const match = /^\.sporades-host-helper-payload-([a-f0-9]{64})\.mjs$/.exec(payloadName);
|
|
43484
43550
|
if (!match) throw helperError("Host helper payload pointer was invalid.", "Retry the Host helper upgrade.");
|
|
43485
|
-
const payload =
|
|
43486
|
-
const actual = createHash("sha256").update(await
|
|
43551
|
+
const payload = path4.join(directory, payloadName);
|
|
43552
|
+
const actual = createHash("sha256").update(await readFile3(payload)).digest("hex");
|
|
43487
43553
|
if (actual !== match[1]) throw helperError("Host helper payload checksum did not match.", "Retry the Host helper upgrade.");
|
|
43488
43554
|
}
|
|
43489
43555
|
async function drainUncooperativeHostHelpers(target) {
|
|
@@ -43553,11 +43619,11 @@ async function findUncooperativeHostHelperProcesses(target, deadline) {
|
|
|
43553
43619
|
);
|
|
43554
43620
|
}
|
|
43555
43621
|
try {
|
|
43556
|
-
const processDir =
|
|
43557
|
-
const argv = (await
|
|
43622
|
+
const processDir = path4.join(procRoot, entry.name);
|
|
43623
|
+
const argv = (await readFile3(path4.join(processDir, "cmdline"))).toString("utf8").split("\0").filter(Boolean);
|
|
43558
43624
|
assertHostHelperDrainDeadline(deadline);
|
|
43559
43625
|
if (argv[1] !== target) continue;
|
|
43560
|
-
const environment = (await
|
|
43626
|
+
const environment = (await readFile3(path4.join(processDir, "environ"))).toString("utf8").split("\0");
|
|
43561
43627
|
assertHostHelperDrainDeadline(deadline);
|
|
43562
43628
|
if (environment.includes("SPORADES_HOST_DISPATCH_LOCK_HELD=1")) continue;
|
|
43563
43629
|
active.push(Number(entry.name));
|
|
@@ -43614,7 +43680,7 @@ async function runManagedRouteActionProcess(request, input, lockIdentity) {
|
|
|
43614
43680
|
process.argv[1]
|
|
43615
43681
|
] : [process.execPath, process.argv[1]];
|
|
43616
43682
|
const result = spawnSync2(flock, [
|
|
43617
|
-
lockIdentity.routeLockFile ? "--shared" : "--exclusive",
|
|
43683
|
+
lockIdentity.routeLockFile && request.action !== "capsule.register" ? "--shared" : "--exclusive",
|
|
43618
43684
|
"--timeout",
|
|
43619
43685
|
String(timeoutMs / 1e3),
|
|
43620
43686
|
"--conflict-exit-code",
|
|
@@ -43689,7 +43755,7 @@ function managedRouteMutationLockIdentity(request) {
|
|
|
43689
43755
|
const domainDirectory = canonicalManagedRouteDomainDirectory(request, remoteRoot2);
|
|
43690
43756
|
const bootstrapTrust = bootstrapTrustManifest(request);
|
|
43691
43757
|
return {
|
|
43692
|
-
globalLockFile:
|
|
43758
|
+
globalLockFile: path4.join(remoteRoot2, "bin", ".sporades-host-helper.host-route.lock"),
|
|
43693
43759
|
routeLockFile: null,
|
|
43694
43760
|
domainDirectory,
|
|
43695
43761
|
bootstrapTrust
|
|
@@ -43700,7 +43766,7 @@ function managedRouteMutationLockIdentity(request) {
|
|
|
43700
43766
|
}
|
|
43701
43767
|
const remoteRoot = validateCanonicalHostRouteRoot(request);
|
|
43702
43768
|
return {
|
|
43703
|
-
globalLockFile:
|
|
43769
|
+
globalLockFile: path4.join(remoteRoot, "bin", ".sporades-host-helper.host-route.lock"),
|
|
43704
43770
|
routeLockFile: `${canonicalManagedRouteFile(request, remoteRoot)}.lock`,
|
|
43705
43771
|
domainDirectory: canonicalManagedRouteDomainDirectory(request, remoteRoot),
|
|
43706
43772
|
...actionCanProvisionCapsuleHttpLog(request.action) ? { routeLogTrust: capsuleHttpLogTrustManifest(request, remoteRoot) } : {}
|
|
@@ -43711,7 +43777,7 @@ function actionCanProvisionCapsuleHttpLog(action) {
|
|
|
43711
43777
|
}
|
|
43712
43778
|
function validateCanonicalHostRouteRoot(request) {
|
|
43713
43779
|
const remoteRoot = request.host.remoteRoot;
|
|
43714
|
-
if (typeof remoteRoot !== "string" || remoteRoot.length < 2 || remoteRoot.length > 4096 || !
|
|
43780
|
+
if (typeof remoteRoot !== "string" || remoteRoot.length < 2 || remoteRoot.length > 4096 || !path4.isAbsolute(remoteRoot) || /[\0\r\n]/.test(remoteRoot) || path4.normalize(remoteRoot) !== remoteRoot || remoteRoot === path4.parse(remoteRoot).root) {
|
|
43715
43781
|
throw helperError("Invalid Hosted Capsule route identity.", "Use a bounded absolute canonical Host remote root and retry.");
|
|
43716
43782
|
}
|
|
43717
43783
|
const domain = request.host.domain;
|
|
@@ -43730,22 +43796,22 @@ function canonicalManagedRouteFile(request, validatedRemoteRoot = validateCanoni
|
|
|
43730
43796
|
throw helperError("Invalid Hosted Capsule route identity.", "Use a canonical DNS-safe Capsule subname and retry.");
|
|
43731
43797
|
}
|
|
43732
43798
|
const domainDirectory = canonicalManagedRouteDomainDirectory(request, validatedRemoteRoot);
|
|
43733
|
-
const routeFile =
|
|
43734
|
-
if (
|
|
43799
|
+
const routeFile = path4.resolve(domainDirectory, `${subname}.caddy`);
|
|
43800
|
+
if (path4.dirname(routeFile) !== domainDirectory) {
|
|
43735
43801
|
throw helperError("Invalid Hosted Capsule route identity.", "Use a route identity within the configured Hosted domain directory and retry.");
|
|
43736
43802
|
}
|
|
43737
43803
|
return routeFile;
|
|
43738
43804
|
}
|
|
43739
43805
|
function canonicalManagedRouteDomainDirectory(request, validatedRemoteRoot = validateCanonicalHostRouteRoot(request)) {
|
|
43740
|
-
const hostsDirectory =
|
|
43741
|
-
const domainDirectory =
|
|
43742
|
-
if (
|
|
43806
|
+
const hostsDirectory = path4.resolve(validatedRemoteRoot, "caddy", "hosts");
|
|
43807
|
+
const domainDirectory = path4.resolve(hostsDirectory, request.host.domain);
|
|
43808
|
+
if (path4.dirname(domainDirectory) !== hostsDirectory) {
|
|
43743
43809
|
throw helperError("Invalid Hosted Capsule route identity.", "Use a route identity within the configured Hosted domain directory and retry.");
|
|
43744
43810
|
}
|
|
43745
43811
|
return domainDirectory;
|
|
43746
43812
|
}
|
|
43747
43813
|
async function captureManagedRouteTrust(lockIdentity, createMissing) {
|
|
43748
|
-
const remoteRoot =
|
|
43814
|
+
const remoteRoot = path4.dirname(path4.dirname(lockIdentity.globalLockFile));
|
|
43749
43815
|
const canonicalRemoteRoot = await canonicalTrustedTarget(remoteRoot);
|
|
43750
43816
|
const directoryIdentities = /* @__PURE__ */ new Map();
|
|
43751
43817
|
const captureDirectory = async (directory, caddyOwned = false) => {
|
|
@@ -43755,18 +43821,18 @@ async function captureManagedRouteTrust(lockIdentity, createMissing) {
|
|
|
43755
43821
|
}
|
|
43756
43822
|
};
|
|
43757
43823
|
await captureDirectory(remoteRoot);
|
|
43758
|
-
await captureDirectory(
|
|
43824
|
+
await captureDirectory(path4.join(canonicalRemoteRoot, "bin"));
|
|
43759
43825
|
let routeFile = null;
|
|
43760
43826
|
if (lockIdentity.routeLockFile) {
|
|
43761
43827
|
routeFile = lockIdentity.routeLockFile.slice(0, -5);
|
|
43762
|
-
const domainDirectory =
|
|
43763
|
-
await captureDirectory(
|
|
43764
|
-
await captureDirectory(
|
|
43828
|
+
const domainDirectory = path4.dirname(routeFile);
|
|
43829
|
+
await captureDirectory(path4.join(canonicalRemoteRoot, "caddy"));
|
|
43830
|
+
await captureDirectory(path4.join(canonicalRemoteRoot, "caddy", "hosts"));
|
|
43765
43831
|
await captureDirectory(await canonicalTrustedTarget(domainDirectory));
|
|
43766
43832
|
await assertTrustedRegularFileIfExists(routeFile);
|
|
43767
43833
|
} else {
|
|
43768
|
-
await captureDirectory(
|
|
43769
|
-
await captureDirectory(
|
|
43834
|
+
await captureDirectory(path4.join(canonicalRemoteRoot, "caddy"));
|
|
43835
|
+
await captureDirectory(path4.join(canonicalRemoteRoot, "caddy", "hosts"));
|
|
43770
43836
|
await captureDirectory(await canonicalTrustedTarget(lockIdentity.domainDirectory));
|
|
43771
43837
|
for (const directory of lockIdentity.bootstrapTrust?.directories ?? []) await captureDirectory(directory.path, directory.caddyOwned === true);
|
|
43772
43838
|
}
|
|
@@ -43790,17 +43856,17 @@ async function captureManagedRouteTrust(lockIdentity, createMissing) {
|
|
|
43790
43856
|
return { directories: [...directoryIdentities.values()], managedRoot: canonicalRemoteRoot, finalFiles, globalLockFile: lockIdentity.globalLockFile, routeLockFile: lockIdentity.routeLockFile, routeFile };
|
|
43791
43857
|
}
|
|
43792
43858
|
async function canonicalTrustedTarget(target) {
|
|
43793
|
-
if (!
|
|
43859
|
+
if (!path4.isAbsolute(target) || path4.normalize(target) !== target) throw routeTrustError();
|
|
43794
43860
|
return target;
|
|
43795
43861
|
}
|
|
43796
43862
|
async function trustedDirectoryChain(target, createMissing, managedRoot, targetOwner) {
|
|
43797
43863
|
const canonicalTarget = await canonicalTrustedTarget(target);
|
|
43798
|
-
const parsed =
|
|
43799
|
-
const components = canonicalTarget.slice(parsed.root.length).split(
|
|
43864
|
+
const parsed = path4.parse(canonicalTarget);
|
|
43865
|
+
const components = canonicalTarget.slice(parsed.root.length).split(path4.sep).filter(Boolean);
|
|
43800
43866
|
const identities = [];
|
|
43801
43867
|
let current2 = parsed.root;
|
|
43802
43868
|
for (const component of [null, ...components]) {
|
|
43803
|
-
if (component !== null) current2 =
|
|
43869
|
+
if (component !== null) current2 = path4.join(current2, component);
|
|
43804
43870
|
let details;
|
|
43805
43871
|
try {
|
|
43806
43872
|
details = await lstat(current2);
|
|
@@ -43817,7 +43883,7 @@ async function trustedDirectoryChain(target, createMissing, managedRoot, targetO
|
|
|
43817
43883
|
throw routeTrustError();
|
|
43818
43884
|
}
|
|
43819
43885
|
}
|
|
43820
|
-
const managed = current2 === managedRoot || current2.startsWith(`${managedRoot}${
|
|
43886
|
+
const managed = current2 === managedRoot || current2.startsWith(`${managedRoot}${path4.sep}`);
|
|
43821
43887
|
const expectedOwner = current2 === canonicalTarget ? targetOwner : void 0;
|
|
43822
43888
|
if (!details.isDirectory() || details.isSymbolicLink() || !(expectedOwner ? trustedExactOwnerMetadata(details, expectedOwner) : managed ? trustedHostPathMetadata(details) : trustedAnchorPathMetadata(details))) throw routeTrustError();
|
|
43823
43889
|
identities.push({ path: current2, dev: details.dev, ino: details.ino, ...expectedOwner ? { expectedOwner } : {} });
|
|
@@ -43878,7 +43944,7 @@ async function assertActiveManagedRouteTrust(routeFile) {
|
|
|
43878
43944
|
} catch {
|
|
43879
43945
|
throw routeTrustError();
|
|
43880
43946
|
}
|
|
43881
|
-
const managed = expected.path === trust.managedRoot || expected.path.startsWith(`${trust.managedRoot}${
|
|
43947
|
+
const managed = expected.path === trust.managedRoot || expected.path.startsWith(`${trust.managedRoot}${path4.sep}`);
|
|
43882
43948
|
if (!current2.isDirectory() || current2.isSymbolicLink() || !(expected.expectedOwner ? trustedExactOwnerMetadata(current2, expected.expectedOwner) : managed ? trustedHostPathMetadata(current2) : trustedAnchorPathMetadata(current2)) || current2.dev !== expected.dev || current2.ino !== expected.ino) throw routeTrustError();
|
|
43883
43949
|
}
|
|
43884
43950
|
await assertTrustedRegularFileIfExists(trust.globalLockFile);
|
|
@@ -44049,6 +44115,7 @@ function versionHost(request) {
|
|
|
44049
44115
|
async function bootstrapHost(request) {
|
|
44050
44116
|
validateBootstrapRequest(request);
|
|
44051
44117
|
const bootstrap = normaliseBootstrap(request);
|
|
44118
|
+
await assertHostnamesAvailable(request.host.remoteRoot, [`host.${request.host.domain}`], `health:${request.host.domain}`);
|
|
44052
44119
|
await ensureBootstrapDirectories(bootstrap);
|
|
44053
44120
|
await validateBootstrapTls(request, bootstrap);
|
|
44054
44121
|
const network = ensureDockerNetwork(bootstrap.network);
|
|
@@ -44073,26 +44140,60 @@ async function bootstrapHost(request) {
|
|
|
44073
44140
|
}
|
|
44074
44141
|
async function registerCapsule(request) {
|
|
44075
44142
|
validateRegisterRequest(request);
|
|
44076
|
-
const
|
|
44143
|
+
const claimPath = path4.join(request.host.remoteRoot, "hosts", request.host.domain, "registry", "registration-claims", `${request.capsule.subname}.json`);
|
|
44144
|
+
const pendingClaim = await readFile3(claimPath, "utf8").then((contents) => JSON.parse(contents)).catch((error) => {
|
|
44145
|
+
if (errorDetails(error).code === "ENOENT") return null;
|
|
44146
|
+
throw error;
|
|
44147
|
+
});
|
|
44148
|
+
if (pendingClaim) {
|
|
44149
|
+
const aliases = validateAliasDomains(pendingClaim.aliasDomains);
|
|
44150
|
+
if (pendingClaim.domain !== request.host.domain || pendingClaim.subname !== request.capsule.subname || request.registration?.aliasDomains !== void 0 && !isDeepStrictEqual([...request.registration.aliasDomains].sort(), [...aliases].sort())) {
|
|
44151
|
+
throw helperError("Hosted Capsule registration recovery is required.", "Repair Caddy and retry the original registration with the same alias domains before changing its aliases.");
|
|
44152
|
+
}
|
|
44153
|
+
request = { ...request, registration: { ...request.registration, aliasDomains: aliases } };
|
|
44154
|
+
}
|
|
44155
|
+
let registration = normaliseRegistration(request);
|
|
44077
44156
|
await ensureHostedDomainBootstrapped(request, registration);
|
|
44078
44157
|
let reactivated = false;
|
|
44079
44158
|
let sealedServerEnv = null;
|
|
44080
44159
|
let priorRuntime = null;
|
|
44081
44160
|
let admissionError = null;
|
|
44161
|
+
let registrationCommitted = false;
|
|
44162
|
+
const recoveryErrors = [];
|
|
44163
|
+
let priorRoute = null;
|
|
44164
|
+
let routeAttempted = false;
|
|
44165
|
+
let claimWritten = Boolean(pendingClaim);
|
|
44166
|
+
const reserveClaim = async (previous = null) => {
|
|
44167
|
+
if (claimWritten) return;
|
|
44168
|
+
await mkdir(path4.dirname(claimPath), { recursive: true });
|
|
44169
|
+
await publishHostHelperBytes(Buffer.from(JSON.stringify({
|
|
44170
|
+
domain: registration.domain,
|
|
44171
|
+
subname: registration.subname,
|
|
44172
|
+
aliasDomains: registration.aliasDomains,
|
|
44173
|
+
previousAliasDomains: validateAliasDomains(previous?.aliasDomains)
|
|
44174
|
+
})), claimPath, 384);
|
|
44175
|
+
claimWritten = true;
|
|
44176
|
+
};
|
|
44082
44177
|
try {
|
|
44083
|
-
await mkdir(
|
|
44178
|
+
await mkdir(path4.dirname(registryLockPath(request)), { recursive: true });
|
|
44084
44179
|
await withRegistryLock(request, async () => {
|
|
44085
44180
|
if (await pathExists(registration.registryRecord)) {
|
|
44086
44181
|
const existing = await readRegistryRecordForCapsule(request, "register");
|
|
44087
44182
|
assertRegistryRecordMatchesRequest(request, existing);
|
|
44088
|
-
if (existing.status === "unregistered") {
|
|
44183
|
+
if (existing.status === "unregistered" || pendingClaim) {
|
|
44184
|
+
registration = normaliseRegistration(request, existing);
|
|
44185
|
+
await assertHostnamesAvailable(request.host.remoteRoot, [registration.route.hostname, ...registration.aliasDomains], registration.remoteCapsuleId);
|
|
44186
|
+
priorRoute = await captureReleaseInstallRoute(request, existing);
|
|
44187
|
+
await reserveClaim(existing);
|
|
44089
44188
|
priorRuntime = captureCapsuleRuntimeSettlement(request, existing);
|
|
44090
44189
|
quiesceCapsuleRuntime(priorRuntime);
|
|
44091
|
-
await mkdir(
|
|
44190
|
+
await mkdir(path4.dirname(registration.registryRecord), { recursive: true });
|
|
44092
44191
|
await mkdir(registration.directories.releases, { recursive: true });
|
|
44192
|
+
routeAttempted = true;
|
|
44093
44193
|
await writeUnavailableRoute(registration.lifecycle);
|
|
44094
44194
|
sealedServerEnv = await ensureHostSealedEnvKeyPair(registration, existing);
|
|
44095
|
-
await writeRegistryRecordAtomic(registration.registryRecord, reactivateRegistrationRecord(existing, sealedServerEnv));
|
|
44195
|
+
await writeRegistryRecordAtomic(registration.registryRecord, { ...reactivateRegistrationRecord(existing, sealedServerEnv), aliasDomains: registration.aliasDomains, route: { tls: registration.route.tls } });
|
|
44196
|
+
registrationCommitted = true;
|
|
44096
44197
|
reactivated = true;
|
|
44097
44198
|
return;
|
|
44098
44199
|
}
|
|
@@ -44101,17 +44202,50 @@ async function registerCapsule(request) {
|
|
|
44101
44202
|
`Choose a different Capsule subname for ${request.host.domain}.`
|
|
44102
44203
|
);
|
|
44103
44204
|
}
|
|
44104
|
-
await
|
|
44205
|
+
await assertHostnamesAvailable(request.host.remoteRoot, [registration.route.hostname, ...registration.aliasDomains], registration.remoteCapsuleId);
|
|
44206
|
+
priorRoute = await captureReleaseInstallRoute(request, null);
|
|
44207
|
+
await reserveClaim();
|
|
44208
|
+
await mkdir(path4.dirname(registration.registryRecord), { recursive: true });
|
|
44105
44209
|
await mkdir(registration.directories.releases, { recursive: true });
|
|
44106
44210
|
await mkdir(registration.directories.logs, { recursive: true });
|
|
44211
|
+
routeAttempted = true;
|
|
44107
44212
|
await writeUnavailableRoute(registration.lifecycle);
|
|
44108
44213
|
sealedServerEnv = await ensureHostSealedEnvKeyPair(registration);
|
|
44109
44214
|
await writeRegistryRecordAtomic(registration.registryRecord, createRegistrationRecord(registration, sealedServerEnv));
|
|
44215
|
+
registrationCommitted = true;
|
|
44110
44216
|
});
|
|
44111
44217
|
} catch (error) {
|
|
44112
|
-
|
|
44218
|
+
if (registrationCommitted) {
|
|
44219
|
+
recoveryErrors.push(`Registry lock cleanup: ${errorDetails(error).message}`);
|
|
44220
|
+
} else {
|
|
44221
|
+
admissionError = error;
|
|
44222
|
+
}
|
|
44223
|
+
}
|
|
44224
|
+
if (admissionError && routeAttempted && priorRoute) {
|
|
44225
|
+
try {
|
|
44226
|
+
await restoreReleaseInstallRoute(priorRoute, true);
|
|
44227
|
+
} catch (error) {
|
|
44228
|
+
recoveryErrors.push(`Route rollback: ${errorDetails(error).message}`);
|
|
44229
|
+
}
|
|
44230
|
+
}
|
|
44231
|
+
try {
|
|
44232
|
+
await settleCapsuleRuntime(request, priorRuntime, admissionError);
|
|
44233
|
+
} catch (error) {
|
|
44234
|
+
recoveryErrors.push(`Runtime settlement: ${errorDetails(error).message}`);
|
|
44235
|
+
}
|
|
44236
|
+
if (claimWritten && recoveryErrors.length === 0 && (!admissionError || !pendingClaim)) {
|
|
44237
|
+
try {
|
|
44238
|
+
await rm(claimPath);
|
|
44239
|
+
} catch (error) {
|
|
44240
|
+
recoveryErrors.push(`Reservation cleanup: ${errorDetails(error).message}`);
|
|
44241
|
+
}
|
|
44242
|
+
}
|
|
44243
|
+
if (recoveryErrors.length) {
|
|
44244
|
+
throw helperError(
|
|
44245
|
+
"Hosted Capsule registration recovery failed.",
|
|
44246
|
+
`${admissionError ? `Registration: ${errorDetails(admissionError).message}` : "Registration committed."} ${recoveryErrors.join(" ")} Hostnames remain reserved; repair the Host services and retry the original registration with the same alias domains.`
|
|
44247
|
+
);
|
|
44113
44248
|
}
|
|
44114
|
-
await settleCapsuleRuntime(request, priorRuntime, admissionError);
|
|
44115
44249
|
if (admissionError) throw admissionError;
|
|
44116
44250
|
writeEnvelope({
|
|
44117
44251
|
ok: true,
|
|
@@ -44123,6 +44257,8 @@ async function registerCapsule(request) {
|
|
|
44123
44257
|
subname: registration.subname,
|
|
44124
44258
|
domain: registration.domain,
|
|
44125
44259
|
hostedUrl: registration.hostedUrl,
|
|
44260
|
+
aliasDomains: registration.aliasDomains,
|
|
44261
|
+
aliasUrls: registration.aliasDomains.map((hostname) => `https://${hostname}`),
|
|
44126
44262
|
remoteCapsuleId: registration.remoteCapsuleId
|
|
44127
44263
|
},
|
|
44128
44264
|
registryRecord: registration.registryRecord,
|
|
@@ -44135,7 +44271,7 @@ async function registerCapsule(request) {
|
|
|
44135
44271
|
}
|
|
44136
44272
|
async function rotateCapsuleSealedEnvKey(request) {
|
|
44137
44273
|
validateSealedEnvRotationRequest(request);
|
|
44138
|
-
await mkdir(
|
|
44274
|
+
await mkdir(path4.dirname(registryLockPath(request)), { recursive: true });
|
|
44139
44275
|
let data2;
|
|
44140
44276
|
let priorRuntime = null;
|
|
44141
44277
|
let rotationError = null;
|
|
@@ -44151,7 +44287,7 @@ async function rotateCapsuleSealedEnvKey(request) {
|
|
|
44151
44287
|
}
|
|
44152
44288
|
priorRuntime = captureCapsuleRuntimeSettlement(request, record);
|
|
44153
44289
|
quiesceCapsuleRuntime(priorRuntime);
|
|
44154
|
-
const dataDirectory =
|
|
44290
|
+
const dataDirectory = path4.join(request.host.remoteRoot, "hosts", request.host.domain, "capsules", request.capsule.subname, "data");
|
|
44155
44291
|
const previousPublicKeyFingerprint = record.sealedServerEnv?.currentKeyFingerprint ?? null;
|
|
44156
44292
|
const sealedServerEnv = await generateHostSealedEnvKeyPair(dataDirectory);
|
|
44157
44293
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -44192,10 +44328,24 @@ async function rotateCapsuleSealedEnvKey(request) {
|
|
|
44192
44328
|
if (rotationError) throw rotationError;
|
|
44193
44329
|
writeEnvelope({ ok: true, data: data2, error: null });
|
|
44194
44330
|
}
|
|
44331
|
+
async function assertRegistrationRecoveryComplete(request) {
|
|
44332
|
+
const claimPath = path4.join(request.host.remoteRoot, "hosts", request.host.domain, "registry", "registration-claims", `${request.capsule.subname}.json`);
|
|
44333
|
+
const pending = await lstat(claimPath).catch((error) => {
|
|
44334
|
+
if (errorDetails(error).code === "ENOENT") return null;
|
|
44335
|
+
throw error;
|
|
44336
|
+
});
|
|
44337
|
+
if (pending) {
|
|
44338
|
+
throw helperError(
|
|
44339
|
+
"Hosted Capsule registration recovery is required before teardown.",
|
|
44340
|
+
"Repair the reported Host services or storage problem and retry the original registration with the same alias domains before unregistering or deleting this Capsule."
|
|
44341
|
+
);
|
|
44342
|
+
}
|
|
44343
|
+
}
|
|
44195
44344
|
async function unregisterCapsule(request) {
|
|
44196
44345
|
validateUnregisterRequest(request);
|
|
44346
|
+
await assertRegistrationRecoveryComplete(request);
|
|
44197
44347
|
const unregister = normaliseUnregister(request);
|
|
44198
|
-
await mkdir(
|
|
44348
|
+
await mkdir(path4.dirname(registryLockPath(request)), { recursive: true });
|
|
44199
44349
|
let data2;
|
|
44200
44350
|
await withRegistryLock(request, async () => {
|
|
44201
44351
|
const record = await readRegistryRecordForCapsule(request, "unregister");
|
|
@@ -44231,8 +44381,9 @@ async function unregisterCapsule(request) {
|
|
|
44231
44381
|
}
|
|
44232
44382
|
async function deleteCapsule(request) {
|
|
44233
44383
|
validateDeleteRequest(request);
|
|
44384
|
+
await assertRegistrationRecoveryComplete(request);
|
|
44234
44385
|
const deletion = normaliseDeletion(request);
|
|
44235
|
-
await mkdir(
|
|
44386
|
+
await mkdir(path4.dirname(registryLockPath(request)), { recursive: true });
|
|
44236
44387
|
let data2;
|
|
44237
44388
|
await withRegistryLock(request, async () => {
|
|
44238
44389
|
const record = await readOptionalRegistryRecordForCapsule(request);
|
|
@@ -44284,7 +44435,7 @@ async function installRelease(request) {
|
|
|
44284
44435
|
async function installClaimedRelease(request, previousRecord, paths, claimedArchive) {
|
|
44285
44436
|
const release = request.release;
|
|
44286
44437
|
const previousCurrentRelease = previousRecord.currentRelease?.id ? { id: previousRecord.currentRelease.id } : null;
|
|
44287
|
-
const previousRegistryContents = await
|
|
44438
|
+
const previousRegistryContents = await readFile3(registryPath(request), "utf8");
|
|
44288
44439
|
const previousCurrentTarget = await readlink(paths.currentLink).catch((error) => {
|
|
44289
44440
|
if (errorDetails(error).code === "ENOENT") return null;
|
|
44290
44441
|
throw error;
|
|
@@ -44454,18 +44605,18 @@ async function installClaimedRelease(request, previousRecord, paths, claimedArch
|
|
|
44454
44605
|
writeEnvelope({ ok: true, data: data2, error: null });
|
|
44455
44606
|
}
|
|
44456
44607
|
async function claimReleaseArchive(request) {
|
|
44457
|
-
const expectedIncoming =
|
|
44458
|
-
if (
|
|
44608
|
+
const expectedIncoming = path4.join(request.host.remoteRoot, "incoming", `${request.release.id}.tar.gz`);
|
|
44609
|
+
if (path4.resolve(request.release.remoteArchive) !== path4.resolve(expectedIncoming)) {
|
|
44459
44610
|
throw helperError("Invalid release install request.", "Upload the release to the canonical Host incoming path and retry `sporades host push`.");
|
|
44460
44611
|
}
|
|
44461
|
-
const claimsDirectory =
|
|
44612
|
+
const claimsDirectory = path4.join(request.host.remoteRoot, ".release-claims");
|
|
44462
44613
|
await mkdir(claimsDirectory, { recursive: true, mode: 448 });
|
|
44463
44614
|
const claimsStats = await lstat(claimsDirectory);
|
|
44464
44615
|
if (!claimsStats.isDirectory() || claimsStats.isSymbolicLink() || typeof process.getuid === "function" && claimsStats.uid !== process.getuid()) {
|
|
44465
44616
|
throw helperError("Hosted Capsule release claim directory is unsafe.", "Repair Host helper ownership of the release claim directory and retry.");
|
|
44466
44617
|
}
|
|
44467
44618
|
await chmod(claimsDirectory, 448);
|
|
44468
|
-
const claimedPath =
|
|
44619
|
+
const claimedPath = path4.join(claimsDirectory, `${request.release.id}-${process.pid}-${randomBytes(16).toString("hex")}.tar.gz`);
|
|
44469
44620
|
await rename(request.release.remoteArchive, claimedPath);
|
|
44470
44621
|
try {
|
|
44471
44622
|
const stats = await lstat(claimedPath);
|
|
@@ -44500,15 +44651,15 @@ async function validateExtractedReleaseTree(root, expectedFiles) {
|
|
|
44500
44651
|
let totalBytes = 0;
|
|
44501
44652
|
const publicClaims = [];
|
|
44502
44653
|
async function visit(directory, prefix = "") {
|
|
44503
|
-
for (const entry of await
|
|
44654
|
+
for (const entry of await readdir2(directory, { withFileTypes: true })) {
|
|
44504
44655
|
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
44505
44656
|
const normalized = relative.normalize("NFC");
|
|
44506
|
-
const safe = relative.length > 0 && !relative.startsWith("/") && !relative.includes("\\") && !relative.includes("\0") &&
|
|
44657
|
+
const safe = relative.length > 0 && !relative.startsWith("/") && !relative.includes("\\") && !relative.includes("\0") && path4.posix.normalize(relative) === relative && Buffer.byteLength(relative, "utf8") <= HOST_RELEASE_ARCHIVE_LIMITS.pathBytes && relative.split("/").every((segment) => segment && segment !== "." && segment !== "..");
|
|
44507
44658
|
if (!safe || canonical.has(normalized)) {
|
|
44508
44659
|
throw helperError("Extracted Hosted Capsule release is unsafe.", "Upload a release with unique bounded relative paths.");
|
|
44509
44660
|
}
|
|
44510
44661
|
canonical.add(normalized);
|
|
44511
|
-
const entryPath =
|
|
44662
|
+
const entryPath = path4.join(directory, entry.name);
|
|
44512
44663
|
const stats = await lstat(entryPath);
|
|
44513
44664
|
if (stats.isSymbolicLink()) {
|
|
44514
44665
|
throw helperError("Extracted Hosted Capsule release is unsafe.", "Upload regular release files without symbolic links.");
|
|
@@ -44538,7 +44689,7 @@ async function validateExtractedReleaseTree(root, expectedFiles) {
|
|
|
44538
44689
|
}
|
|
44539
44690
|
publicClaims.push({ path: publicPath, size: stats.size });
|
|
44540
44691
|
}
|
|
44541
|
-
actual.push({ path: relative, size: stats.size, sha256: createHash("sha256").update(await
|
|
44692
|
+
actual.push({ path: relative, size: stats.size, sha256: createHash("sha256").update(await readFile3(entryPath)).digest("hex") });
|
|
44542
44693
|
}
|
|
44543
44694
|
}
|
|
44544
44695
|
await visit(root);
|
|
@@ -44679,7 +44830,7 @@ function readVerificationHealthTimeoutMs(request) {
|
|
|
44679
44830
|
return Math.min(value, 18e4);
|
|
44680
44831
|
}
|
|
44681
44832
|
async function routeVerifiedFailureToUnavailable(request, releaseId, message) {
|
|
44682
|
-
const lifecycle = normaliseLifecycle(request);
|
|
44833
|
+
const lifecycle = normaliseLifecycle(request, await readRegistryRecordForCapsule(request, "lifecycle"));
|
|
44683
44834
|
stopAndRemoveContainer(lifecycle.container.name);
|
|
44684
44835
|
try {
|
|
44685
44836
|
await writeUnavailableRoute(lifecycle);
|
|
@@ -44749,7 +44900,7 @@ async function restoreFailedReleaseAfterFallbackRestartFailure(request, failedRe
|
|
|
44749
44900
|
reason
|
|
44750
44901
|
);
|
|
44751
44902
|
try {
|
|
44752
|
-
await writeUnavailableRoute(normaliseLifecycle(request));
|
|
44903
|
+
await writeUnavailableRoute(normaliseLifecycle(request, await readRegistryRecordForCapsule(request, "lifecycle")));
|
|
44753
44904
|
} catch {
|
|
44754
44905
|
}
|
|
44755
44906
|
}
|
|
@@ -44985,7 +45136,7 @@ function releaseIncludesSealedServerEnvPrivateKey(release) {
|
|
|
44985
45136
|
return Boolean(release.sealedServerEnvIncluded && privateKey && privateKeyPath);
|
|
44986
45137
|
}
|
|
44987
45138
|
function releasePrivateKeyPath(paths, releaseId) {
|
|
44988
|
-
return
|
|
45139
|
+
return path4.join(paths.data, "sealed-server-env", "releases", `${releaseId}.private.pem`);
|
|
44989
45140
|
}
|
|
44990
45141
|
async function installSealedServerEnvPrivateKey(release, paths) {
|
|
44991
45142
|
if (!releaseIncludesSealedServerEnvPrivateKey(release)) return;
|
|
@@ -44993,9 +45144,9 @@ async function installSealedServerEnvPrivateKey(release, paths) {
|
|
|
44993
45144
|
const privateKeyPath = releasePrivateKeyPath(paths, release.id);
|
|
44994
45145
|
const dataHandle = await openCanonicalRuntimeDataDirectory(paths.data, true);
|
|
44995
45146
|
try {
|
|
44996
|
-
const rootHandle = await openOrCreateRuntimeDirectory(dataHandle,
|
|
45147
|
+
const rootHandle = await openOrCreateRuntimeDirectory(dataHandle, path4.join(paths.data, "sealed-server-env"));
|
|
44997
45148
|
try {
|
|
44998
|
-
const releasesHandle = await openOrCreateRuntimeDirectory(rootHandle,
|
|
45149
|
+
const releasesHandle = await openOrCreateRuntimeDirectory(rootHandle, path4.dirname(privateKeyPath));
|
|
44999
45150
|
try {
|
|
45000
45151
|
await publishRuntimeFile(releasesHandle, privateKeyPath, privateKey, 384, "release-private-key-publish");
|
|
45001
45152
|
} finally {
|
|
@@ -45013,11 +45164,11 @@ async function removeInstalledReleasePrivateKey(release, paths) {
|
|
|
45013
45164
|
const privateKeyPath = releasePrivateKeyPath(paths, release.id);
|
|
45014
45165
|
const dataHandle = await openCanonicalRuntimeDataDirectory(paths.data, false);
|
|
45015
45166
|
try {
|
|
45016
|
-
const rootHandle = await openOrCreateRuntimeDirectory(dataHandle,
|
|
45167
|
+
const rootHandle = await openOrCreateRuntimeDirectory(dataHandle, path4.join(paths.data, "sealed-server-env"));
|
|
45017
45168
|
try {
|
|
45018
|
-
const releasesHandle = await openOrCreateRuntimeDirectory(rootHandle,
|
|
45169
|
+
const releasesHandle = await openOrCreateRuntimeDirectory(rootHandle, path4.dirname(privateKeyPath));
|
|
45019
45170
|
try {
|
|
45020
|
-
const descriptorPath = descriptorChildPath(releasesHandle.fd,
|
|
45171
|
+
const descriptorPath = descriptorChildPath(releasesHandle.fd, path4.basename(privateKeyPath), privateKeyPath);
|
|
45021
45172
|
const retained = await open(descriptorPath, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW).catch((error) => {
|
|
45022
45173
|
if (errorDetails(error).code === "ENOENT") return null;
|
|
45023
45174
|
throw runtimeDataTrustError(privateKeyPath);
|
|
@@ -45044,7 +45195,7 @@ async function removeInstalledReleasePrivateKey(release, paths) {
|
|
|
45044
45195
|
async function captureReleaseInstallRoute(request, previousRecord) {
|
|
45045
45196
|
const lifecycle = normaliseLifecycle(request, previousRecord, { ignoreProvidedLifecycle: true });
|
|
45046
45197
|
const routeFile = lifecycle.routes.running.routeFile;
|
|
45047
|
-
const contents = await
|
|
45198
|
+
const contents = await readFile3(routeFile, "utf8").catch((error) => {
|
|
45048
45199
|
if (errorDetails(error).code === "ENOENT") return null;
|
|
45049
45200
|
throw error;
|
|
45050
45201
|
});
|
|
@@ -45060,13 +45211,14 @@ async function restoreCurrentReleasePointerTarget(currentLink, previousTarget) {
|
|
|
45060
45211
|
await symlink(previousTarget, temporary);
|
|
45061
45212
|
await rename(temporary, currentLink);
|
|
45062
45213
|
}
|
|
45063
|
-
async function restoreReleaseInstallRoute(snapshot) {
|
|
45214
|
+
async function restoreReleaseInstallRoute(snapshot, reloadWhenAbsent = false) {
|
|
45064
45215
|
if (snapshot.contents !== null) {
|
|
45065
45216
|
await applyManagedRoute(snapshot.lifecycle, snapshot.routeFile, snapshot.contents);
|
|
45066
45217
|
return;
|
|
45067
45218
|
}
|
|
45068
45219
|
await withManagedRouteLock(snapshot.routeFile, async () => {
|
|
45069
45220
|
const removed = await removeManagedRouteLocked(snapshot.lifecycle, snapshot.routeFile);
|
|
45221
|
+
if (reloadWhenAbsent && !removed.removed) reloadCaddy(snapshot.lifecycle);
|
|
45070
45222
|
await finalizeRemovedRouteLocked(removed);
|
|
45071
45223
|
});
|
|
45072
45224
|
}
|
|
@@ -45120,11 +45272,11 @@ function validateSealedServerEnvPrivateKeyPath(release, paths) {
|
|
|
45120
45272
|
);
|
|
45121
45273
|
}
|
|
45122
45274
|
const compatiblePrivateKeyPaths = /* @__PURE__ */ new Set([
|
|
45123
|
-
|
|
45124
|
-
|
|
45125
|
-
...fingerprint ? [
|
|
45275
|
+
path4.resolve(releasePrivateKeyPath(paths, release.id)),
|
|
45276
|
+
path4.resolve(path4.join(paths.data, "sealed-server-env", "server-env.private.pem")),
|
|
45277
|
+
...fingerprint ? [path4.resolve(path4.join(paths.data, "sealed-server-env", "keys", `${fingerprint}.private.pem`))] : []
|
|
45126
45278
|
]);
|
|
45127
|
-
if (typeof privateKeyPath !== "string" || !compatiblePrivateKeyPaths.has(
|
|
45279
|
+
if (typeof privateKeyPath !== "string" || !compatiblePrivateKeyPaths.has(path4.resolve(privateKeyPath))) {
|
|
45128
45280
|
throw helperError(
|
|
45129
45281
|
"Invalid Sealed Server env private key path.",
|
|
45130
45282
|
"Update the Sporades CLI and retry `sporades host push`."
|
|
@@ -45343,7 +45495,7 @@ async function routeRuntimeExhaustionToUnavailable(request, record, health) {
|
|
|
45343
45495
|
async function stopCapsule(request, options = {}) {
|
|
45344
45496
|
validateLifecycleRequest(request);
|
|
45345
45497
|
await verifyRegisteredCapsule(request, "lifecycle");
|
|
45346
|
-
const lifecycle = normaliseLifecycle(request);
|
|
45498
|
+
const lifecycle = normaliseLifecycle(request, await readRegistryRecordForCapsule(request, "lifecycle"));
|
|
45347
45499
|
stopAndRemoveContainer(lifecycle.container.name);
|
|
45348
45500
|
await writeUnavailableRoute(lifecycle);
|
|
45349
45501
|
await updateRegistryStatus(request, "stopped");
|
|
@@ -45518,6 +45670,7 @@ async function listReleases(request) {
|
|
|
45518
45670
|
subname: record.subname,
|
|
45519
45671
|
domain: record.domain,
|
|
45520
45672
|
hostedUrl: record.hostedUrl ?? `${request.host.scheme ?? "https"}://${record.subname}.${request.host.domain}`,
|
|
45673
|
+
aliasDomains: validateAliasDomains(record.aliasDomains),
|
|
45521
45674
|
remoteCapsuleId: record.remoteCapsuleId ?? `${request.host.domain}/${record.subname}`
|
|
45522
45675
|
},
|
|
45523
45676
|
currentRelease: publicCurrentRelease(record),
|
|
@@ -45586,7 +45739,7 @@ async function rollbackRelease(request) {
|
|
|
45586
45739
|
return;
|
|
45587
45740
|
}
|
|
45588
45741
|
try {
|
|
45589
|
-
await writeUnavailableRoute(normaliseLifecycle(request));
|
|
45742
|
+
await writeUnavailableRoute(normaliseLifecycle(request, await readRegistryRecordForCapsule(request, "lifecycle")));
|
|
45590
45743
|
} catch (error) {
|
|
45591
45744
|
restartError = restartError ?? error;
|
|
45592
45745
|
}
|
|
@@ -45665,6 +45818,7 @@ async function listCapsules(request) {
|
|
|
45665
45818
|
subname: record.subname,
|
|
45666
45819
|
domain: record.domain,
|
|
45667
45820
|
hostedUrl: record.hostedUrl ?? `${request.host.scheme ?? "https"}://${record.subname}.${request.host.domain}`,
|
|
45821
|
+
aliasDomains: validateAliasDomains(record.aliasDomains),
|
|
45668
45822
|
registry: {
|
|
45669
45823
|
remoteCapsuleId: record.remoteCapsuleId ?? `${request.host.domain}/${record.subname}`,
|
|
45670
45824
|
createdAt: record.createdAt ?? null,
|
|
@@ -45709,7 +45863,7 @@ function createUnregisterResult(request, unregister, record, idempotent, route =
|
|
|
45709
45863
|
registryRecord: unregister.registryRecord,
|
|
45710
45864
|
directories: unregister.directories,
|
|
45711
45865
|
preserved: {
|
|
45712
|
-
releases: record.currentRelease?.id ?
|
|
45866
|
+
releases: record.currentRelease?.id ? path4.join(unregister.directories.releases, record.currentRelease.id) : unregister.directories.releases,
|
|
45713
45867
|
data: unregister.directories.data
|
|
45714
45868
|
},
|
|
45715
45869
|
deleteAfter: record.deleteAfter ?? null,
|
|
@@ -45758,28 +45912,28 @@ function createDeleteResult(request, deletion, removals) {
|
|
|
45758
45912
|
};
|
|
45759
45913
|
}
|
|
45760
45914
|
function canonicalReleasePaths(request) {
|
|
45761
|
-
const capsule =
|
|
45915
|
+
const capsule = path4.join(
|
|
45762
45916
|
request.host.remoteRoot,
|
|
45763
45917
|
"hosts",
|
|
45764
45918
|
request.host.domain,
|
|
45765
45919
|
"capsules",
|
|
45766
45920
|
request.capsule.subname
|
|
45767
45921
|
);
|
|
45768
|
-
const releases =
|
|
45922
|
+
const releases = path4.join(capsule, "releases");
|
|
45769
45923
|
return {
|
|
45770
45924
|
capsule,
|
|
45771
45925
|
releases,
|
|
45772
|
-
release: request.release?.id ?
|
|
45773
|
-
data:
|
|
45774
|
-
logs:
|
|
45775
|
-
currentLink:
|
|
45926
|
+
release: request.release?.id ? path4.join(releases, request.release.id) : null,
|
|
45927
|
+
data: path4.join(capsule, "data"),
|
|
45928
|
+
logs: path4.join(capsule, "logs"),
|
|
45929
|
+
currentLink: path4.join(capsule, "current")
|
|
45776
45930
|
};
|
|
45777
45931
|
}
|
|
45778
45932
|
function canonicalRollbackPaths(request, releaseId) {
|
|
45779
45933
|
const paths = canonicalReleasePaths({ ...request, release: { id: releaseId, remoteArchive: "", files: [] } });
|
|
45780
45934
|
return {
|
|
45781
45935
|
...paths,
|
|
45782
|
-
release:
|
|
45936
|
+
release: path4.join(paths.releases, releaseId)
|
|
45783
45937
|
};
|
|
45784
45938
|
}
|
|
45785
45939
|
function normaliseLifecycle(request, registryRecord = null, options = {}) {
|
|
@@ -45806,12 +45960,12 @@ function normaliseLifecycle(request, registryRecord = null, options = {}) {
|
|
|
45806
45960
|
};
|
|
45807
45961
|
const defaultMounts = {
|
|
45808
45962
|
files: [
|
|
45809
|
-
{ host:
|
|
45810
|
-
{ host:
|
|
45811
|
-
{ host:
|
|
45812
|
-
{ host:
|
|
45963
|
+
{ host: path4.join(currentLink, "server.mjs"), container: "/app/server.mjs", mode: "ro" },
|
|
45964
|
+
{ host: path4.join(currentLink, "public"), container: "/app/public", mode: "ro" },
|
|
45965
|
+
{ host: path4.join(currentLink, "sporades.json"), container: "/app/sporades.json", mode: "ro" },
|
|
45966
|
+
{ host: path4.join(currentLink, ".env.sporades.server"), container: "/app/.env.sporades.server", mode: "ro", optional: true },
|
|
45813
45967
|
{
|
|
45814
|
-
host:
|
|
45968
|
+
host: path4.join(currentLink, ".sporades", "sealed-server-env", "server-env.sealed.json"),
|
|
45815
45969
|
container: "/app/.sporades/sealed-server-env/server-env.sealed.json",
|
|
45816
45970
|
mode: "ro",
|
|
45817
45971
|
optional: true
|
|
@@ -45863,9 +46017,11 @@ function normaliseLifecycle(request, registryRecord = null, options = {}) {
|
|
|
45863
46017
|
...baseImageLabels(helperPackageBaseImage.updatePolicy.mode)
|
|
45864
46018
|
}
|
|
45865
46019
|
};
|
|
46020
|
+
const aliasDomains = validateAliasDomains(registryRecord?.aliasDomains);
|
|
45866
46021
|
const canonicalRoutes = {
|
|
45867
46022
|
running: {
|
|
45868
46023
|
hostname: `${subname}.${domain}`,
|
|
46024
|
+
...aliasDomains.length ? { aliasDomains } : {},
|
|
45869
46025
|
target: "container",
|
|
45870
46026
|
containerName,
|
|
45871
46027
|
port: 4e3,
|
|
@@ -45875,6 +46031,7 @@ function normaliseLifecycle(request, registryRecord = null, options = {}) {
|
|
|
45875
46031
|
},
|
|
45876
46032
|
unavailable: {
|
|
45877
46033
|
hostname: `${subname}.${domain}`,
|
|
46034
|
+
...aliasDomains.length ? { aliasDomains } : {},
|
|
45878
46035
|
target: "hosted-capsule-unavailable",
|
|
45879
46036
|
statusCode: 503,
|
|
45880
46037
|
routeFile,
|
|
@@ -45923,12 +46080,12 @@ function canonicalLifecycleRouteTls(request, registryRecord, provided) {
|
|
|
45923
46080
|
if (!(/* @__PURE__ */ new Set(["automatic", "cloudflare-origin"])).has(mode) || suppliedModes.some((value) => value !== mode)) {
|
|
45924
46081
|
throw invalidLifecycleAuthorityError();
|
|
45925
46082
|
}
|
|
45926
|
-
const directory =
|
|
46083
|
+
const directory = path4.join(request.host.remoteRoot, "hosts", request.host.domain, "tls");
|
|
45927
46084
|
return {
|
|
45928
46085
|
mode,
|
|
45929
46086
|
directory,
|
|
45930
|
-
certificate: mode === "cloudflare-origin" ?
|
|
45931
|
-
key: mode === "cloudflare-origin" ?
|
|
46087
|
+
certificate: mode === "cloudflare-origin" ? path4.join(directory, "origin.crt") : null,
|
|
46088
|
+
key: mode === "cloudflare-origin" ? path4.join(directory, "origin.key") : null
|
|
45932
46089
|
};
|
|
45933
46090
|
}
|
|
45934
46091
|
function invalidLifecycleAuthorityError() {
|
|
@@ -45971,14 +46128,14 @@ function assertCanonicalLifecycleAuthority(provided, canonical) {
|
|
|
45971
46128
|
const allowedByContainer = new Map(canonical.defaultMounts.files.map((mount) => [mount.container, mount]));
|
|
45972
46129
|
const privateKeyContainerPath = "/app/.sporades/sealed-server-env/server-env.private.pem";
|
|
45973
46130
|
const legacySealedServerEnvPrivateKeyMount = {
|
|
45974
|
-
host:
|
|
46131
|
+
host: path4.join(canonical.paths.data, "sealed-server-env", "server-env.private.pem"),
|
|
45975
46132
|
container: privateKeyContainerPath,
|
|
45976
46133
|
mode: "ro",
|
|
45977
46134
|
optional: true
|
|
45978
46135
|
};
|
|
45979
46136
|
const legacyAllowed = [
|
|
45980
|
-
{ host:
|
|
45981
|
-
{ host:
|
|
46137
|
+
{ host: path4.join(canonical.currentLink, "client.js"), container: "/app/client.js", mode: "ro" },
|
|
46138
|
+
{ host: path4.join(canonical.currentLink, "index.html"), container: "/app/index.html", mode: "ro" }
|
|
45982
46139
|
];
|
|
45983
46140
|
for (const mount of legacyAllowed) allowedByContainer.set(mount.container, mount);
|
|
45984
46141
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -46260,10 +46417,10 @@ function inspectContainerLifecycle(containerName) {
|
|
|
46260
46417
|
};
|
|
46261
46418
|
}
|
|
46262
46419
|
async function readCapsuleRegistryRecords(request) {
|
|
46263
|
-
const registryDirectory =
|
|
46420
|
+
const registryDirectory = path4.join(request.host.remoteRoot, "hosts", request.host.domain, "registry", "capsules");
|
|
46264
46421
|
let entries;
|
|
46265
46422
|
try {
|
|
46266
|
-
entries = await
|
|
46423
|
+
entries = await readdir2(registryDirectory, { withFileTypes: true });
|
|
46267
46424
|
} catch (error) {
|
|
46268
46425
|
if (errorDetails(error).code === "ENOENT") {
|
|
46269
46426
|
return [];
|
|
@@ -46273,10 +46430,10 @@ async function readCapsuleRegistryRecords(request) {
|
|
|
46273
46430
|
const records = [];
|
|
46274
46431
|
const files = entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => entry.name).sort();
|
|
46275
46432
|
for (const file of files) {
|
|
46276
|
-
const recordPath =
|
|
46433
|
+
const recordPath = path4.join(registryDirectory, file);
|
|
46277
46434
|
let record;
|
|
46278
46435
|
try {
|
|
46279
|
-
record = JSON.parse(await
|
|
46436
|
+
record = JSON.parse(await readFile3(recordPath, "utf8"));
|
|
46280
46437
|
} catch (error) {
|
|
46281
46438
|
if (error instanceof SyntaxError) {
|
|
46282
46439
|
throw helperError(
|
|
@@ -46404,19 +46561,21 @@ function normaliseProvidedBaseImage(value) {
|
|
|
46404
46561
|
version: provided.version ?? SPORADES_BASE_IMAGE.version
|
|
46405
46562
|
};
|
|
46406
46563
|
}
|
|
46407
|
-
function normaliseRegistration(request) {
|
|
46564
|
+
function normaliseRegistration(request, existing = null) {
|
|
46565
|
+
const aliasDomains = validateAliasDomains(request.registration?.aliasDomains ?? existing?.aliasDomains);
|
|
46408
46566
|
const subname = request.capsule.subname;
|
|
46409
46567
|
const domain = request.host.domain;
|
|
46410
46568
|
const remoteRoot = request.host.remoteRoot;
|
|
46411
46569
|
const scheme = request.host.scheme ?? "https";
|
|
46412
46570
|
const hostedUrl = `${scheme}://${subname}.${domain}`;
|
|
46413
46571
|
const remoteCapsuleId = `${domain}/${subname}`;
|
|
46414
|
-
const capsuleDirectory =
|
|
46415
|
-
const routeFile =
|
|
46572
|
+
const capsuleDirectory = path4.join(remoteRoot, "hosts", domain, "capsules", subname);
|
|
46573
|
+
const routeFile = path4.join(remoteRoot, "caddy", "hosts", domain, `${subname}.caddy`);
|
|
46416
46574
|
const routeTls = normaliseRegistrationTls(request);
|
|
46417
46575
|
const accessLog = canonicalCapsuleHttpLogPath(request, remoteRoot);
|
|
46418
46576
|
const route = {
|
|
46419
46577
|
hostname: `${subname}.${domain}`,
|
|
46578
|
+
...aliasDomains.length ? { aliasDomains } : {},
|
|
46420
46579
|
target: "hosted-capsule-unavailable",
|
|
46421
46580
|
statusCode: 503,
|
|
46422
46581
|
routeFile,
|
|
@@ -46429,12 +46588,13 @@ function normaliseRegistration(request) {
|
|
|
46429
46588
|
remoteRoot,
|
|
46430
46589
|
hostedUrl,
|
|
46431
46590
|
remoteCapsuleId,
|
|
46432
|
-
|
|
46591
|
+
aliasDomains,
|
|
46592
|
+
registryRecord: path4.join(remoteRoot, "hosts", domain, "registry", "capsules", `${subname}.json`),
|
|
46433
46593
|
directories: {
|
|
46434
46594
|
capsule: capsuleDirectory,
|
|
46435
|
-
releases:
|
|
46436
|
-
data:
|
|
46437
|
-
logs:
|
|
46595
|
+
releases: path4.join(capsuleDirectory, "releases"),
|
|
46596
|
+
data: path4.join(capsuleDirectory, "data"),
|
|
46597
|
+
logs: path4.join(capsuleDirectory, "logs")
|
|
46438
46598
|
},
|
|
46439
46599
|
baseImage: normaliseProvidedBaseImage(request.registration?.baseImage),
|
|
46440
46600
|
route,
|
|
@@ -46452,19 +46612,19 @@ function normaliseUnregister(request) {
|
|
|
46452
46612
|
const scheme = request.host.scheme ?? "https";
|
|
46453
46613
|
const hostedUrl = `${scheme}://${subname}.${domain}`;
|
|
46454
46614
|
const remoteCapsuleId = `${domain}/${subname}`;
|
|
46455
|
-
const capsuleDirectory =
|
|
46456
|
-
const routeFile =
|
|
46615
|
+
const capsuleDirectory = path4.join(remoteRoot, "hosts", domain, "capsules", subname);
|
|
46616
|
+
const routeFile = path4.join(remoteRoot, "caddy", "hosts", domain, `${subname}.caddy`);
|
|
46457
46617
|
const containerName = createHostedContainerName(domain, subname);
|
|
46458
46618
|
return {
|
|
46459
46619
|
subname,
|
|
46460
46620
|
domain,
|
|
46461
46621
|
hostedUrl,
|
|
46462
46622
|
remoteCapsuleId,
|
|
46463
|
-
registryRecord:
|
|
46623
|
+
registryRecord: path4.join(remoteRoot, "hosts", domain, "registry", "capsules", `${subname}.json`),
|
|
46464
46624
|
directories: {
|
|
46465
46625
|
capsule: capsuleDirectory,
|
|
46466
|
-
releases:
|
|
46467
|
-
data:
|
|
46626
|
+
releases: path4.join(capsuleDirectory, "releases"),
|
|
46627
|
+
data: path4.join(capsuleDirectory, "data")
|
|
46468
46628
|
},
|
|
46469
46629
|
container: {
|
|
46470
46630
|
name: containerName
|
|
@@ -46485,21 +46645,21 @@ function normaliseDeletion(request) {
|
|
|
46485
46645
|
const domain = request.host.domain;
|
|
46486
46646
|
const remoteRoot = request.host.remoteRoot;
|
|
46487
46647
|
const scheme = request.host.scheme ?? "https";
|
|
46488
|
-
const capsuleDirectory =
|
|
46648
|
+
const capsuleDirectory = path4.join(remoteRoot, "hosts", domain, "capsules", subname);
|
|
46489
46649
|
return {
|
|
46490
46650
|
subname,
|
|
46491
46651
|
domain,
|
|
46492
46652
|
hostedUrl: `${scheme}://${subname}.${domain}`,
|
|
46493
46653
|
remoteCapsuleId: `${domain}/${subname}`,
|
|
46494
|
-
registryRecord:
|
|
46654
|
+
registryRecord: path4.join(remoteRoot, "hosts", domain, "registry", "capsules", `${subname}.json`),
|
|
46495
46655
|
directories: {
|
|
46496
46656
|
capsule: capsuleDirectory,
|
|
46497
|
-
releases:
|
|
46498
|
-
data:
|
|
46657
|
+
releases: path4.join(capsuleDirectory, "releases"),
|
|
46658
|
+
data: path4.join(capsuleDirectory, "data")
|
|
46499
46659
|
},
|
|
46500
46660
|
route: {
|
|
46501
46661
|
hostname: `${subname}.${domain}`,
|
|
46502
|
-
routeFile:
|
|
46662
|
+
routeFile: path4.join(remoteRoot, "caddy", "hosts", domain, `${subname}.caddy`)
|
|
46503
46663
|
},
|
|
46504
46664
|
lifecycle: {
|
|
46505
46665
|
remoteRoot
|
|
@@ -46510,17 +46670,17 @@ function normaliseRegistrationTls(request) {
|
|
|
46510
46670
|
const remoteRoot = request.host.remoteRoot;
|
|
46511
46671
|
const domain = request.host.domain;
|
|
46512
46672
|
const tlsMode = request.registration?.bootstrap?.tls?.mode ?? request.bootstrap?.tls?.mode ?? "automatic";
|
|
46513
|
-
const tlsDirectory =
|
|
46673
|
+
const tlsDirectory = path4.join(remoteRoot, "hosts", domain, "tls");
|
|
46514
46674
|
return {
|
|
46515
46675
|
mode: tlsMode,
|
|
46516
46676
|
directory: tlsDirectory,
|
|
46517
|
-
certificate: tlsMode === "cloudflare-origin" ?
|
|
46518
|
-
key: tlsMode === "cloudflare-origin" ?
|
|
46677
|
+
certificate: tlsMode === "cloudflare-origin" ? path4.join(tlsDirectory, "origin.crt") : null,
|
|
46678
|
+
key: tlsMode === "cloudflare-origin" ? path4.join(tlsDirectory, "origin.key") : null
|
|
46519
46679
|
};
|
|
46520
46680
|
}
|
|
46521
46681
|
async function ensureHostedDomainBootstrapped(request, registration) {
|
|
46522
|
-
const caddyfile =
|
|
46523
|
-
const domainInclude =
|
|
46682
|
+
const caddyfile = path4.join(request.host.remoteRoot, "caddy", "Caddyfile");
|
|
46683
|
+
const domainInclude = path4.join(request.host.remoteRoot, "caddy", "hosts", `${request.host.domain}.caddy`);
|
|
46524
46684
|
const bootstrapped = await pathExists(caddyfile) && await pathExists(domainInclude);
|
|
46525
46685
|
if (bootstrapped) {
|
|
46526
46686
|
return;
|
|
@@ -46539,6 +46699,7 @@ function createRegistrationRecord(registration, sealedServerEnv = null) {
|
|
|
46539
46699
|
domain: registration.domain,
|
|
46540
46700
|
remoteCapsuleId: registration.remoteCapsuleId,
|
|
46541
46701
|
hostedUrl: registration.hostedUrl,
|
|
46702
|
+
aliasDomains: registration.aliasDomains,
|
|
46542
46703
|
status: "registered",
|
|
46543
46704
|
createdAt: now,
|
|
46544
46705
|
updatedAt: now,
|
|
@@ -46598,7 +46759,7 @@ async function generateHostSealedEnvKeyPair(dataDirectory) {
|
|
|
46598
46759
|
};
|
|
46599
46760
|
}
|
|
46600
46761
|
async function openOrCreateRuntimeDirectory(parentHandle, targetPath) {
|
|
46601
|
-
const descriptorPath = descriptorChildPath(parentHandle.fd,
|
|
46762
|
+
const descriptorPath = descriptorChildPath(parentHandle.fd, path4.basename(targetPath), targetPath);
|
|
46602
46763
|
await mkdir(descriptorPath, { mode: 448 }).catch((error) => {
|
|
46603
46764
|
if (errorDetails(error).code !== "EEXIST") throw runtimeDataTrustError(targetPath);
|
|
46604
46765
|
});
|
|
@@ -46619,7 +46780,7 @@ async function openOrCreateRuntimeDirectory(parentHandle, targetPath) {
|
|
|
46619
46780
|
return handle;
|
|
46620
46781
|
}
|
|
46621
46782
|
async function writeExclusiveRuntimeFile(parentHandle, targetPath, contents, mode) {
|
|
46622
|
-
const descriptorPath = descriptorChildPath(parentHandle.fd,
|
|
46783
|
+
const descriptorPath = descriptorChildPath(parentHandle.fd, path4.basename(targetPath), targetPath);
|
|
46623
46784
|
const handle = await open(
|
|
46624
46785
|
descriptorPath,
|
|
46625
46786
|
fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_NOFOLLOW,
|
|
@@ -46642,14 +46803,14 @@ async function writeExclusiveRuntimeFile(parentHandle, targetPath, contents, mod
|
|
|
46642
46803
|
}
|
|
46643
46804
|
}
|
|
46644
46805
|
async function publishRuntimeFile(parentHandle, targetPath, contents, mode, boundary) {
|
|
46645
|
-
const temporaryPath =
|
|
46806
|
+
const temporaryPath = path4.join(path4.dirname(targetPath), `.${path4.basename(targetPath)}.tmp-${process.pid}-${randomBytes(8).toString("hex")}`);
|
|
46646
46807
|
await writeExclusiveRuntimeFile(parentHandle, temporaryPath, contents, mode);
|
|
46647
|
-
const temporaryDescriptor = descriptorChildPath(parentHandle.fd,
|
|
46648
|
-
const targetDescriptor = descriptorChildPath(parentHandle.fd,
|
|
46808
|
+
const temporaryDescriptor = descriptorChildPath(parentHandle.fd, path4.basename(temporaryPath), temporaryPath);
|
|
46809
|
+
const targetDescriptor = descriptorChildPath(parentHandle.fd, path4.basename(targetPath), targetPath);
|
|
46649
46810
|
try {
|
|
46650
46811
|
await pauseRuntimeTreePublication(boundary, targetPath);
|
|
46651
46812
|
const parentIdentity = await parentHandle.stat();
|
|
46652
|
-
await assertRuntimeDataPathIdentity(
|
|
46813
|
+
await assertRuntimeDataPathIdentity(path4.dirname(targetPath), { dev: parentIdentity.dev, ino: parentIdentity.ino }, true);
|
|
46653
46814
|
try {
|
|
46654
46815
|
const existing = await lstat(targetPath);
|
|
46655
46816
|
if (!existing.isFile() || existing.isSymbolicLink()) throw runtimeDataTrustError(targetPath);
|
|
@@ -46692,11 +46853,11 @@ async function cleanupUnreferencedHostSealedEnvKeys(dataDirectory, referencedFin
|
|
|
46692
46853
|
try {
|
|
46693
46854
|
const keysIdentity = await keysHandle.stat();
|
|
46694
46855
|
const descriptorDirectory = process.platform === "linux" ? `/proc/self/fd/${keysHandle.fd}` : paths.keys;
|
|
46695
|
-
const entries = await
|
|
46856
|
+
const entries = await readdir2(descriptorDirectory);
|
|
46696
46857
|
for (const entry of entries) {
|
|
46697
46858
|
const match = /^([a-f0-9]{16})\.(private|public)\.pem$/.exec(entry);
|
|
46698
46859
|
if (!match || referencedFingerprints.has(match[1])) continue;
|
|
46699
|
-
const targetPath =
|
|
46860
|
+
const targetPath = path4.join(paths.keys, entry);
|
|
46700
46861
|
const descriptorPath = descriptorChildPath(keysHandle.fd, entry, targetPath);
|
|
46701
46862
|
const retained = await open(descriptorPath, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW).catch(() => {
|
|
46702
46863
|
throw runtimeDataTrustError(targetPath);
|
|
@@ -46754,7 +46915,7 @@ async function inspectHostSealedEnvKey(record, remoteRoot) {
|
|
|
46754
46915
|
if (typeof publicKeyFingerprint !== "string" || publicKeyFingerprint.length === 0) {
|
|
46755
46916
|
return null;
|
|
46756
46917
|
}
|
|
46757
|
-
const dataDirectory =
|
|
46918
|
+
const dataDirectory = path4.join(remoteRoot, "hosts", record.domain, "capsules", record.subname, "data");
|
|
46758
46919
|
const paths = hostSealedEnvKeyPaths(dataDirectory, publicKeyFingerprint);
|
|
46759
46920
|
const [publicKeyReadable, privateKeyReadable] = await Promise.all([
|
|
46760
46921
|
pathReadable(paths.publicKey),
|
|
@@ -46774,7 +46935,7 @@ async function inspectHostSealedEnvKey(record, remoteRoot) {
|
|
|
46774
46935
|
try {
|
|
46775
46936
|
return {
|
|
46776
46937
|
...base,
|
|
46777
|
-
publicKey: await
|
|
46938
|
+
publicKey: await readFile3(paths.publicKey, "utf8")
|
|
46778
46939
|
};
|
|
46779
46940
|
} catch (error) {
|
|
46780
46941
|
if (errorDetails(error).code === "ENOENT") {
|
|
@@ -46821,13 +46982,13 @@ function currentReleaseSealedServerEnvFingerprint(record) {
|
|
|
46821
46982
|
return typeof fingerprint === "string" ? fingerprint : null;
|
|
46822
46983
|
}
|
|
46823
46984
|
function hostSealedEnvKeyPaths(dataDirectory, fingerprint) {
|
|
46824
|
-
const root =
|
|
46825
|
-
const keys =
|
|
46985
|
+
const root = path4.join(dataDirectory, "sealed-server-env");
|
|
46986
|
+
const keys = path4.join(root, "keys");
|
|
46826
46987
|
return {
|
|
46827
46988
|
root,
|
|
46828
46989
|
keys,
|
|
46829
|
-
privateKey:
|
|
46830
|
-
publicKey:
|
|
46990
|
+
privateKey: path4.join(keys, `${fingerprint}.private.pem`),
|
|
46991
|
+
publicKey: path4.join(keys, `${fingerprint}.public.pem`)
|
|
46831
46992
|
};
|
|
46832
46993
|
}
|
|
46833
46994
|
function fingerprintPublicKey(publicKey) {
|
|
@@ -46887,20 +47048,20 @@ function normaliseBootstrap(request) {
|
|
|
46887
47048
|
const provided = request.bootstrap ?? {};
|
|
46888
47049
|
const remoteRoot = request.host.remoteRoot;
|
|
46889
47050
|
const domain = request.host.domain;
|
|
46890
|
-
const caddyDirectory =
|
|
46891
|
-
const domainDirectory =
|
|
46892
|
-
const tlsDirectory =
|
|
47051
|
+
const caddyDirectory = path4.join(remoteRoot, "caddy");
|
|
47052
|
+
const domainDirectory = path4.join(remoteRoot, "hosts", domain);
|
|
47053
|
+
const tlsDirectory = path4.join(domainDirectory, "tls");
|
|
46893
47054
|
const expectedDirectories = {
|
|
46894
47055
|
remoteRoot,
|
|
46895
|
-
bin:
|
|
46896
|
-
incoming:
|
|
47056
|
+
bin: path4.join(remoteRoot, "bin"),
|
|
47057
|
+
incoming: path4.join(remoteRoot, "incoming"),
|
|
46897
47058
|
caddy: caddyDirectory,
|
|
46898
|
-
caddyHosts:
|
|
46899
|
-
hosts:
|
|
47059
|
+
caddyHosts: path4.join(caddyDirectory, "hosts"),
|
|
47060
|
+
hosts: path4.join(remoteRoot, "hosts"),
|
|
46900
47061
|
domain: domainDirectory,
|
|
46901
47062
|
tls: tlsDirectory,
|
|
46902
|
-
registry:
|
|
46903
|
-
capsules:
|
|
47063
|
+
registry: path4.join(domainDirectory, "registry"),
|
|
47064
|
+
capsules: path4.join(domainDirectory, "capsules")
|
|
46904
47065
|
};
|
|
46905
47066
|
for (const [name2, value] of Object.entries(provided.directories ?? {})) {
|
|
46906
47067
|
if (!(name2 in expectedDirectories) || value !== expectedDirectories[name2]) throw bootstrapPathError();
|
|
@@ -46910,8 +47071,8 @@ function normaliseBootstrap(request) {
|
|
|
46910
47071
|
const directories = {
|
|
46911
47072
|
...expectedDirectories
|
|
46912
47073
|
};
|
|
46913
|
-
const expectedCertificate =
|
|
46914
|
-
const expectedKey =
|
|
47074
|
+
const expectedCertificate = path4.join(directories.tls, "origin.crt");
|
|
47075
|
+
const expectedKey = path4.join(directories.tls, "origin.key");
|
|
46915
47076
|
if (provided.tls?.directory !== void 0 && provided.tls.directory !== directories.tls) throw bootstrapPathError();
|
|
46916
47077
|
if (provided.tls?.certificate != null && provided.tls.certificate !== expectedCertificate) throw bootstrapPathError();
|
|
46917
47078
|
if (provided.tls?.key != null && provided.tls.key !== expectedKey) throw bootstrapPathError();
|
|
@@ -46921,8 +47082,8 @@ function normaliseBootstrap(request) {
|
|
|
46921
47082
|
certificate: tlsMode === "cloudflare-origin" ? expectedCertificate : null,
|
|
46922
47083
|
key: tlsMode === "cloudflare-origin" ? expectedKey : null
|
|
46923
47084
|
};
|
|
46924
|
-
const expectedManagedInclude =
|
|
46925
|
-
const expectedDomainInclude =
|
|
47085
|
+
const expectedManagedInclude = path4.join(directories.caddy, "sporades-hosted-domains.caddy");
|
|
47086
|
+
const expectedDomainInclude = path4.join(directories.caddyHosts, `${domain}.caddy`);
|
|
46926
47087
|
const expectedAccessLog = defaultCaddyAccessLogPath(remoteRoot);
|
|
46927
47088
|
if (provided.caddy?.managedInclude !== void 0 && provided.caddy.managedInclude !== expectedManagedInclude) throw bootstrapPathError();
|
|
46928
47089
|
if (provided.caddy?.domainInclude !== void 0 && provided.caddy.domainInclude !== expectedDomainInclude) throw bootstrapPathError();
|
|
@@ -46937,11 +47098,11 @@ function normaliseBootstrap(request) {
|
|
|
46937
47098
|
tls,
|
|
46938
47099
|
network: provided.network ?? hostHelperConfig.hostedCapsule.dockerNetwork,
|
|
46939
47100
|
caddy: {
|
|
46940
|
-
caddyfile:
|
|
47101
|
+
caddyfile: path4.join(directories.caddy, "Caddyfile"),
|
|
46941
47102
|
managedInclude: expectedManagedInclude,
|
|
46942
47103
|
domainInclude: expectedDomainInclude,
|
|
46943
|
-
routesDirectory:
|
|
46944
|
-
healthRoute:
|
|
47104
|
+
routesDirectory: path4.join(directories.caddyHosts, domain),
|
|
47105
|
+
healthRoute: path4.join(directories.caddyHosts, domain, "host.caddy"),
|
|
46945
47106
|
accessLog: expectedAccessLog
|
|
46946
47107
|
}
|
|
46947
47108
|
};
|
|
@@ -46954,7 +47115,7 @@ function bootstrapPathError() {
|
|
|
46954
47115
|
}
|
|
46955
47116
|
function bootstrapTrustManifest(request) {
|
|
46956
47117
|
const bootstrap = normaliseBootstrap(request);
|
|
46957
|
-
const placeholder =
|
|
47118
|
+
const placeholder = path4.join(bootstrap.caddy.routesDirectory, ".sporades-placeholder.caddy");
|
|
46958
47119
|
const finalFiles = [
|
|
46959
47120
|
{ path: bootstrap.caddy.caddyfile },
|
|
46960
47121
|
{ path: bootstrap.caddy.managedInclude },
|
|
@@ -46969,8 +47130,8 @@ function bootstrapTrustManifest(request) {
|
|
|
46969
47130
|
return {
|
|
46970
47131
|
directories: [
|
|
46971
47132
|
...Object.values(bootstrap.directories).map((entry) => ({ path: entry })),
|
|
46972
|
-
{ path:
|
|
46973
|
-
{ path:
|
|
47133
|
+
{ path: path4.dirname(bootstrap.caddy.accessLog), caddyOwned: true },
|
|
47134
|
+
{ path: path4.join(bootstrap.directories.registry, "capsules") },
|
|
46974
47135
|
{ path: bootstrap.caddy.routesDirectory }
|
|
46975
47136
|
],
|
|
46976
47137
|
finalFiles
|
|
@@ -46982,13 +47143,13 @@ async function ensureBootstrapDirectories(bootstrap) {
|
|
|
46982
47143
|
bootstrap.directories.bin,
|
|
46983
47144
|
bootstrap.directories.incoming,
|
|
46984
47145
|
bootstrap.directories.caddy,
|
|
46985
|
-
|
|
47146
|
+
path4.dirname(bootstrap.caddy.accessLog),
|
|
46986
47147
|
bootstrap.directories.caddyHosts,
|
|
46987
47148
|
bootstrap.directories.hosts,
|
|
46988
47149
|
bootstrap.directories.domain,
|
|
46989
47150
|
bootstrap.directories.tls,
|
|
46990
47151
|
bootstrap.directories.registry,
|
|
46991
|
-
|
|
47152
|
+
path4.join(bootstrap.directories.registry, "capsules"),
|
|
46992
47153
|
bootstrap.directories.capsules,
|
|
46993
47154
|
bootstrap.caddy.routesDirectory
|
|
46994
47155
|
];
|
|
@@ -46999,7 +47160,7 @@ async function ensureBootstrapDirectories(bootstrap) {
|
|
|
46999
47160
|
}
|
|
47000
47161
|
async function provisionCaddyAccessLog(request, bootstrap) {
|
|
47001
47162
|
const logFile = bootstrap.caddy.accessLog;
|
|
47002
|
-
const logDirectory =
|
|
47163
|
+
const logDirectory = path4.dirname(logFile);
|
|
47003
47164
|
const caddyUser = resolveCaddyServiceUser();
|
|
47004
47165
|
if (!caddyUser) {
|
|
47005
47166
|
throw helperError(
|
|
@@ -47043,7 +47204,7 @@ async function provisionCaddyOwnedLogDescriptors(options) {
|
|
|
47043
47204
|
try {
|
|
47044
47205
|
let directoryDetails = await directoryHandle.stat();
|
|
47045
47206
|
if (!directoryDetails.isDirectory() || directoryDetails.dev !== directoryIdentity.dev || directoryDetails.ino !== directoryIdentity.ino || !(directoryIdentity.expectedOwner ? trustedExactOwnerMetadata(directoryDetails, directoryIdentity.expectedOwner) : trustedHostPathMetadata(directoryDetails))) throw routeTrustError();
|
|
47046
|
-
const descriptorFile = process.platform === "linux" ? `/proc/self/fd/${directoryHandle.fd}/${
|
|
47207
|
+
const descriptorFile = process.platform === "linux" ? `/proc/self/fd/${directoryHandle.fd}/${path4.basename(logFile)}` : logFile;
|
|
47047
47208
|
let fileHandle;
|
|
47048
47209
|
try {
|
|
47049
47210
|
fileHandle = await open(
|
|
@@ -47120,7 +47281,7 @@ async function provisionRouteLogFile(route, mutationBoundary) {
|
|
|
47120
47281
|
if (!logFile) {
|
|
47121
47282
|
return;
|
|
47122
47283
|
}
|
|
47123
|
-
const logDirectory =
|
|
47284
|
+
const logDirectory = path4.dirname(logFile);
|
|
47124
47285
|
const caddyUser = resolveCaddyServiceUser() ?? {
|
|
47125
47286
|
uid: String(process.geteuid?.() ?? process.getuid?.() ?? 0),
|
|
47126
47287
|
gid: String(process.getegid?.() ?? process.getgid?.() ?? 0)
|
|
@@ -47188,15 +47349,15 @@ async function installCaddyBootstrapConfig(request, bootstrap) {
|
|
|
47188
47349
|
const caddyfile = bootstrap.caddy.caddyfile;
|
|
47189
47350
|
const managedInclude = bootstrap.caddy.managedInclude;
|
|
47190
47351
|
const domainInclude = bootstrap.caddy.domainInclude;
|
|
47191
|
-
const placeholderRoute =
|
|
47352
|
+
const placeholderRoute = path4.join(bootstrap.caddy.routesDirectory, ".sporades-placeholder.caddy");
|
|
47192
47353
|
await atomicPublishBootstrapFile(placeholderRoute, "# Sporades keeps this placeholder so Caddy route imports are valid before Capsules are registered.\n", "bootstrap-placeholder");
|
|
47193
47354
|
await atomicPublishBootstrapFile(bootstrap.caddy.healthRoute, renderHostHealthRoute(request.host.domain, bootstrap.tls), "bootstrap-health-route");
|
|
47194
47355
|
await writeManagedCaddyfile(caddyfile, `import ${managedInclude}`);
|
|
47195
47356
|
await atomicPublishBootstrapFile(managedInclude, `# Sporades-managed Hosted domain include list.
|
|
47196
|
-
import ${
|
|
47357
|
+
import ${path4.join(bootstrap.directories.caddyHosts, "*.caddy")}
|
|
47197
47358
|
`, "bootstrap-managed-include");
|
|
47198
47359
|
await atomicPublishBootstrapFile(domainInclude, `# Sporades-managed routes for ${request.host.domain}.
|
|
47199
|
-
import ${
|
|
47360
|
+
import ${path4.join(bootstrap.caddy.routesDirectory, "*.caddy")}
|
|
47200
47361
|
`, "bootstrap-domain-include");
|
|
47201
47362
|
await assertActiveManagedRouteTrust(null);
|
|
47202
47363
|
validateCaddyBootstrap(caddyfile);
|
|
@@ -47240,7 +47401,7 @@ ${end}
|
|
|
47240
47401
|
let existing = "";
|
|
47241
47402
|
await assertBootstrapMutationBoundary("bootstrap-caddyfile-read", [caddyfile]);
|
|
47242
47403
|
try {
|
|
47243
|
-
existing = await
|
|
47404
|
+
existing = await readFile3(caddyfile, "utf8");
|
|
47244
47405
|
} catch (error) {
|
|
47245
47406
|
if (errorDetails(error).code !== "ENOENT") {
|
|
47246
47407
|
throw error;
|
|
@@ -47431,6 +47592,8 @@ async function dockerRunArgs(lifecycle, releaseId, runtimeProbe) {
|
|
|
47431
47592
|
"--env",
|
|
47432
47593
|
`SPORADES_PUBLIC_ORIGIN=${lifecycle.hostedUrl}`,
|
|
47433
47594
|
"--env",
|
|
47595
|
+
`SPORADES_PUBLIC_ALIASES=${JSON.stringify(validateAliasDomains(lifecycle.routes.running.aliasDomains).map((hostname) => `https://${hostname}`))}`,
|
|
47596
|
+
"--env",
|
|
47434
47597
|
`SPORADES_RELEASE_ID=${releaseId}`
|
|
47435
47598
|
);
|
|
47436
47599
|
args.push("--publish", `127.0.0.1::${lifecycle.routes.running.port ?? 4e3}`);
|
|
@@ -47509,7 +47672,7 @@ function ensureHostedBaseImage(lifecycle) {
|
|
|
47509
47672
|
if (pull.ok) {
|
|
47510
47673
|
return;
|
|
47511
47674
|
}
|
|
47512
|
-
const dockerfilePath =
|
|
47675
|
+
const dockerfilePath = path4.join(lifecycle.remoteRoot, "Dockerfile.base");
|
|
47513
47676
|
if (!pathExistsSync(dockerfilePath)) {
|
|
47514
47677
|
throw helperError(
|
|
47515
47678
|
"Unable to prepare the Sporades Base image.",
|
|
@@ -47599,7 +47762,7 @@ async function currentReleaseId(currentLink, request) {
|
|
|
47599
47762
|
}
|
|
47600
47763
|
throw error;
|
|
47601
47764
|
}
|
|
47602
|
-
return
|
|
47765
|
+
return path4.basename(target);
|
|
47603
47766
|
}
|
|
47604
47767
|
function loopbackRunningRoute(route, publishedPort) {
|
|
47605
47768
|
return {
|
|
@@ -47615,7 +47778,7 @@ async function refreshLoopbackRunningRoute(request, registryRecord, containerNam
|
|
|
47615
47778
|
return withManagedRouteLock(routeFile, async () => {
|
|
47616
47779
|
let currentRoute;
|
|
47617
47780
|
try {
|
|
47618
|
-
currentRoute = await
|
|
47781
|
+
currentRoute = await readFile3(routeFile, "utf8");
|
|
47619
47782
|
} catch (error) {
|
|
47620
47783
|
if (errorDetails(error).code === "ENOENT") {
|
|
47621
47784
|
return { ok: true, refreshed: false };
|
|
@@ -47642,6 +47805,17 @@ async function refreshLoopbackRunningRoute(request, registryRecord, containerNam
|
|
|
47642
47805
|
}
|
|
47643
47806
|
async function writeRunningRoute(lifecycle, route = lifecycle.routes.running) {
|
|
47644
47807
|
await provisionRouteLogFile(route, "capsule-running-http-log-descriptor-mutate");
|
|
47808
|
+
await applyManagedRoute(lifecycle, route.routeFile, routeVariants(route).map(renderRunningRoute).join("\n"));
|
|
47809
|
+
}
|
|
47810
|
+
function routeVariants(route) {
|
|
47811
|
+
return [route, ...validateAliasDomains(route.aliasDomains).map((hostname) => ({
|
|
47812
|
+
...route,
|
|
47813
|
+
hostname,
|
|
47814
|
+
aliasDomains: void 0,
|
|
47815
|
+
tls: { mode: "automatic" }
|
|
47816
|
+
}))];
|
|
47817
|
+
}
|
|
47818
|
+
function renderRunningRoute(route) {
|
|
47645
47819
|
const cloudflareOrigin = route.tls?.mode === "cloudflare-origin";
|
|
47646
47820
|
const proxyLine = [
|
|
47647
47821
|
`reverse_proxy ${route.upstream ?? `${route.containerName}:${route.port ?? 4e3}`} {`,
|
|
@@ -47654,11 +47828,7 @@ async function writeRunningRoute(lifecycle, route = lifecycle.routes.running) {
|
|
|
47654
47828
|
"respond @sporadesUntrustedCloudflareSource 403",
|
|
47655
47829
|
routeHandler
|
|
47656
47830
|
].join("\n ") : routeHandler;
|
|
47657
|
-
|
|
47658
|
-
lifecycle,
|
|
47659
|
-
route.routeFile,
|
|
47660
|
-
renderRoute(route, guardedHandler)
|
|
47661
|
-
);
|
|
47831
|
+
return renderRoute(route, guardedHandler);
|
|
47662
47832
|
}
|
|
47663
47833
|
function renderRunningRouteHandler(route, proxyLine) {
|
|
47664
47834
|
const probe = route.runtimeProbe;
|
|
@@ -47684,7 +47854,7 @@ async function writeUnavailableRoute(lifecycle) {
|
|
|
47684
47854
|
await applyManagedRoute(
|
|
47685
47855
|
lifecycle,
|
|
47686
47856
|
route.routeFile,
|
|
47687
|
-
renderRoute(
|
|
47857
|
+
routeVariants(route).map((variant) => renderRoute(variant, renderUnavailableRouteHandler(variant))).join("\n")
|
|
47688
47858
|
);
|
|
47689
47859
|
}
|
|
47690
47860
|
function renderUnavailableRouteHandler(route) {
|
|
@@ -47865,7 +48035,7 @@ async function processRetainsOsFlock(lockFile) {
|
|
|
47865
48035
|
for (const descriptorRoot of ["/proc/self/fd", "/dev/fd"]) {
|
|
47866
48036
|
let entries;
|
|
47867
48037
|
try {
|
|
47868
|
-
entries = await
|
|
48038
|
+
entries = await readdir2(descriptorRoot);
|
|
47869
48039
|
} catch {
|
|
47870
48040
|
continue;
|
|
47871
48041
|
}
|
|
@@ -47877,7 +48047,7 @@ async function processRetainsOsFlock(lockFile) {
|
|
|
47877
48047
|
for (const descriptor of descriptorNumbers) {
|
|
47878
48048
|
for (const descriptorRoot of ["/proc/self/fd", "/dev/fd"]) {
|
|
47879
48049
|
try {
|
|
47880
|
-
const descriptorStat = await stat(
|
|
48050
|
+
const descriptorStat = await stat(path4.join(descriptorRoot, descriptor));
|
|
47881
48051
|
if (!routeLockFileIdentityMatches(descriptorStat, expected)) continue;
|
|
47882
48052
|
if (process.platform === "linux") {
|
|
47883
48053
|
if (descriptorStat.dev !== expected.dev) continue;
|
|
@@ -47894,7 +48064,7 @@ async function processRetainsOsFlock(lockFile) {
|
|
|
47894
48064
|
async function linuxDescriptorOwnsFlock(descriptor, expected) {
|
|
47895
48065
|
let fdinfo;
|
|
47896
48066
|
try {
|
|
47897
|
-
fdinfo = await
|
|
48067
|
+
fdinfo = await readFile3(`/proc/self/fdinfo/${descriptor}`, "utf8");
|
|
47898
48068
|
} catch {
|
|
47899
48069
|
return false;
|
|
47900
48070
|
}
|
|
@@ -47952,7 +48122,7 @@ function managedRouteLockTimeoutMs() {
|
|
|
47952
48122
|
async function readManagedRouteProtocolOwner(protocolFile) {
|
|
47953
48123
|
let parsed;
|
|
47954
48124
|
try {
|
|
47955
|
-
parsed = JSON.parse(await
|
|
48125
|
+
parsed = JSON.parse(await readFile3(protocolFile, "utf8"));
|
|
47956
48126
|
} catch (error) {
|
|
47957
48127
|
if (errorDetails(error).code === "ENOENT") {
|
|
47958
48128
|
return { state: "absent", owner: null };
|
|
@@ -47981,12 +48151,12 @@ function managedRouteProtocolOwnerIsLive(owner) {
|
|
|
47981
48151
|
return currentIdentity === null || currentIdentity === owner.processIdentity;
|
|
47982
48152
|
}
|
|
47983
48153
|
async function cleanupManagedRouteProtocolArtifacts(lockFile) {
|
|
47984
|
-
const directory =
|
|
47985
|
-
const base =
|
|
48154
|
+
const directory = path4.dirname(lockFile);
|
|
48155
|
+
const base = path4.basename(lockFile);
|
|
47986
48156
|
const artifact = new RegExp(`^${escapeRegExp(base)}\\.(?:claim|stale|reclaim)-[a-f0-9]{32}$`);
|
|
47987
|
-
const entries = (await
|
|
48157
|
+
const entries = (await readdir2(directory)).filter((entry) => artifact.test(entry)).sort().slice(0, 100);
|
|
47988
48158
|
for (const entry of entries) {
|
|
47989
|
-
const artifactPath =
|
|
48159
|
+
const artifactPath = path4.join(directory, entry);
|
|
47990
48160
|
const owner = await readManagedRouteProtocolOwner(artifactPath);
|
|
47991
48161
|
if (owner.state === "owner" && !managedRouteProtocolOwnerIsLive(owner.owner)) {
|
|
47992
48162
|
await rm(artifactPath, { force: true });
|
|
@@ -48019,10 +48189,10 @@ async function prepareWritableDataPath(targetPath) {
|
|
|
48019
48189
|
}
|
|
48020
48190
|
async function openCanonicalRuntimeDataDirectory(targetPath, createData) {
|
|
48021
48191
|
const managedRoot = activeManagedRouteTrust?.managedRoot;
|
|
48022
|
-
if (!managedRoot || !
|
|
48023
|
-
const relative =
|
|
48024
|
-
const components = relative.split(
|
|
48025
|
-
if (relative.startsWith("..") ||
|
|
48192
|
+
if (!managedRoot || !path4.isAbsolute(targetPath) || path4.normalize(targetPath) !== targetPath) throw runtimeDataTrustError(targetPath);
|
|
48193
|
+
const relative = path4.relative(managedRoot, targetPath);
|
|
48194
|
+
const components = relative.split(path4.sep).filter(Boolean);
|
|
48195
|
+
if (relative.startsWith("..") || path4.isAbsolute(relative) || components.length !== 5 || components[0] !== "hosts" || components[2] !== "capsules" || components[4] !== "data" || !/^[a-z0-9.-]+(?::[1-9][0-9]{0,4})?$/.test(components[1]) || !/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(components[3])) throw runtimeDataTrustError(targetPath);
|
|
48026
48196
|
let handle = await open(
|
|
48027
48197
|
managedRoot,
|
|
48028
48198
|
fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW | (fsConstants.O_DIRECTORY ?? 0)
|
|
@@ -48032,7 +48202,7 @@ async function openCanonicalRuntimeDataDirectory(targetPath, createData) {
|
|
|
48032
48202
|
let logical = managedRoot;
|
|
48033
48203
|
try {
|
|
48034
48204
|
for (let index = 0; index < components.length; index += 1) {
|
|
48035
|
-
logical =
|
|
48205
|
+
logical = path4.join(logical, components[index]);
|
|
48036
48206
|
const descriptorPath = descriptorChildPath(handle.fd, components[index], logical);
|
|
48037
48207
|
if (createData && index === components.length - 1) {
|
|
48038
48208
|
await mkdir(descriptorPath, { mode: 448 }).catch((error) => {
|
|
@@ -48062,7 +48232,7 @@ async function openCanonicalRuntimeDataDirectory(targetPath, createData) {
|
|
|
48062
48232
|
}
|
|
48063
48233
|
}
|
|
48064
48234
|
function descriptorChildPath(parentFd, name2, fallbackPath) {
|
|
48065
|
-
if (name2.length === 0 || name2 === "." || name2 === ".." || name2.includes(
|
|
48235
|
+
if (name2.length === 0 || name2 === "." || name2 === ".." || name2.includes(path4.sep)) throw runtimeDataTrustError(fallbackPath);
|
|
48066
48236
|
return process.platform === "linux" ? `/proc/self/fd/${parentFd}/${name2}` : fallbackPath;
|
|
48067
48237
|
}
|
|
48068
48238
|
async function prepareWritableDataHandle(handle, targetPath, directory) {
|
|
@@ -48078,12 +48248,12 @@ async function prepareWritableDataHandle(handle, targetPath, directory) {
|
|
|
48078
48248
|
await assertRuntimeDataPathIdentity(targetPath, identity, directory);
|
|
48079
48249
|
if (!directory) return;
|
|
48080
48250
|
const descriptorDirectory = process.platform === "linux" ? `/proc/self/fd/${handle.fd}` : targetPath;
|
|
48081
|
-
const entries = await
|
|
48251
|
+
const entries = await readdir2(descriptorDirectory, { withFileTypes: true }).catch(() => {
|
|
48082
48252
|
throw runtimeDataTrustError(targetPath);
|
|
48083
48253
|
});
|
|
48084
48254
|
for (const entry of entries) {
|
|
48085
|
-
if (entry.name === "." || entry.name === ".." || entry.name.includes(
|
|
48086
|
-
const childPath =
|
|
48255
|
+
if (entry.name === "." || entry.name === ".." || entry.name.includes(path4.sep)) throw runtimeDataTrustError(targetPath);
|
|
48256
|
+
const childPath = path4.join(targetPath, entry.name);
|
|
48087
48257
|
const descriptorPath = descriptorChildPath(handle.fd, entry.name, childPath);
|
|
48088
48258
|
const flags = fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW | (entry.isDirectory() ? fsConstants.O_DIRECTORY ?? 0 : 0);
|
|
48089
48259
|
const child = await open(descriptorPath, flags).catch(() => {
|
|
@@ -48151,7 +48321,7 @@ function validateCaddyRoute(routeFile) {
|
|
|
48151
48321
|
}
|
|
48152
48322
|
}
|
|
48153
48323
|
function reloadCaddy(lifecycle) {
|
|
48154
|
-
const configPath =
|
|
48324
|
+
const configPath = path4.join(lifecycle.remoteRoot, "caddy", "Caddyfile");
|
|
48155
48325
|
const result = spawnSync2("caddy", ["reload", "--config", configPath, "--adapter", "caddyfile"], { encoding: "utf8" });
|
|
48156
48326
|
if (result.error || result.status !== 0) {
|
|
48157
48327
|
throw helperError(
|
|
@@ -48467,12 +48637,12 @@ function releaseSealedServerEnvPrivateKeyMount(registryRecord, paths) {
|
|
|
48467
48637
|
const fingerprint = release?.source?.sealedServerEnv?.publicKeyFingerprint;
|
|
48468
48638
|
if (typeof fingerprint === "string" && /^[a-f0-9]{16}$/.test(fingerprint)) {
|
|
48469
48639
|
return {
|
|
48470
|
-
host:
|
|
48640
|
+
host: path4.join(paths.data, "sealed-server-env", "keys", `${fingerprint}.private.pem`),
|
|
48471
48641
|
fingerprint
|
|
48472
48642
|
};
|
|
48473
48643
|
}
|
|
48474
48644
|
return {
|
|
48475
|
-
host:
|
|
48645
|
+
host: path4.join(paths.data, "sealed-server-env", "server-env.private.pem"),
|
|
48476
48646
|
fingerprint: null
|
|
48477
48647
|
};
|
|
48478
48648
|
}
|
|
@@ -48484,7 +48654,7 @@ function releaseSshAuthorizedKeysMount(registryRecord, paths) {
|
|
|
48484
48654
|
return null;
|
|
48485
48655
|
}
|
|
48486
48656
|
return {
|
|
48487
|
-
host:
|
|
48657
|
+
host: path4.join(paths.currentLink, ".sporades", "ssh", "authorized_keys"),
|
|
48488
48658
|
container: "/run/sporades/ssh/authorized_keys",
|
|
48489
48659
|
mode: "ro",
|
|
48490
48660
|
optional: false
|
|
@@ -48596,7 +48766,7 @@ function compareReleasesNewestFirst(left, right) {
|
|
|
48596
48766
|
async function readRegistryRecordForCapsule(request, purpose) {
|
|
48597
48767
|
const registryRecordPath = registryPath(request);
|
|
48598
48768
|
try {
|
|
48599
|
-
return JSON.parse(await
|
|
48769
|
+
return JSON.parse(await readFile3(registryRecordPath, "utf8"));
|
|
48600
48770
|
} catch (error) {
|
|
48601
48771
|
if (errorDetails(error).code === "ENOENT") {
|
|
48602
48772
|
throw helperError(
|
|
@@ -48636,7 +48806,7 @@ function assertRegistryRecordMatchesRequest(request, record) {
|
|
|
48636
48806
|
async function mutateRegistryRecord(request, mutate) {
|
|
48637
48807
|
return withRegistryLock(request, async () => {
|
|
48638
48808
|
const registryRecordPath = registryPath(request);
|
|
48639
|
-
const record = JSON.parse(await
|
|
48809
|
+
const record = JSON.parse(await readFile3(registryRecordPath, "utf8"));
|
|
48640
48810
|
await writeRegistryRecordAtomic(registryRecordPath, mutate(record));
|
|
48641
48811
|
});
|
|
48642
48812
|
}
|
|
@@ -48688,7 +48858,7 @@ async function withRegistryLock(request, fn) {
|
|
|
48688
48858
|
}
|
|
48689
48859
|
}
|
|
48690
48860
|
function registryPath(request) {
|
|
48691
|
-
return
|
|
48861
|
+
return path4.join(
|
|
48692
48862
|
request.host.remoteRoot,
|
|
48693
48863
|
"hosts",
|
|
48694
48864
|
request.host.domain,
|
|
@@ -48698,7 +48868,7 @@ function registryPath(request) {
|
|
|
48698
48868
|
);
|
|
48699
48869
|
}
|
|
48700
48870
|
function registryLockPath(request) {
|
|
48701
|
-
return
|
|
48871
|
+
return path4.join(request.host.remoteRoot, "hosts", request.host.domain, "registry", ".lock");
|
|
48702
48872
|
}
|
|
48703
48873
|
function capsuleData(request, lifecycle) {
|
|
48704
48874
|
return {
|
|
@@ -48750,7 +48920,7 @@ async function readManagedCaddyAccessLog(logs) {
|
|
|
48750
48920
|
}
|
|
48751
48921
|
return null;
|
|
48752
48922
|
}
|
|
48753
|
-
const contents = await
|
|
48923
|
+
const contents = await readFile3(logs.file, "utf8");
|
|
48754
48924
|
return lastLogEntries(contents, logs.lines);
|
|
48755
48925
|
}
|
|
48756
48926
|
function readCaddyJournalLogs(logs) {
|
|
@@ -48800,14 +48970,14 @@ function createHostedContainerName(domain, subname) {
|
|
|
48800
48970
|
return `sporades-${domain.replace(/[^a-z0-9]+/gi, "-").replace(/^-+|-+$/g, "").toLowerCase()}-${subname}`;
|
|
48801
48971
|
}
|
|
48802
48972
|
function defaultCaddyAccessLogPath(remoteRoot) {
|
|
48803
|
-
return
|
|
48973
|
+
return path4.join(remoteRoot, "caddy", "logs", "access.log");
|
|
48804
48974
|
}
|
|
48805
48975
|
function defaultCapsuleHttpLogPath(remoteRoot, domain, subname) {
|
|
48806
|
-
return
|
|
48976
|
+
return path4.join(remoteRoot, "hosts", domain, "capsules", subname, "logs", "http.log");
|
|
48807
48977
|
}
|
|
48808
48978
|
function canonicalCapsuleHttpLogPath(request, validatedRemoteRoot = validateCanonicalHostRouteRoot(request)) {
|
|
48809
48979
|
canonicalManagedRouteFile(request, validatedRemoteRoot);
|
|
48810
|
-
const expectedDirectory =
|
|
48980
|
+
const expectedDirectory = path4.resolve(
|
|
48811
48981
|
validatedRemoteRoot,
|
|
48812
48982
|
"hosts",
|
|
48813
48983
|
request.host.domain,
|
|
@@ -48815,8 +48985,8 @@ function canonicalCapsuleHttpLogPath(request, validatedRemoteRoot = validateCano
|
|
|
48815
48985
|
request.capsule.subname,
|
|
48816
48986
|
"logs"
|
|
48817
48987
|
);
|
|
48818
|
-
const expected =
|
|
48819
|
-
if (
|
|
48988
|
+
const expected = path4.resolve(expectedDirectory, "http.log");
|
|
48989
|
+
if (path4.dirname(expected) !== expectedDirectory) throw invalidCapsuleHttpLogPathError();
|
|
48820
48990
|
const lifecycle = request.lifecycle;
|
|
48821
48991
|
const supplied = [
|
|
48822
48992
|
request.registration?.route?.log?.file,
|
|
@@ -48831,7 +49001,7 @@ function canonicalCapsuleHttpLogPath(request, validatedRemoteRoot = validateCano
|
|
|
48831
49001
|
function capsuleHttpLogTrustManifest(request, validatedRemoteRoot) {
|
|
48832
49002
|
const logFile = canonicalCapsuleHttpLogPath(request, validatedRemoteRoot);
|
|
48833
49003
|
return {
|
|
48834
|
-
directories: [{ path:
|
|
49004
|
+
directories: [{ path: path4.dirname(logFile), caddyOwned: true }],
|
|
48835
49005
|
finalFiles: [{ path: logFile, caddyOwned: true }]
|
|
48836
49006
|
};
|
|
48837
49007
|
}
|
|
@@ -48898,7 +49068,7 @@ async function recordedReleaseFileClaims(releaseDirectory, recordedRelease) {
|
|
|
48898
49068
|
const normalized = file.normalize("NFC");
|
|
48899
49069
|
if (canonical.has(normalized)) throw helperError("Hosted Capsule release inventory is invalid.", "Choose another recorded release or push a replacement.");
|
|
48900
49070
|
canonical.add(normalized);
|
|
48901
|
-
const stats = await lstat(
|
|
49071
|
+
const stats = await lstat(path4.join(releaseDirectory, ...file.split("/")));
|
|
48902
49072
|
if (!stats.isFile() || stats.isSymbolicLink() || stats.nlink !== 1) {
|
|
48903
49073
|
throw helperError("Hosted Capsule release files are missing.", "Choose another complete immutable release.");
|
|
48904
49074
|
}
|
|
@@ -48909,12 +49079,12 @@ async function recordedReleaseFileClaims(releaseDirectory, recordedRelease) {
|
|
|
48909
49079
|
async function deriveReleaseFileClaims(root) {
|
|
48910
49080
|
const claims = [];
|
|
48911
49081
|
async function visit(directory, prefix = "") {
|
|
48912
|
-
for (const entry of await
|
|
49082
|
+
for (const entry of await readdir2(directory, { withFileTypes: true })) {
|
|
48913
49083
|
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
48914
49084
|
if (!safeRecordedReleasePath(relative)) {
|
|
48915
49085
|
throw helperError("Hosted Capsule release inventory is invalid.", "Choose another recorded release or push a replacement.");
|
|
48916
49086
|
}
|
|
48917
|
-
const entryPath =
|
|
49087
|
+
const entryPath = path4.join(directory, entry.name);
|
|
48918
49088
|
const stats = await lstat(entryPath);
|
|
48919
49089
|
if (stats.isSymbolicLink()) {
|
|
48920
49090
|
throw helperError("Hosted Capsule release inventory is invalid.", "Choose another recorded release or push a replacement.");
|
|
@@ -48938,7 +49108,7 @@ function validRecordedReleaseIdentity(file) {
|
|
|
48938
49108
|
return file && safeRecordedReleasePath(file.path) && Number.isSafeInteger(file.size) && file.size >= 0 && typeof file.sha256 === "string" && /^[a-f0-9]{64}$/.test(file.sha256);
|
|
48939
49109
|
}
|
|
48940
49110
|
function safeRecordedReleasePath(file) {
|
|
48941
|
-
return file.length > 0 && !file.startsWith("/") && !file.includes("\\") && !file.includes("\0") &&
|
|
49111
|
+
return file.length > 0 && !file.startsWith("/") && !file.includes("\\") && !file.includes("\0") && path4.posix.normalize(file) === file && Buffer.byteLength(file, "utf8") <= HOST_RELEASE_ARCHIVE_LIMITS.pathBytes && file.split("/").every((segment) => segment && segment !== "." && segment !== "..");
|
|
48942
49112
|
}
|
|
48943
49113
|
async function verifyRegisteredCapsule(request, purpose = "push") {
|
|
48944
49114
|
const record = await readRegistryRecordForCapsule(request, purpose);
|