sporades 0.9.21 → 0.9.23
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 +13 -4
- package/README.md +11 -0
- package/bin/sporades-host-helper.js +1049 -442
- package/bin/sporades.js +2998 -1542
- package/dist/bundle-pipeline.d.ts +2 -0
- package/dist/bundle-pipeline.d.ts.map +1 -1
- package/dist/bundle-pipeline.js +3 -0
- package/dist/bundle-pipeline.js.map +1 -1
- package/dist/cli/cli-help.d.ts +3 -3
- package/dist/cli/cli-help.d.ts.map +1 -1
- package/dist/cli/cli-help.js +5 -1
- 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-release-files.d.ts +1 -1
- package/dist/cli/host-helper-release-files.d.ts.map +1 -1
- package/dist/cli/host-helper-release-files.js +4 -3
- package/dist/cli/host-helper-release-files.js.map +1 -1
- package/dist/cli/host-helper-requests.d.ts +5 -2
- 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 +7 -1
- 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 +4 -2
- 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 +280 -25
- package/dist/cli/sporades-host-helper.js.map +1 -1
- package/dist/cli/sporades.js +251 -18
- package/dist/cli/sporades.js.map +1 -1
- package/dist/database-runtime.d.ts.map +1 -1
- package/dist/database-runtime.js +10 -0
- package/dist/database-runtime.js.map +1 -1
- package/dist/deploy-files.d.ts +47 -0
- package/dist/deploy-files.d.ts.map +1 -0
- package/dist/deploy-files.js +399 -0
- package/dist/deploy-files.js.map +1 -0
- package/dist/file-storage-runtime.d.ts +12 -1
- package/dist/file-storage-runtime.d.ts.map +1 -1
- package/dist/file-storage-runtime.js +685 -5
- package/dist/file-storage-runtime.js.map +1 -1
- package/dist/generated-source-manifest.json +12 -2
- package/dist/http-runtime.d.ts +4 -1
- package/dist/http-runtime.d.ts.map +1 -1
- package/dist/http-runtime.js +43 -3
- package/dist/http-runtime.js.map +1 -1
- package/dist/mail-runtime.d.ts.map +1 -1
- package/dist/mail-runtime.js +6 -3
- package/dist/mail-runtime.js.map +1 -1
- package/dist/server-runtime-source.d.ts.map +1 -1
- package/dist/server-runtime-source.js +167 -70
- package/dist/server-runtime-source.js.map +1 -1
- package/dist/templates/client-runtime-template.d.ts.map +1 -1
- package/dist/templates/client-runtime-template.js +127 -5
- package/dist/templates/client-runtime-template.js.map +1 -1
- package/dist/templates/server-bundle-entry.d.ts.map +1 -1
- package/dist/templates/server-bundle-entry.js +12 -3
- package/dist/templates/server-bundle-entry.js.map +1 -1
- package/package.json +1 -1
- package/src/types/server.d.ts +27 -8
|
@@ -5147,9 +5147,9 @@ var require_pako = __commonJS({
|
|
|
5147
5147
|
var assign = require_common().assign;
|
|
5148
5148
|
var deflate = require_deflate2();
|
|
5149
5149
|
var inflate = require_inflate2();
|
|
5150
|
-
var
|
|
5150
|
+
var constants2 = require_constants();
|
|
5151
5151
|
var pako = {};
|
|
5152
|
-
assign(pako, deflate, inflate,
|
|
5152
|
+
assign(pako, deflate, inflate, constants2);
|
|
5153
5153
|
module.exports = pako;
|
|
5154
5154
|
}
|
|
5155
5155
|
});
|
|
@@ -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(path6) {
|
|
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 = path6; _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(path6) {
|
|
17003
|
+
return apply(parse5(path6));
|
|
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(path6, 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(path6), [
|
|
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(path6, 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(path6, "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(path6, {
|
|
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(path6) {
|
|
23609
23609
|
var xhr = new XMLHttpRequest();
|
|
23610
|
-
xhr.open("GET",
|
|
23610
|
+
xhr.open("GET", path6, true);
|
|
23611
23611
|
xhr.responseType = "arraybuffer";
|
|
23612
23612
|
xhr.onload = (function() {
|
|
23613
23613
|
var data2 = new Uint8Array(xhr.response || xhr.mozResponseArrayBuffer);
|
|
@@ -24660,7 +24660,7 @@ var require_crc = __commonJS({
|
|
|
24660
24660
|
var require_parser = __commonJS({
|
|
24661
24661
|
"node_modules/pngjs/lib/parser.js"(exports, module) {
|
|
24662
24662
|
"use strict";
|
|
24663
|
-
var
|
|
24663
|
+
var constants2 = require_constants2();
|
|
24664
24664
|
var CrcCalculator = require_crc();
|
|
24665
24665
|
var Parser5 = module.exports = function(options, dependencies) {
|
|
24666
24666
|
this._options = options;
|
|
@@ -24671,12 +24671,12 @@ var require_parser = __commonJS({
|
|
|
24671
24671
|
this._palette = [];
|
|
24672
24672
|
this._colorType = 0;
|
|
24673
24673
|
this._chunks = {};
|
|
24674
|
-
this._chunks[
|
|
24675
|
-
this._chunks[
|
|
24676
|
-
this._chunks[
|
|
24677
|
-
this._chunks[
|
|
24678
|
-
this._chunks[
|
|
24679
|
-
this._chunks[
|
|
24674
|
+
this._chunks[constants2.TYPE_IHDR] = this._handleIHDR.bind(this);
|
|
24675
|
+
this._chunks[constants2.TYPE_IEND] = this._handleIEND.bind(this);
|
|
24676
|
+
this._chunks[constants2.TYPE_IDAT] = this._handleIDAT.bind(this);
|
|
24677
|
+
this._chunks[constants2.TYPE_PLTE] = this._handlePLTE.bind(this);
|
|
24678
|
+
this._chunks[constants2.TYPE_tRNS] = this._handleTRNS.bind(this);
|
|
24679
|
+
this._chunks[constants2.TYPE_gAMA] = this._handleGAMA.bind(this);
|
|
24680
24680
|
this.read = dependencies.read;
|
|
24681
24681
|
this.error = dependencies.error;
|
|
24682
24682
|
this.metadata = dependencies.metadata;
|
|
@@ -24691,10 +24691,10 @@ var require_parser = __commonJS({
|
|
|
24691
24691
|
};
|
|
24692
24692
|
};
|
|
24693
24693
|
Parser5.prototype.start = function() {
|
|
24694
|
-
this.read(
|
|
24694
|
+
this.read(constants2.PNG_SIGNATURE.length, this._parseSignature.bind(this));
|
|
24695
24695
|
};
|
|
24696
24696
|
Parser5.prototype._parseSignature = function(data2) {
|
|
24697
|
-
let signature =
|
|
24697
|
+
let signature = constants2.PNG_SIGNATURE;
|
|
24698
24698
|
for (let i = 0; i < signature.length; i++) {
|
|
24699
24699
|
if (data2[i] !== signature[i]) {
|
|
24700
24700
|
this.error(new Error("Invalid file signature"));
|
|
@@ -24711,7 +24711,7 @@ var require_parser = __commonJS({
|
|
|
24711
24711
|
name2 += String.fromCharCode(data2[i]);
|
|
24712
24712
|
}
|
|
24713
24713
|
let ancillary = Boolean(data2[4] & 32);
|
|
24714
|
-
if (!this._hasIHDR && type !==
|
|
24714
|
+
if (!this._hasIHDR && type !== constants2.TYPE_IHDR) {
|
|
24715
24715
|
this.error(new Error("Expected IHDR on beggining"));
|
|
24716
24716
|
return;
|
|
24717
24717
|
}
|
|
@@ -24759,7 +24759,7 @@ var require_parser = __commonJS({
|
|
|
24759
24759
|
this.error(new Error("Unsupported bit depth " + depth));
|
|
24760
24760
|
return;
|
|
24761
24761
|
}
|
|
24762
|
-
if (!(colorType in
|
|
24762
|
+
if (!(colorType in constants2.COLORTYPE_TO_BPP_MAP)) {
|
|
24763
24763
|
this.error(new Error("Unsupported color type"));
|
|
24764
24764
|
return;
|
|
24765
24765
|
}
|
|
@@ -24776,16 +24776,16 @@ var require_parser = __commonJS({
|
|
|
24776
24776
|
return;
|
|
24777
24777
|
}
|
|
24778
24778
|
this._colorType = colorType;
|
|
24779
|
-
let bpp =
|
|
24779
|
+
let bpp = constants2.COLORTYPE_TO_BPP_MAP[this._colorType];
|
|
24780
24780
|
this._hasIHDR = true;
|
|
24781
24781
|
this.metadata({
|
|
24782
24782
|
width,
|
|
24783
24783
|
height,
|
|
24784
24784
|
depth,
|
|
24785
24785
|
interlace: Boolean(interlace),
|
|
24786
|
-
palette: Boolean(colorType &
|
|
24787
|
-
color: Boolean(colorType &
|
|
24788
|
-
alpha: Boolean(colorType &
|
|
24786
|
+
palette: Boolean(colorType & constants2.COLORTYPE_PALETTE),
|
|
24787
|
+
color: Boolean(colorType & constants2.COLORTYPE_COLOR),
|
|
24788
|
+
alpha: Boolean(colorType & constants2.COLORTYPE_ALPHA),
|
|
24789
24789
|
bpp,
|
|
24790
24790
|
colorType
|
|
24791
24791
|
});
|
|
@@ -24809,7 +24809,7 @@ var require_parser = __commonJS({
|
|
|
24809
24809
|
};
|
|
24810
24810
|
Parser5.prototype._parseTRNS = function(data2) {
|
|
24811
24811
|
this._crc.write(data2);
|
|
24812
|
-
if (this._colorType ===
|
|
24812
|
+
if (this._colorType === constants2.COLORTYPE_PALETTE_COLOR) {
|
|
24813
24813
|
if (this._palette.length === 0) {
|
|
24814
24814
|
this.error(new Error("Transparency chunk must be after palette"));
|
|
24815
24815
|
return;
|
|
@@ -24823,10 +24823,10 @@ var require_parser = __commonJS({
|
|
|
24823
24823
|
}
|
|
24824
24824
|
this.palette(this._palette);
|
|
24825
24825
|
}
|
|
24826
|
-
if (this._colorType ===
|
|
24826
|
+
if (this._colorType === constants2.COLORTYPE_GRAYSCALE) {
|
|
24827
24827
|
this.transColor([data2.readUInt16BE(0)]);
|
|
24828
24828
|
}
|
|
24829
|
-
if (this._colorType ===
|
|
24829
|
+
if (this._colorType === constants2.COLORTYPE_COLOR) {
|
|
24830
24830
|
this.transColor([
|
|
24831
24831
|
data2.readUInt16BE(0),
|
|
24832
24832
|
data2.readUInt16BE(2),
|
|
@@ -24840,7 +24840,7 @@ var require_parser = __commonJS({
|
|
|
24840
24840
|
};
|
|
24841
24841
|
Parser5.prototype._parseGAMA = function(data2) {
|
|
24842
24842
|
this._crc.write(data2);
|
|
24843
|
-
this.gamma(data2.readUInt32BE(0) /
|
|
24843
|
+
this.gamma(data2.readUInt32BE(0) / constants2.GAMMA_DIVISION);
|
|
24844
24844
|
this._handleChunkEnd();
|
|
24845
24845
|
};
|
|
24846
24846
|
Parser5.prototype._handleIDAT = function(length) {
|
|
@@ -24852,7 +24852,7 @@ var require_parser = __commonJS({
|
|
|
24852
24852
|
};
|
|
24853
24853
|
Parser5.prototype._parseIDAT = function(length, data2) {
|
|
24854
24854
|
this._crc.write(data2);
|
|
24855
|
-
if (this._colorType ===
|
|
24855
|
+
if (this._colorType === constants2.COLORTYPE_PALETTE_COLOR && this._palette.length === 0) {
|
|
24856
24856
|
throw new Error("Expected palette not found");
|
|
24857
24857
|
}
|
|
24858
24858
|
this.inflateData(data2);
|
|
@@ -25340,9 +25340,9 @@ var require_parser_async = __commonJS({
|
|
|
25340
25340
|
var require_bitpacker = __commonJS({
|
|
25341
25341
|
"node_modules/pngjs/lib/bitpacker.js"(exports, module) {
|
|
25342
25342
|
"use strict";
|
|
25343
|
-
var
|
|
25343
|
+
var constants2 = require_constants2();
|
|
25344
25344
|
module.exports = function(dataIn, width, height, options) {
|
|
25345
|
-
let outHasAlpha = [
|
|
25345
|
+
let outHasAlpha = [constants2.COLORTYPE_COLOR_ALPHA, constants2.COLORTYPE_ALPHA].indexOf(
|
|
25346
25346
|
options.colorType
|
|
25347
25347
|
) !== -1;
|
|
25348
25348
|
if (options.colorType === options.inputColorType) {
|
|
@@ -25362,11 +25362,11 @@ var require_bitpacker = __commonJS({
|
|
|
25362
25362
|
}
|
|
25363
25363
|
let data2 = options.bitDepth !== 16 ? dataIn : new Uint16Array(dataIn.buffer);
|
|
25364
25364
|
let maxValue = 255;
|
|
25365
|
-
let inBpp =
|
|
25365
|
+
let inBpp = constants2.COLORTYPE_TO_BPP_MAP[options.inputColorType];
|
|
25366
25366
|
if (inBpp === 4 && !options.inputHasAlpha) {
|
|
25367
25367
|
inBpp = 3;
|
|
25368
25368
|
}
|
|
25369
|
-
let outBpp =
|
|
25369
|
+
let outBpp = constants2.COLORTYPE_TO_BPP_MAP[options.colorType];
|
|
25370
25370
|
if (options.bitDepth === 16) {
|
|
25371
25371
|
maxValue = 65535;
|
|
25372
25372
|
outBpp *= 2;
|
|
@@ -25390,24 +25390,24 @@ var require_bitpacker = __commonJS({
|
|
|
25390
25390
|
let blue;
|
|
25391
25391
|
let alpha = maxValue;
|
|
25392
25392
|
switch (options.inputColorType) {
|
|
25393
|
-
case
|
|
25393
|
+
case constants2.COLORTYPE_COLOR_ALPHA:
|
|
25394
25394
|
alpha = data2[inIndex + 3];
|
|
25395
25395
|
red = data2[inIndex];
|
|
25396
25396
|
green = data2[inIndex + 1];
|
|
25397
25397
|
blue = data2[inIndex + 2];
|
|
25398
25398
|
break;
|
|
25399
|
-
case
|
|
25399
|
+
case constants2.COLORTYPE_COLOR:
|
|
25400
25400
|
red = data2[inIndex];
|
|
25401
25401
|
green = data2[inIndex + 1];
|
|
25402
25402
|
blue = data2[inIndex + 2];
|
|
25403
25403
|
break;
|
|
25404
|
-
case
|
|
25404
|
+
case constants2.COLORTYPE_ALPHA:
|
|
25405
25405
|
alpha = data2[inIndex + 1];
|
|
25406
25406
|
red = data2[inIndex];
|
|
25407
25407
|
green = red;
|
|
25408
25408
|
blue = red;
|
|
25409
25409
|
break;
|
|
25410
|
-
case
|
|
25410
|
+
case constants2.COLORTYPE_GRAYSCALE:
|
|
25411
25411
|
red = data2[inIndex];
|
|
25412
25412
|
green = red;
|
|
25413
25413
|
blue = red;
|
|
@@ -25440,8 +25440,8 @@ var require_bitpacker = __commonJS({
|
|
|
25440
25440
|
for (let x = 0; x < width; x++) {
|
|
25441
25441
|
let rgba = getRGBA(data2, inIndex);
|
|
25442
25442
|
switch (options.colorType) {
|
|
25443
|
-
case
|
|
25444
|
-
case
|
|
25443
|
+
case constants2.COLORTYPE_COLOR_ALPHA:
|
|
25444
|
+
case constants2.COLORTYPE_COLOR:
|
|
25445
25445
|
if (options.bitDepth === 8) {
|
|
25446
25446
|
outData[outIndex] = rgba.red;
|
|
25447
25447
|
outData[outIndex + 1] = rgba.green;
|
|
@@ -25458,8 +25458,8 @@ var require_bitpacker = __commonJS({
|
|
|
25458
25458
|
}
|
|
25459
25459
|
}
|
|
25460
25460
|
break;
|
|
25461
|
-
case
|
|
25462
|
-
case
|
|
25461
|
+
case constants2.COLORTYPE_ALPHA:
|
|
25462
|
+
case constants2.COLORTYPE_GRAYSCALE: {
|
|
25463
25463
|
let grayscale = (rgba.red + rgba.green + rgba.blue) / 3;
|
|
25464
25464
|
if (options.bitDepth === 8) {
|
|
25465
25465
|
outData[outIndex] = grayscale;
|
|
@@ -25632,7 +25632,7 @@ var require_filter_pack = __commonJS({
|
|
|
25632
25632
|
var require_packer = __commonJS({
|
|
25633
25633
|
"node_modules/pngjs/lib/packer.js"(exports, module) {
|
|
25634
25634
|
"use strict";
|
|
25635
|
-
var
|
|
25635
|
+
var constants2 = require_constants2();
|
|
25636
25636
|
var CrcStream = require_crc();
|
|
25637
25637
|
var bitPacker = require_bitpacker();
|
|
25638
25638
|
var filter = require_filter_pack();
|
|
@@ -25645,23 +25645,23 @@ var require_packer = __commonJS({
|
|
|
25645
25645
|
options.inputHasAlpha = options.inputHasAlpha != null ? options.inputHasAlpha : true;
|
|
25646
25646
|
options.deflateFactory = options.deflateFactory || zlib2.createDeflate;
|
|
25647
25647
|
options.bitDepth = options.bitDepth || 8;
|
|
25648
|
-
options.colorType = typeof options.colorType === "number" ? options.colorType :
|
|
25649
|
-
options.inputColorType = typeof options.inputColorType === "number" ? options.inputColorType :
|
|
25648
|
+
options.colorType = typeof options.colorType === "number" ? options.colorType : constants2.COLORTYPE_COLOR_ALPHA;
|
|
25649
|
+
options.inputColorType = typeof options.inputColorType === "number" ? options.inputColorType : constants2.COLORTYPE_COLOR_ALPHA;
|
|
25650
25650
|
if ([
|
|
25651
|
-
|
|
25652
|
-
|
|
25653
|
-
|
|
25654
|
-
|
|
25651
|
+
constants2.COLORTYPE_GRAYSCALE,
|
|
25652
|
+
constants2.COLORTYPE_COLOR,
|
|
25653
|
+
constants2.COLORTYPE_COLOR_ALPHA,
|
|
25654
|
+
constants2.COLORTYPE_ALPHA
|
|
25655
25655
|
].indexOf(options.colorType) === -1) {
|
|
25656
25656
|
throw new Error(
|
|
25657
25657
|
"option color type:" + options.colorType + " is not supported at present"
|
|
25658
25658
|
);
|
|
25659
25659
|
}
|
|
25660
25660
|
if ([
|
|
25661
|
-
|
|
25662
|
-
|
|
25663
|
-
|
|
25664
|
-
|
|
25661
|
+
constants2.COLORTYPE_GRAYSCALE,
|
|
25662
|
+
constants2.COLORTYPE_COLOR,
|
|
25663
|
+
constants2.COLORTYPE_COLOR_ALPHA,
|
|
25664
|
+
constants2.COLORTYPE_ALPHA
|
|
25665
25665
|
].indexOf(options.inputColorType) === -1) {
|
|
25666
25666
|
throw new Error(
|
|
25667
25667
|
"option input color type:" + options.inputColorType + " is not supported at present"
|
|
@@ -25685,7 +25685,7 @@ var require_packer = __commonJS({
|
|
|
25685
25685
|
};
|
|
25686
25686
|
Packer.prototype.filterData = function(data2, width, height) {
|
|
25687
25687
|
let packedData = bitPacker(data2, width, height, this._options);
|
|
25688
|
-
let bpp =
|
|
25688
|
+
let bpp = constants2.COLORTYPE_TO_BPP_MAP[this._options.colorType];
|
|
25689
25689
|
let filteredData = filter(packedData, width, height, this._options, bpp);
|
|
25690
25690
|
return filteredData;
|
|
25691
25691
|
};
|
|
@@ -25705,8 +25705,8 @@ var require_packer = __commonJS({
|
|
|
25705
25705
|
};
|
|
25706
25706
|
Packer.prototype.packGAMA = function(gamma) {
|
|
25707
25707
|
let buf = Buffer.alloc(4);
|
|
25708
|
-
buf.writeUInt32BE(Math.floor(gamma *
|
|
25709
|
-
return this._packChunk(
|
|
25708
|
+
buf.writeUInt32BE(Math.floor(gamma * constants2.GAMMA_DIVISION), 0);
|
|
25709
|
+
return this._packChunk(constants2.TYPE_gAMA, buf);
|
|
25710
25710
|
};
|
|
25711
25711
|
Packer.prototype.packIHDR = function(width, height) {
|
|
25712
25712
|
let buf = Buffer.alloc(13);
|
|
@@ -25717,13 +25717,13 @@ var require_packer = __commonJS({
|
|
|
25717
25717
|
buf[10] = 0;
|
|
25718
25718
|
buf[11] = 0;
|
|
25719
25719
|
buf[12] = 0;
|
|
25720
|
-
return this._packChunk(
|
|
25720
|
+
return this._packChunk(constants2.TYPE_IHDR, buf);
|
|
25721
25721
|
};
|
|
25722
25722
|
Packer.prototype.packIDAT = function(data2) {
|
|
25723
|
-
return this._packChunk(
|
|
25723
|
+
return this._packChunk(constants2.TYPE_IDAT, data2);
|
|
25724
25724
|
};
|
|
25725
25725
|
Packer.prototype.packIEND = function() {
|
|
25726
|
-
return this._packChunk(
|
|
25726
|
+
return this._packChunk(constants2.TYPE_IEND, null);
|
|
25727
25727
|
};
|
|
25728
25728
|
}
|
|
25729
25729
|
});
|
|
@@ -25734,7 +25734,7 @@ var require_packer_async = __commonJS({
|
|
|
25734
25734
|
"use strict";
|
|
25735
25735
|
var util = __require("util");
|
|
25736
25736
|
var Stream = __require("stream");
|
|
25737
|
-
var
|
|
25737
|
+
var constants2 = require_constants2();
|
|
25738
25738
|
var Packer = require_packer();
|
|
25739
25739
|
var PackerAsync = module.exports = function(opt) {
|
|
25740
25740
|
Stream.call(this);
|
|
@@ -25745,7 +25745,7 @@ var require_packer_async = __commonJS({
|
|
|
25745
25745
|
};
|
|
25746
25746
|
util.inherits(PackerAsync, Stream);
|
|
25747
25747
|
PackerAsync.prototype.pack = function(data2, width, height, gamma) {
|
|
25748
|
-
this.emit("data", Buffer.from(
|
|
25748
|
+
this.emit("data", Buffer.from(constants2.PNG_SIGNATURE));
|
|
25749
25749
|
this.emit("data", this._packer.packIHDR(width, height));
|
|
25750
25750
|
if (gamma) {
|
|
25751
25751
|
this.emit("data", this._packer.packGAMA(gamma));
|
|
@@ -26073,7 +26073,7 @@ var require_packer_sync = __commonJS({
|
|
|
26073
26073
|
if (!zlib2.deflateSync) {
|
|
26074
26074
|
hasSyncZlib = false;
|
|
26075
26075
|
}
|
|
26076
|
-
var
|
|
26076
|
+
var constants2 = require_constants2();
|
|
26077
26077
|
var Packer = require_packer();
|
|
26078
26078
|
module.exports = function(metaData, opt) {
|
|
26079
26079
|
if (!hasSyncZlib) {
|
|
@@ -26084,7 +26084,7 @@ var require_packer_sync = __commonJS({
|
|
|
26084
26084
|
let options = opt || {};
|
|
26085
26085
|
let packer = new Packer(options);
|
|
26086
26086
|
let chunks = [];
|
|
26087
|
-
chunks.push(Buffer.from(
|
|
26087
|
+
chunks.push(Buffer.from(constants2.PNG_SIGNATURE));
|
|
26088
26088
|
chunks.push(packer.packIHDR(metaData.width, metaData.height));
|
|
26089
26089
|
if (metaData.gamma) {
|
|
26090
26090
|
chunks.push(packer.packGAMA(metaData.gamma));
|
|
@@ -26261,13 +26261,421 @@ var require_png2 = __commonJS({
|
|
|
26261
26261
|
}
|
|
26262
26262
|
});
|
|
26263
26263
|
|
|
26264
|
+
// src/deploy-files.ts
|
|
26265
|
+
import path from "node:path";
|
|
26266
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
26267
|
+
import { constants } from "node:fs";
|
|
26268
|
+
import { lstat, mkdir, open, readFile, readdir, link, rename, rm, realpath } from "node:fs/promises";
|
|
26269
|
+
var RESERVED = [".sporades", "public", "data", "server.mjs", "client.js", "index.html", "sporades.json", ".env.sporades.server"];
|
|
26270
|
+
function resolveDeployFiles(value) {
|
|
26271
|
+
if (value === void 0) return [];
|
|
26272
|
+
if (!Array.isArray(value)) throw new Error("deploy.files must be an array.");
|
|
26273
|
+
const root = path.resolve("/app");
|
|
26274
|
+
const files = value.map((entry) => {
|
|
26275
|
+
if (!entry || typeof entry !== "object" || typeof entry.path !== "string" || !entry.path || path.isAbsolute(entry.path) || /[\\\x00-\x1f:]/.test(entry.path)) {
|
|
26276
|
+
throw new Error("Invalid deploy.files path: use a relative file path under the app root.");
|
|
26277
|
+
}
|
|
26278
|
+
const resolved = path.resolve(root, entry.path);
|
|
26279
|
+
const relative = path.relative(root, resolved);
|
|
26280
|
+
if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
26281
|
+
throw new Error(`deploy.files path escapes the app root: ${entry.path}`);
|
|
26282
|
+
}
|
|
26283
|
+
const reservedCandidate = resolved.toLowerCase();
|
|
26284
|
+
for (const reserved of RESERVED) {
|
|
26285
|
+
const target = path.resolve(root, reserved);
|
|
26286
|
+
if (reservedCandidate === target || reservedCandidate.startsWith(`${target}${path.sep}`) || target.startsWith(`${reservedCandidate}${path.sep}`)) {
|
|
26287
|
+
throw new Error(`deploy.files path collides with Sporades-managed files: ${entry.path}`);
|
|
26288
|
+
}
|
|
26289
|
+
}
|
|
26290
|
+
const normalized = relative.split(path.sep).join("/");
|
|
26291
|
+
if (normalized.split("/").some((part) => part.startsWith("-") || part.startsWith("._") || part === "__MACOSX")) {
|
|
26292
|
+
throw new Error(`Unsupported deploy.files path: ${entry.path}`);
|
|
26293
|
+
}
|
|
26294
|
+
const update = entry.update === void 0 ? "replace" : entry.update;
|
|
26295
|
+
if (update !== "replace" && update !== "preserve") throw new Error(`Invalid deploy.files update for ${entry.path}: use replace or preserve.`);
|
|
26296
|
+
return { path: normalized, update };
|
|
26297
|
+
});
|
|
26298
|
+
const seen = [];
|
|
26299
|
+
for (const file of files) {
|
|
26300
|
+
const name2 = file.path.normalize("NFC");
|
|
26301
|
+
if (seen.some((other) => name2 === other || name2.startsWith(`${other}/`) || other.startsWith(`${name2}/`))) {
|
|
26302
|
+
throw new Error(`Conflicting deploy.files paths: ${file.path}`);
|
|
26303
|
+
}
|
|
26304
|
+
seen.push(name2);
|
|
26305
|
+
}
|
|
26306
|
+
return files;
|
|
26307
|
+
}
|
|
26308
|
+
async function assertDeployFile(root, relative, recoverSeed = false) {
|
|
26309
|
+
const rootInfo = await lstat(root);
|
|
26310
|
+
if (!rootInfo.isDirectory() || rootInfo.isSymbolicLink()) throw new Error(`Unsafe deploy.files root: ${root}`);
|
|
26311
|
+
let current2 = root;
|
|
26312
|
+
const parts = relative.split("/");
|
|
26313
|
+
for (let index = 0; index < parts.length; index++) {
|
|
26314
|
+
current2 = path.join(current2, parts[index]);
|
|
26315
|
+
let info = await lstat(current2);
|
|
26316
|
+
if (recoverSeed && index === parts.length - 1 && info.isFile() && info.nlink === 2) {
|
|
26317
|
+
for (const entry of await readdir(path.dirname(current2))) {
|
|
26318
|
+
if (!/^\.(?:seed|rollback)-[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}$/.test(entry)) continue;
|
|
26319
|
+
const seed = path.join(path.dirname(current2), entry);
|
|
26320
|
+
const candidate = await lstat(seed).catch((error) => {
|
|
26321
|
+
if (error.code !== "ENOENT") throw error;
|
|
26322
|
+
return null;
|
|
26323
|
+
});
|
|
26324
|
+
if (candidate?.isFile() && candidate.dev === info.dev && candidate.ino === info.ino) {
|
|
26325
|
+
await rm(seed, { force: true });
|
|
26326
|
+
info = await lstat(current2);
|
|
26327
|
+
break;
|
|
26328
|
+
}
|
|
26329
|
+
}
|
|
26330
|
+
}
|
|
26331
|
+
if (info.isSymbolicLink() || (index < parts.length - 1 ? !info.isDirectory() : !info.isFile() || recoverSeed && info.nlink !== 1)) {
|
|
26332
|
+
throw new Error(`deploy.files requires regular files without symlinks: ${relative}`);
|
|
26333
|
+
}
|
|
26334
|
+
}
|
|
26335
|
+
return current2;
|
|
26336
|
+
}
|
|
26337
|
+
function preservedDeployFilePath(root, relative) {
|
|
26338
|
+
const key = createHash("sha256").update(relative.normalize("NFC")).digest("hex");
|
|
26339
|
+
return path.join(root, `${key}.file`);
|
|
26340
|
+
}
|
|
26341
|
+
async function assertPreservedDeployFile(root, relative) {
|
|
26342
|
+
return assertDeployFile(root, path.basename(preservedDeployFilePath(root, relative)), true);
|
|
26343
|
+
}
|
|
26344
|
+
async function readDeployFile(root, relative) {
|
|
26345
|
+
root = path.resolve(root);
|
|
26346
|
+
const rootHandle = await open(root, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
26347
|
+
const handles = [rootHandle];
|
|
26348
|
+
try {
|
|
26349
|
+
const identity = await rootHandle.stat();
|
|
26350
|
+
await assertDeployFile(root, relative);
|
|
26351
|
+
const rootNow = await lstat(root);
|
|
26352
|
+
if (rootNow.isSymbolicLink() || rootNow.dev !== identity.dev || rootNow.ino !== identity.ino) throw new Error("deploy.files project root changed during the build.");
|
|
26353
|
+
let file;
|
|
26354
|
+
let checkRoot;
|
|
26355
|
+
if (process.platform === "darwin") {
|
|
26356
|
+
const canonicalRoot = path.join(await realpath(path.dirname(root)), path.basename(root));
|
|
26357
|
+
checkRoot = async () => {
|
|
26358
|
+
const current2 = await lstat(canonicalRoot);
|
|
26359
|
+
if (current2.isSymbolicLink() || current2.dev !== identity.dev || current2.ino !== identity.ino) throw new Error("deploy.files project root changed during the build.");
|
|
26360
|
+
};
|
|
26361
|
+
await checkRoot();
|
|
26362
|
+
const O_NOFOLLOW_ANY = 536870912;
|
|
26363
|
+
file = await open(path.join(canonicalRoot, relative), constants.O_RDONLY | constants.O_NONBLOCK | O_NOFOLLOW_ANY);
|
|
26364
|
+
} else if (process.platform === "linux") {
|
|
26365
|
+
let directory = rootHandle;
|
|
26366
|
+
const parts = relative.split("/");
|
|
26367
|
+
for (const part of parts.slice(0, -1)) {
|
|
26368
|
+
directory = await open(`/proc/self/fd/${directory.fd}/${part}`, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
26369
|
+
handles.push(directory);
|
|
26370
|
+
}
|
|
26371
|
+
file = await open(`/proc/self/fd/${directory.fd}/${parts.at(-1)}`, constants.O_RDONLY | constants.O_NONBLOCK | constants.O_NOFOLLOW);
|
|
26372
|
+
} else {
|
|
26373
|
+
const target = path.join(root, relative);
|
|
26374
|
+
file = await open(target, constants.O_RDONLY | constants.O_NONBLOCK);
|
|
26375
|
+
handles.push(file);
|
|
26376
|
+
const opened = await file.stat();
|
|
26377
|
+
await assertDeployFile(root, relative);
|
|
26378
|
+
const named = await lstat(target);
|
|
26379
|
+
if (!opened.isFile() || named.dev !== opened.dev || named.ino !== opened.ino) throw new Error(`deploy.files source changed during the build: ${relative}`);
|
|
26380
|
+
return await file.readFile();
|
|
26381
|
+
}
|
|
26382
|
+
handles.push(file);
|
|
26383
|
+
await checkRoot?.();
|
|
26384
|
+
if (!(await file.stat()).isFile()) throw new Error(`deploy.files requires a regular file: ${relative}`);
|
|
26385
|
+
return await file.readFile();
|
|
26386
|
+
} finally {
|
|
26387
|
+
for (const handle of handles.reverse()) await handle.close();
|
|
26388
|
+
}
|
|
26389
|
+
}
|
|
26390
|
+
function deployFileMounts(files, releaseRoot, preservedRoot) {
|
|
26391
|
+
return files.map((file) => ({
|
|
26392
|
+
host: file.update === "preserve" ? preservedDeployFilePath(preservedRoot, file.path) : path.join(releaseRoot, file.path),
|
|
26393
|
+
container: `/app/${file.path}`,
|
|
26394
|
+
mode: file.update === "preserve" ? "rw" : "ro"
|
|
26395
|
+
}));
|
|
26396
|
+
}
|
|
26397
|
+
function attemptJournalPath(preservedRoot) {
|
|
26398
|
+
return path.join(path.dirname(preservedRoot), "deploy-file-attempt.jsonl");
|
|
26399
|
+
}
|
|
26400
|
+
function interruptedAttemptError(journal) {
|
|
26401
|
+
return new Error(`Interrupted deploy.files attempt requires recovery: ${journal}. Run the matching reconcile command before retrying.`);
|
|
26402
|
+
}
|
|
26403
|
+
async function assertNoPreservedFileAttempt(preservedRoot) {
|
|
26404
|
+
const journal = attemptJournalPath(preservedRoot);
|
|
26405
|
+
try {
|
|
26406
|
+
await lstat(journal);
|
|
26407
|
+
} catch (error) {
|
|
26408
|
+
if (error.code === "ENOENT") return;
|
|
26409
|
+
throw error;
|
|
26410
|
+
}
|
|
26411
|
+
throw interruptedAttemptError(journal);
|
|
26412
|
+
}
|
|
26413
|
+
async function beginPreservedFileAttempt(preservedRoot, release, needed) {
|
|
26414
|
+
if (!needed) {
|
|
26415
|
+
await assertNoPreservedFileAttempt(preservedRoot);
|
|
26416
|
+
return void 0;
|
|
26417
|
+
}
|
|
26418
|
+
const journal = attemptJournalPath(preservedRoot);
|
|
26419
|
+
let handle;
|
|
26420
|
+
try {
|
|
26421
|
+
handle = await open(journal, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
|
|
26422
|
+
} catch (error) {
|
|
26423
|
+
if (error.code === "EEXIST") throw interruptedAttemptError(journal);
|
|
26424
|
+
throw error;
|
|
26425
|
+
}
|
|
26426
|
+
try {
|
|
26427
|
+
await handle.writeFile(JSON.stringify({ release, preservedRoot }) + "\n");
|
|
26428
|
+
await handle.sync();
|
|
26429
|
+
} finally {
|
|
26430
|
+
await handle.close();
|
|
26431
|
+
}
|
|
26432
|
+
return journal;
|
|
26433
|
+
}
|
|
26434
|
+
async function readPreservedFileAttempt(journal) {
|
|
26435
|
+
const handle = await open(journal, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK).catch((error) => {
|
|
26436
|
+
if (error.code !== "ENOENT") throw error;
|
|
26437
|
+
return null;
|
|
26438
|
+
});
|
|
26439
|
+
if (!handle) return null;
|
|
26440
|
+
let records;
|
|
26441
|
+
try {
|
|
26442
|
+
if (!(await handle.stat()).isFile()) throw new Error("Unsafe deploy.files journal.");
|
|
26443
|
+
const contents = (await handle.readFile("utf8")).trim();
|
|
26444
|
+
records = contents ? contents.split("\n").map((line) => JSON.parse(line)) : [];
|
|
26445
|
+
} finally {
|
|
26446
|
+
await handle.close();
|
|
26447
|
+
}
|
|
26448
|
+
const header = records[0] ?? {};
|
|
26449
|
+
const release = typeof header.release === "string" ? header.release : "";
|
|
26450
|
+
const preservedRoot = typeof header.preservedRoot === "string" ? header.preservedRoot : path.join(path.dirname(journal), "preserved-files");
|
|
26451
|
+
const seeds = records.filter((record) => typeof record.root === "string" && typeof record.path === "string" && typeof record.dev === "number" && typeof record.ino === "number" && typeof record.sha256 === "string");
|
|
26452
|
+
const temporaries = records.map((record) => record.temporary).filter((value) => typeof value === "string");
|
|
26453
|
+
return { journal, release, preservedRoot, seeds: seeds.map((seed) => ({ ...seed })), temporaries, records };
|
|
26454
|
+
}
|
|
26455
|
+
async function recordPreservedFileAttempt(journal, entry) {
|
|
26456
|
+
if (!journal) return;
|
|
26457
|
+
const handle = await open(journal, constants.O_WRONLY | constants.O_APPEND | constants.O_NOFOLLOW);
|
|
26458
|
+
try {
|
|
26459
|
+
await handle.writeFile(JSON.stringify(entry) + "\n");
|
|
26460
|
+
await handle.sync();
|
|
26461
|
+
} finally {
|
|
26462
|
+
await handle.close();
|
|
26463
|
+
}
|
|
26464
|
+
}
|
|
26465
|
+
async function finishPreservedFileAttempt(journal) {
|
|
26466
|
+
if (!journal) return;
|
|
26467
|
+
const attempt = await readPreservedFileAttempt(journal);
|
|
26468
|
+
if (!attempt) return;
|
|
26469
|
+
const root = path.join(path.dirname(journal), "preserved-files");
|
|
26470
|
+
for (const relative of attempt.temporaries) {
|
|
26471
|
+
const resolved = path.resolve(root, relative);
|
|
26472
|
+
if (path.isAbsolute(relative) || relative === ".." || relative.startsWith(`..${path.sep}`) || path.relative(root, resolved) !== relative || !/^\.seed-[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}$/.test(path.basename(relative))) {
|
|
26473
|
+
throw new Error("Unsafe temporary seed path in deploy.files journal.");
|
|
26474
|
+
}
|
|
26475
|
+
try {
|
|
26476
|
+
await rm(await assertDeployFile(root, relative), { force: true });
|
|
26477
|
+
} catch (error) {
|
|
26478
|
+
if (error.code !== "ENOENT") throw error;
|
|
26479
|
+
}
|
|
26480
|
+
}
|
|
26481
|
+
await rm(journal, { force: true });
|
|
26482
|
+
}
|
|
26483
|
+
async function preparePreservedFiles(files, releaseRoot, preservedRoot, owner, created = [], journal) {
|
|
26484
|
+
for (const file of files.filter((entry) => entry.update === "preserve")) {
|
|
26485
|
+
await mkdir(preservedRoot, { mode: 448 }).catch((error) => {
|
|
26486
|
+
if (error.code !== "EEXIST") throw error;
|
|
26487
|
+
});
|
|
26488
|
+
const directory = await lstat(preservedRoot);
|
|
26489
|
+
if (!directory.isDirectory() || directory.isSymbolicLink()) throw new Error(`Unsafe preserved deploy.files directory: ${file.path}`);
|
|
26490
|
+
const rootHandle = await open(preservedRoot, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
26491
|
+
try {
|
|
26492
|
+
await rootHandle.chmod(448);
|
|
26493
|
+
} finally {
|
|
26494
|
+
await rootHandle.close();
|
|
26495
|
+
}
|
|
26496
|
+
const destination = preservedDeployFilePath(preservedRoot, file.path);
|
|
26497
|
+
try {
|
|
26498
|
+
await assertPreservedDeployFile(preservedRoot, file.path);
|
|
26499
|
+
continue;
|
|
26500
|
+
} catch (error) {
|
|
26501
|
+
if (error.code !== "ENOENT") throw error;
|
|
26502
|
+
}
|
|
26503
|
+
let handle;
|
|
26504
|
+
const temporary = path.join(path.dirname(destination), `.seed-${randomUUID()}`);
|
|
26505
|
+
try {
|
|
26506
|
+
await recordPreservedFileAttempt(journal, { temporary: path.relative(preservedRoot, temporary) });
|
|
26507
|
+
const contents = await readDeployFile(releaseRoot, file.path);
|
|
26508
|
+
handle = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
|
|
26509
|
+
await handle.writeFile(contents);
|
|
26510
|
+
if (owner) await owner(handle, destination, await handle.stat());
|
|
26511
|
+
const identity = await handle.stat();
|
|
26512
|
+
const seed = { root: preservedRoot, path: file.path, storagePath: destination, dev: identity.dev, ino: identity.ino, sha256: createHash("sha256").update(contents).digest("hex") };
|
|
26513
|
+
await recordPreservedFileAttempt(journal, seed);
|
|
26514
|
+
await link(temporary, destination);
|
|
26515
|
+
created.push(seed);
|
|
26516
|
+
} catch (error) {
|
|
26517
|
+
if (error.code !== "EEXIST") throw error;
|
|
26518
|
+
} finally {
|
|
26519
|
+
await handle?.close();
|
|
26520
|
+
await rm(temporary, { force: true });
|
|
26521
|
+
}
|
|
26522
|
+
await assertPreservedDeployFile(preservedRoot, file.path);
|
|
26523
|
+
}
|
|
26524
|
+
}
|
|
26525
|
+
async function rollbackPreservedFiles(created, hooks = {}) {
|
|
26526
|
+
for (const seed of [...created].reverse()) {
|
|
26527
|
+
let handle;
|
|
26528
|
+
try {
|
|
26529
|
+
const target = await assertPreservedDeployFile(seed.root, seed.path);
|
|
26530
|
+
handle = await open(target, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
26531
|
+
const info = await handle.stat();
|
|
26532
|
+
if (info.dev !== seed.dev || info.ino !== seed.ino || info.nlink !== 1) continue;
|
|
26533
|
+
if (createHash("sha256").update(await handle.readFile()).digest("hex") !== seed.sha256) continue;
|
|
26534
|
+
await hooks.beforeClaim?.(target);
|
|
26535
|
+
const claimed = path.join(seed.root, `.rollback-${randomUUID()}`);
|
|
26536
|
+
await rename(target, claimed);
|
|
26537
|
+
const captured = await lstat(claimed);
|
|
26538
|
+
const sameSeed = captured.isFile() && captured.dev === info.dev && captured.ino === info.ino && createHash("sha256").update(await readFile(claimed)).digest("hex") === seed.sha256;
|
|
26539
|
+
if (!sameSeed) {
|
|
26540
|
+
try {
|
|
26541
|
+
await link(claimed, target);
|
|
26542
|
+
await rm(claimed);
|
|
26543
|
+
} catch (error) {
|
|
26544
|
+
if (error.code !== "EEXIST") throw error;
|
|
26545
|
+
}
|
|
26546
|
+
}
|
|
26547
|
+
} catch (error) {
|
|
26548
|
+
if (error.code !== "ENOENT") throw error;
|
|
26549
|
+
} finally {
|
|
26550
|
+
await handle?.close();
|
|
26551
|
+
}
|
|
26552
|
+
}
|
|
26553
|
+
}
|
|
26554
|
+
async function rethrowAfterDeployCleanup(error, cleanups) {
|
|
26555
|
+
const failures = [];
|
|
26556
|
+
for (const cleanup of cleanups) {
|
|
26557
|
+
try {
|
|
26558
|
+
await cleanup();
|
|
26559
|
+
} catch (failure) {
|
|
26560
|
+
failures.push(failure);
|
|
26561
|
+
}
|
|
26562
|
+
}
|
|
26563
|
+
if (failures.length) throw new AggregateError([error, ...failures], "Deployment failed and cleanup is incomplete.");
|
|
26564
|
+
throw error;
|
|
26565
|
+
}
|
|
26566
|
+
|
|
26567
|
+
// src/cli/host-domain-aliases.ts
|
|
26568
|
+
import { readdir as readdir2, readFile as readFile2 } from "node:fs/promises";
|
|
26569
|
+
import path2 from "node:path";
|
|
26570
|
+
|
|
26571
|
+
// src/cli/cli-support.ts
|
|
26572
|
+
function errorDetails(error) {
|
|
26573
|
+
if (error === null || error === void 0) {
|
|
26574
|
+
return {};
|
|
26575
|
+
}
|
|
26576
|
+
return typeof error === "object" ? error : { message: String(error) };
|
|
26577
|
+
}
|
|
26578
|
+
function helperError(message, hint, diagnostics = null) {
|
|
26579
|
+
const error = new Error(message);
|
|
26580
|
+
error.hint = hint;
|
|
26581
|
+
if (diagnostics) {
|
|
26582
|
+
error.diagnostics = diagnostics;
|
|
26583
|
+
}
|
|
26584
|
+
return error;
|
|
26585
|
+
}
|
|
26586
|
+
function readStdin() {
|
|
26587
|
+
return new Promise((resolve, reject) => {
|
|
26588
|
+
let stdin = "";
|
|
26589
|
+
process.stdin.setEncoding("utf8");
|
|
26590
|
+
process.stdin.on("data", (chunk) => {
|
|
26591
|
+
stdin += chunk;
|
|
26592
|
+
});
|
|
26593
|
+
process.stdin.on("end", () => resolve(stdin));
|
|
26594
|
+
process.stdin.on("error", reject);
|
|
26595
|
+
});
|
|
26596
|
+
}
|
|
26597
|
+
function delay(ms) {
|
|
26598
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
26599
|
+
}
|
|
26600
|
+
function writeResult(result, failed = false) {
|
|
26601
|
+
process.stdout.write(`${JSON.stringify(result)}
|
|
26602
|
+
`);
|
|
26603
|
+
if (failed) {
|
|
26604
|
+
process.exitCode = 1;
|
|
26605
|
+
}
|
|
26606
|
+
}
|
|
26607
|
+
function writeEnvelope(result, failed = false) {
|
|
26608
|
+
writeResult(result, failed);
|
|
26609
|
+
}
|
|
26610
|
+
|
|
26611
|
+
// src/cli/host-domain-aliases.ts
|
|
26612
|
+
function validateAliasDomains(value) {
|
|
26613
|
+
if (value === void 0) return [];
|
|
26614
|
+
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)))) {
|
|
26615
|
+
throw helperError(
|
|
26616
|
+
"Invalid Hosted Capsule alias domains.",
|
|
26617
|
+
"Supply at most 20 unique lowercase DNS hostnames, without schemes, ports, paths, wildcards, or IP addresses."
|
|
26618
|
+
);
|
|
26619
|
+
}
|
|
26620
|
+
if (new Set(value).size !== value.length) {
|
|
26621
|
+
throw helperError("Duplicate Hosted Capsule alias domain.", "Supply each alias domain once.");
|
|
26622
|
+
}
|
|
26623
|
+
return [...value];
|
|
26624
|
+
}
|
|
26625
|
+
async function assertHostnamesAvailable(remoteRoot, hostnames, owner) {
|
|
26626
|
+
const wanted = new Set(hostnames.map((hostname) => hostname.replace(/:\d+$/, "")));
|
|
26627
|
+
if (wanted.size !== hostnames.length) {
|
|
26628
|
+
throw helperError("Duplicate Hosted Capsule hostname.", "An alias must differ from the Capsule subdomain.");
|
|
26629
|
+
}
|
|
26630
|
+
const assertUnclaimed = (hostname, claimant) => {
|
|
26631
|
+
if (claimant !== owner && wanted.has(hostname.replace(/:\d+$/, ""))) {
|
|
26632
|
+
throw helperError(
|
|
26633
|
+
"Hosted Capsule hostname is already reserved.",
|
|
26634
|
+
`Choose another hostname; ${hostname} belongs to ${claimant}.`
|
|
26635
|
+
);
|
|
26636
|
+
}
|
|
26637
|
+
};
|
|
26638
|
+
const entries = async (directory) => readdir2(directory, { withFileTypes: true }).catch((error) => {
|
|
26639
|
+
if (error.code === "ENOENT") return [];
|
|
26640
|
+
throw error;
|
|
26641
|
+
});
|
|
26642
|
+
for (const entry of await entries(path2.join(remoteRoot, "caddy", "hosts"))) {
|
|
26643
|
+
if (entry.name.endsWith(".caddy")) {
|
|
26644
|
+
const domain = entry.name.slice(0, -6);
|
|
26645
|
+
assertUnclaimed(`host.${domain}`, `health:${domain}`);
|
|
26646
|
+
}
|
|
26647
|
+
}
|
|
26648
|
+
for (const domain of await entries(path2.join(remoteRoot, "hosts"))) {
|
|
26649
|
+
if (domain.isSymbolicLink()) throw helperError("Cannot inspect Hosted domain ownership.", "Replace symbolic links in the Host registry with canonical directories.");
|
|
26650
|
+
if (!domain.isDirectory()) continue;
|
|
26651
|
+
for (const collection of ["capsules", "registration-claims"]) {
|
|
26652
|
+
const directory = path2.join(remoteRoot, "hosts", domain.name, "registry", collection);
|
|
26653
|
+
for (const entry of await entries(directory)) {
|
|
26654
|
+
if (!entry.name.endsWith(".json")) continue;
|
|
26655
|
+
if (!entry.isFile()) throw helperError("Cannot inspect Hosted Capsule ownership.", "Repair the Host registry before registering domains.");
|
|
26656
|
+
const record = JSON.parse(await readFile2(path2.join(directory, entry.name), "utf8"));
|
|
26657
|
+
if (record.domain !== domain.name || record.subname !== entry.name.slice(0, -5)) {
|
|
26658
|
+
throw helperError("Invalid Hosted Capsule registry identity.", "Repair the Host registry before registering domains.");
|
|
26659
|
+
}
|
|
26660
|
+
if (collection === "capsules" && record.status === "unregistered") continue;
|
|
26661
|
+
const claimant = `${record.domain}/${record.subname}`;
|
|
26662
|
+
const aliases = validateAliasDomains(record.aliasDomains);
|
|
26663
|
+
const previousAliases = collection === "registration-claims" ? validateAliasDomains(record.previousAliasDomains) : [];
|
|
26664
|
+
for (const hostname of [`${record.subname}.${record.domain}`, ...aliases, ...previousAliases]) {
|
|
26665
|
+
assertUnclaimed(hostname, claimant);
|
|
26666
|
+
}
|
|
26667
|
+
}
|
|
26668
|
+
}
|
|
26669
|
+
}
|
|
26670
|
+
}
|
|
26671
|
+
|
|
26264
26672
|
// src/cli/sporades-host-helper.ts
|
|
26265
26673
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
26266
26674
|
import { constants as fsConstants, createReadStream, statSync } from "node:fs";
|
|
26267
|
-
import { access, chmod, lstat, mkdir, open, opendir, readdir, readFile as
|
|
26268
|
-
import { createHash, generateKeyPairSync, randomBytes } from "node:crypto";
|
|
26675
|
+
import { access, chmod, lstat as lstat2, mkdir as mkdir2, open as open2, opendir, readdir as readdir3, readFile as readFile4, readlink, rename as rename2, rm as rm2, stat, statfs, symlink, writeFile } from "node:fs/promises";
|
|
26676
|
+
import { createHash as createHash2, generateKeyPairSync, randomBytes } from "node:crypto";
|
|
26269
26677
|
import { freemem, loadavg, totalmem } from "node:os";
|
|
26270
|
-
import
|
|
26678
|
+
import path5 from "node:path";
|
|
26271
26679
|
import { isDeepStrictEqual } from "node:util";
|
|
26272
26680
|
|
|
26273
26681
|
// src/base-image.ts
|
|
@@ -26422,7 +26830,7 @@ function validatePublicTreeFileSet(files) {
|
|
|
26422
26830
|
}
|
|
26423
26831
|
|
|
26424
26832
|
// src/log-envelope.ts
|
|
26425
|
-
import { randomUUID } from "node:crypto";
|
|
26833
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
26426
26834
|
function uncappedLogEnvelope(input) {
|
|
26427
26835
|
const config = input.config ?? {};
|
|
26428
26836
|
const capsuleName = String(config.name ?? "unknown");
|
|
@@ -26439,7 +26847,7 @@ function uncappedLogEnvelope(input) {
|
|
|
26439
26847
|
},
|
|
26440
26848
|
release: input.release ?? config.release ?? null,
|
|
26441
26849
|
request: input.request ? {
|
|
26442
|
-
id: input.request.id ??
|
|
26850
|
+
id: input.request.id ?? randomUUID2(),
|
|
26443
26851
|
method: input.request.method ?? null,
|
|
26444
26852
|
path: input.request.path ?? null
|
|
26445
26853
|
} : null,
|
|
@@ -26567,6 +26975,7 @@ var ACL_HELPER_STATE = Symbol("sporades.aclHelperState");
|
|
|
26567
26975
|
|
|
26568
26976
|
// src/file-storage-runtime.ts
|
|
26569
26977
|
var nodeCryptoModule2 = process.getBuiltinModule("node:crypto");
|
|
26978
|
+
var nodePromiseHooks = process.getBuiltinModule("node:v8")?.promiseHooks;
|
|
26570
26979
|
|
|
26571
26980
|
// src/file-ingress-runtime.ts
|
|
26572
26981
|
var import_pdf_lib = __toESM(require_cjs(), 1);
|
|
@@ -40519,17 +40928,17 @@ var Lexer = class _Lexer {
|
|
|
40519
40928
|
if (next < end && this.src.charCodeAt(next) === CH_LBRACE) {
|
|
40520
40929
|
close = this.findClosingBrace(next + 1, end);
|
|
40521
40930
|
} else {
|
|
40522
|
-
const
|
|
40523
|
-
if (
|
|
40931
|
+
const open3 = skipLineContinuations(this.src, next, end);
|
|
40932
|
+
if (open3 >= end || this.src.charCodeAt(open3) !== CH_LPAREN)
|
|
40524
40933
|
return start;
|
|
40525
|
-
close = this.findClosingParenthesis(
|
|
40934
|
+
close = this.findClosingParenthesis(open3 + 1, end);
|
|
40526
40935
|
}
|
|
40527
40936
|
} else {
|
|
40528
|
-
const
|
|
40529
|
-
if (quoted || ch !== CH_LT && ch !== CH_GT ||
|
|
40937
|
+
const open3 = start + 1;
|
|
40938
|
+
if (quoted || ch !== CH_LT && ch !== CH_GT || open3 >= end || this.src.charCodeAt(open3) !== CH_LPAREN) {
|
|
40530
40939
|
return start;
|
|
40531
40940
|
}
|
|
40532
|
-
close = this.findClosingParenthesis(
|
|
40941
|
+
close = this.findClosingParenthesis(open3 + 1, end);
|
|
40533
40942
|
}
|
|
40534
40943
|
return close === -1 ? end : close + 1;
|
|
40535
40944
|
}
|
|
@@ -42546,48 +42955,8 @@ var serviceUserMutationAuthority = Object.freeze({ kind: "service-user-mutation-
|
|
|
42546
42955
|
var MutationExecutionStorage = process.getBuiltinModule("node:async_hooks").AsyncLocalStorage;
|
|
42547
42956
|
var mutationExecution = new MutationExecutionStorage();
|
|
42548
42957
|
|
|
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
42958
|
// src/cli/cli-version.ts
|
|
42590
|
-
var CLI_VERSION = "0.9.
|
|
42959
|
+
var CLI_VERSION = "0.9.23";
|
|
42591
42960
|
|
|
42592
42961
|
// src/cli/schedule-inspection-envelope.ts
|
|
42593
42962
|
var SCHEDULE_DIAGNOSTIC_FIELDS = /* @__PURE__ */ new Set([
|
|
@@ -42668,7 +43037,7 @@ import { spawnSync } from "node:child_process";
|
|
|
42668
43037
|
// src/cli/host-helper-release-files.ts
|
|
42669
43038
|
function expectedReleaseFiles(release) {
|
|
42670
43039
|
const publicFiles = Array.isArray(release.files) ? release.files.filter((file) => typeof file === "string" && file.startsWith("public/")) : [];
|
|
42671
|
-
const files = ["server.mjs", "sporades.json", ...publicFiles];
|
|
43040
|
+
const files = ["server.mjs", "sporades.json", ...publicFiles, ...resolveDeployFiles(release.deployFiles).map((file) => file.path)];
|
|
42672
43041
|
if (release.serverEnvIncluded) {
|
|
42673
43042
|
files.push(".env.sporades.server");
|
|
42674
43043
|
}
|
|
@@ -42680,8 +43049,8 @@ function expectedReleaseFiles(release) {
|
|
|
42680
43049
|
}
|
|
42681
43050
|
return files;
|
|
42682
43051
|
}
|
|
42683
|
-
function isExpectedClaimedReleaseFile(file) {
|
|
42684
|
-
return typeof file === "string" && (file.startsWith("public/") || [
|
|
43052
|
+
function isExpectedClaimedReleaseFile(file, deployFiles = []) {
|
|
43053
|
+
return typeof file === "string" && (file.startsWith("public/") || deployFiles.includes(file) || [
|
|
42685
43054
|
"server.mjs",
|
|
42686
43055
|
"sporades.json",
|
|
42687
43056
|
".env.sporades.server",
|
|
@@ -42833,8 +43202,8 @@ function validatePublicArchiveBounds(entries) {
|
|
|
42833
43202
|
}
|
|
42834
43203
|
|
|
42835
43204
|
// src/cli/host-helper-config.ts
|
|
42836
|
-
import { readFile } from "node:fs/promises";
|
|
42837
|
-
import
|
|
43205
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
43206
|
+
import path3 from "node:path";
|
|
42838
43207
|
var HOST_HELPER_CONFIG_FILE = "sporades-host-helper.json";
|
|
42839
43208
|
var DEFAULT_HOSTED_CAPSULE_DOCKER_IMAGE = SPORADES_BASE_IMAGE.image;
|
|
42840
43209
|
var DEFAULT_HOSTED_CAPSULE_DOCKER_NETWORK = "sporades-hosted-capsules";
|
|
@@ -42862,7 +43231,7 @@ async function loadHostHelperConfig(request) {
|
|
|
42862
43231
|
}
|
|
42863
43232
|
let contents;
|
|
42864
43233
|
try {
|
|
42865
|
-
contents = await
|
|
43234
|
+
contents = await readFile3(configPath, "utf8");
|
|
42866
43235
|
} catch (error) {
|
|
42867
43236
|
if (errorDetails(error).code === "ENOENT" && !process.env.SPORADES_HOST_HELPER_CONFIG) {
|
|
42868
43237
|
return loaded;
|
|
@@ -42890,7 +43259,7 @@ function hostHelperConfigPath(request) {
|
|
|
42890
43259
|
if (typeof request.host?.remoteRoot !== "string" || request.host.remoteRoot.length === 0) {
|
|
42891
43260
|
return null;
|
|
42892
43261
|
}
|
|
42893
|
-
return
|
|
43262
|
+
return path3.join(request.host.remoteRoot, HOST_HELPER_CONFIG_FILE);
|
|
42894
43263
|
}
|
|
42895
43264
|
function applyHostHelperConfig(loaded, config, configPath) {
|
|
42896
43265
|
assertPlainObject(config, "Host helper config", configPath);
|
|
@@ -42962,7 +43331,7 @@ function readConfigPositiveInteger(value, key, configPath) {
|
|
|
42962
43331
|
}
|
|
42963
43332
|
|
|
42964
43333
|
// src/cli/host-helper-validation.ts
|
|
42965
|
-
import
|
|
43334
|
+
import path4 from "node:path";
|
|
42966
43335
|
function missingCapsuleHint(request, purpose) {
|
|
42967
43336
|
if (purpose === "push") {
|
|
42968
43337
|
return `Run \`sporades host register ${request.capsule.subname} --host ${request.host.alias}\` before pushing a release.`;
|
|
@@ -43110,7 +43479,7 @@ function validateListRequest(request) {
|
|
|
43110
43479
|
}
|
|
43111
43480
|
function validateListRegistryRecord(request, record, recordPath) {
|
|
43112
43481
|
const capsuleRecord = record;
|
|
43113
|
-
const expectedSubname =
|
|
43482
|
+
const expectedSubname = path4.basename(recordPath, ".json");
|
|
43114
43483
|
const expectedRemoteCapsuleId = `${request.host.domain}/${typeof capsuleRecord?.subname === "string" ? capsuleRecord.subname : expectedSubname}`;
|
|
43115
43484
|
const valid = capsuleRecord && typeof capsuleRecord.subname === "string" && capsuleRecord.subname.length > 0 && capsuleRecord.subname === expectedSubname && capsuleRecord.domain === request.host.domain && (capsuleRecord.remoteCapsuleId ?? expectedRemoteCapsuleId) === expectedRemoteCapsuleId;
|
|
43116
43485
|
if (!valid) {
|
|
@@ -43148,6 +43517,7 @@ function validateRegisterRequest(request) {
|
|
|
43148
43517
|
throw helperError("Invalid Hosted Capsule registration request.", "Update the Sporades CLI and retry `sporades host register`.");
|
|
43149
43518
|
}
|
|
43150
43519
|
const registration = request.registration ?? {};
|
|
43520
|
+
validateAliasDomains(registration.aliasDomains);
|
|
43151
43521
|
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
43522
|
if (mismatchedIdentity) {
|
|
43153
43523
|
throw helperError(
|
|
@@ -43222,7 +43592,9 @@ function validateInstallRequest(request) {
|
|
|
43222
43592
|
if (!/^\d{8}T\d{6}Z-[a-f0-9]{8}$/.test(release.id)) {
|
|
43223
43593
|
throw helperError("Invalid Hosted Capsule release ID.", "Push again to generate a fresh UTC-sortable release ID.");
|
|
43224
43594
|
}
|
|
43225
|
-
|
|
43595
|
+
const deployFiles = resolveDeployFiles(release.deployFiles);
|
|
43596
|
+
if (JSON.stringify(release.deployFiles ?? []) !== JSON.stringify(deployFiles)) throw helperError("Invalid release deploy.files manifest.", "Rebuild with canonical relative paths and retry.");
|
|
43597
|
+
if (!Array.isArray(release.files) || release.files.some((file) => !isExpectedClaimedReleaseFile(file, deployFiles.map((entry) => entry.path)))) {
|
|
43226
43598
|
throw helperError("Invalid Hosted Capsule release file list.", "Update the Sporades CLI and retry `sporades host push`.");
|
|
43227
43599
|
}
|
|
43228
43600
|
validateClaimedReleaseFiles(release.files);
|
|
@@ -43236,8 +43608,8 @@ function validateInstallRequest(request) {
|
|
|
43236
43608
|
if (!directories?.releases || !directories.release) {
|
|
43237
43609
|
throw helperError("Invalid Hosted Capsule release directory.", "Update the Sporades CLI and retry `sporades host push`.");
|
|
43238
43610
|
}
|
|
43239
|
-
const expectedReleaseDirectory =
|
|
43240
|
-
if (
|
|
43611
|
+
const expectedReleaseDirectory = path4.join(directories.releases, release.id);
|
|
43612
|
+
if (path4.resolve(directories.release) !== path4.resolve(expectedReleaseDirectory)) {
|
|
43241
43613
|
throw helperError("Invalid Hosted Capsule release directory.", "Update the Sporades CLI and retry `sporades host push`.");
|
|
43242
43614
|
}
|
|
43243
43615
|
}
|
|
@@ -43356,12 +43728,12 @@ async function runHostHelperInstaller() {
|
|
|
43356
43728
|
const internal = process.argv[2] === "--install-host-helper-internal";
|
|
43357
43729
|
const target = process.argv[3];
|
|
43358
43730
|
const expectedChecksum = process.argv[4];
|
|
43359
|
-
if (process.argv.length !== 5 || typeof target !== "string" || !
|
|
43731
|
+
if (process.argv.length !== 5 || typeof target !== "string" || !path5.isAbsolute(target) || path5.basename(target) !== "sporades-host-helper" || !/^[a-f0-9]{64}$/.test(expectedChecksum ?? "")) {
|
|
43360
43732
|
throw helperError("Invalid Host helper upgrade request.", "Retry with `sporades host upgrade --host <alias>`.");
|
|
43361
43733
|
}
|
|
43362
43734
|
const stage = process.argv[1];
|
|
43363
|
-
const directory =
|
|
43364
|
-
const expectedStage =
|
|
43735
|
+
const directory = path5.dirname(target);
|
|
43736
|
+
const expectedStage = path5.join(directory, `.sporades-host-helper-stage-${expectedChecksum}.mjs`);
|
|
43365
43737
|
if (stage !== expectedStage) {
|
|
43366
43738
|
throw helperError("Invalid staged Host helper path.", "Retry with `sporades host upgrade --host <alias>`.");
|
|
43367
43739
|
}
|
|
@@ -43375,7 +43747,7 @@ async function runHostHelperInstaller() {
|
|
|
43375
43747
|
"--conflict-exit-code",
|
|
43376
43748
|
"75",
|
|
43377
43749
|
"--no-fork",
|
|
43378
|
-
|
|
43750
|
+
path5.join(directory, ".sporades-host-helper.upgrade.lock"),
|
|
43379
43751
|
process.execPath,
|
|
43380
43752
|
stage,
|
|
43381
43753
|
"--install-host-helper-internal",
|
|
@@ -43416,14 +43788,14 @@ function hostHelperUpgradeTimeoutMs(name2, fallback) {
|
|
|
43416
43788
|
return value;
|
|
43417
43789
|
}
|
|
43418
43790
|
async function installHostHelperPayload(stage, target, expectedChecksum) {
|
|
43419
|
-
const directory =
|
|
43420
|
-
const pointer =
|
|
43421
|
-
const blocked =
|
|
43422
|
-
const needsDrain =
|
|
43791
|
+
const directory = path5.dirname(target);
|
|
43792
|
+
const pointer = path5.join(directory, ".sporades-host-helper.active");
|
|
43793
|
+
const blocked = path5.join(directory, ".sporades-host-helper.upgrade-blocked");
|
|
43794
|
+
const needsDrain = path5.join(directory, ".sporades-host-helper.needs-drain");
|
|
43423
43795
|
const newPayloadName = `.sporades-host-helper-payload-${expectedChecksum}.mjs`;
|
|
43424
|
-
const newPayload =
|
|
43425
|
-
await
|
|
43426
|
-
if (
|
|
43796
|
+
const newPayload = path5.join(directory, newPayloadName);
|
|
43797
|
+
await mkdir2(directory, { recursive: true });
|
|
43798
|
+
if (createHash2("sha256").update(await readFile4(stage)).digest("hex") !== expectedChecksum) {
|
|
43427
43799
|
throw helperError("Staged Host helper checksum did not match.", "Upload the immutable Host helper again, then retry the upgrade.");
|
|
43428
43800
|
}
|
|
43429
43801
|
await publishHostHelperFile(stage, newPayload, 493);
|
|
@@ -43431,18 +43803,18 @@ async function installHostHelperPayload(stage, target, expectedChecksum) {
|
|
|
43431
43803
|
let firstCooperativeUpgrade = true;
|
|
43432
43804
|
let currentTarget;
|
|
43433
43805
|
try {
|
|
43434
|
-
currentTarget = await
|
|
43806
|
+
currentTarget = await readFile4(target);
|
|
43435
43807
|
} catch {
|
|
43436
43808
|
throw helperError("Current Host helper was not found.", "Install the current Host helper before retrying the upgrade.");
|
|
43437
43809
|
}
|
|
43438
43810
|
if (currentTarget.toString("utf8").includes(HOST_HELPER_DISPATCHER_MARKER)) {
|
|
43439
43811
|
firstCooperativeUpgrade = false;
|
|
43440
|
-
previousPayloadName = (await
|
|
43812
|
+
previousPayloadName = (await readFile4(pointer, "utf8")).trim();
|
|
43441
43813
|
await validateHostHelperPayload(directory, previousPayloadName);
|
|
43442
43814
|
} else {
|
|
43443
|
-
const previousChecksum =
|
|
43815
|
+
const previousChecksum = createHash2("sha256").update(currentTarget).digest("hex");
|
|
43444
43816
|
previousPayloadName = `.sporades-host-helper-payload-${previousChecksum}.mjs`;
|
|
43445
|
-
await publishHostHelperFile(target,
|
|
43817
|
+
await publishHostHelperFile(target, path5.join(directory, previousPayloadName), 493);
|
|
43446
43818
|
await writeHostHelperPointer(pointer, previousPayloadName);
|
|
43447
43819
|
await writeFile(needsDrain, "legacy-helper-drain-required\n", { mode: 384 });
|
|
43448
43820
|
}
|
|
@@ -43452,16 +43824,16 @@ async function installHostHelperPayload(stage, target, expectedChecksum) {
|
|
|
43452
43824
|
try {
|
|
43453
43825
|
if (firstCooperativeUpgrade || await pathExists(needsDrain)) await drainUncooperativeHostHelpers(target);
|
|
43454
43826
|
await writeHostHelperPointer(pointer, newPayloadName);
|
|
43455
|
-
await
|
|
43456
|
-
await
|
|
43457
|
-
await
|
|
43827
|
+
await rm2(needsDrain, { force: true });
|
|
43828
|
+
await rm2(blocked, { force: true });
|
|
43829
|
+
await rm2(stage, { force: true });
|
|
43458
43830
|
} catch (error) {
|
|
43459
43831
|
await writeFile(blocked, "upgrade-recovery-required\n", { mode: 384 });
|
|
43460
43832
|
throw error;
|
|
43461
43833
|
}
|
|
43462
43834
|
}
|
|
43463
43835
|
async function publishHostHelperFile(source, target, mode) {
|
|
43464
|
-
const contents = await
|
|
43836
|
+
const contents = await readFile4(source);
|
|
43465
43837
|
await publishHostHelperBytes(contents, target, mode);
|
|
43466
43838
|
}
|
|
43467
43839
|
async function publishHostHelperBytes(contents, target, mode) {
|
|
@@ -43469,21 +43841,21 @@ async function publishHostHelperBytes(contents, target, mode) {
|
|
|
43469
43841
|
try {
|
|
43470
43842
|
await writeFile(temporary, contents, { flag: "wx", mode });
|
|
43471
43843
|
await chmod(temporary, mode);
|
|
43472
|
-
await
|
|
43844
|
+
await rename2(temporary, target);
|
|
43473
43845
|
} finally {
|
|
43474
|
-
await
|
|
43846
|
+
await rm2(temporary, { force: true });
|
|
43475
43847
|
}
|
|
43476
43848
|
}
|
|
43477
43849
|
async function writeHostHelperPointer(pointer, payloadName) {
|
|
43478
|
-
await validateHostHelperPayload(
|
|
43850
|
+
await validateHostHelperPayload(path5.dirname(pointer), payloadName);
|
|
43479
43851
|
await publishHostHelperBytes(Buffer.from(`${payloadName}
|
|
43480
43852
|
`, "utf8"), pointer, 384);
|
|
43481
43853
|
}
|
|
43482
43854
|
async function validateHostHelperPayload(directory, payloadName) {
|
|
43483
43855
|
const match = /^\.sporades-host-helper-payload-([a-f0-9]{64})\.mjs$/.exec(payloadName);
|
|
43484
43856
|
if (!match) throw helperError("Host helper payload pointer was invalid.", "Retry the Host helper upgrade.");
|
|
43485
|
-
const payload =
|
|
43486
|
-
const actual =
|
|
43857
|
+
const payload = path5.join(directory, payloadName);
|
|
43858
|
+
const actual = createHash2("sha256").update(await readFile4(payload)).digest("hex");
|
|
43487
43859
|
if (actual !== match[1]) throw helperError("Host helper payload checksum did not match.", "Retry the Host helper upgrade.");
|
|
43488
43860
|
}
|
|
43489
43861
|
async function drainUncooperativeHostHelpers(target) {
|
|
@@ -43553,11 +43925,11 @@ async function findUncooperativeHostHelperProcesses(target, deadline) {
|
|
|
43553
43925
|
);
|
|
43554
43926
|
}
|
|
43555
43927
|
try {
|
|
43556
|
-
const processDir =
|
|
43557
|
-
const argv = (await
|
|
43928
|
+
const processDir = path5.join(procRoot, entry.name);
|
|
43929
|
+
const argv = (await readFile4(path5.join(processDir, "cmdline"))).toString("utf8").split("\0").filter(Boolean);
|
|
43558
43930
|
assertHostHelperDrainDeadline(deadline);
|
|
43559
43931
|
if (argv[1] !== target) continue;
|
|
43560
|
-
const environment = (await
|
|
43932
|
+
const environment = (await readFile4(path5.join(processDir, "environ"))).toString("utf8").split("\0");
|
|
43561
43933
|
assertHostHelperDrainDeadline(deadline);
|
|
43562
43934
|
if (environment.includes("SPORADES_HOST_DISPATCH_LOCK_HELD=1")) continue;
|
|
43563
43935
|
active.push(Number(entry.name));
|
|
@@ -43614,7 +43986,7 @@ async function runManagedRouteActionProcess(request, input, lockIdentity) {
|
|
|
43614
43986
|
process.argv[1]
|
|
43615
43987
|
] : [process.execPath, process.argv[1]];
|
|
43616
43988
|
const result = spawnSync2(flock, [
|
|
43617
|
-
lockIdentity.routeLockFile ? "--shared" : "--exclusive",
|
|
43989
|
+
lockIdentity.routeLockFile && request.action !== "capsule.register" ? "--shared" : "--exclusive",
|
|
43618
43990
|
"--timeout",
|
|
43619
43991
|
String(timeoutMs / 1e3),
|
|
43620
43992
|
"--conflict-exit-code",
|
|
@@ -43671,6 +44043,7 @@ function managedRouteMutationLockIdentity(request) {
|
|
|
43671
44043
|
case "capsule.release.rollback":
|
|
43672
44044
|
validateRollbackRequest(request);
|
|
43673
44045
|
break;
|
|
44046
|
+
case "capsule.release.reconcile":
|
|
43674
44047
|
case "capsule.start":
|
|
43675
44048
|
case "capsule.stop":
|
|
43676
44049
|
case "capsule.restart":
|
|
@@ -43689,7 +44062,7 @@ function managedRouteMutationLockIdentity(request) {
|
|
|
43689
44062
|
const domainDirectory = canonicalManagedRouteDomainDirectory(request, remoteRoot2);
|
|
43690
44063
|
const bootstrapTrust = bootstrapTrustManifest(request);
|
|
43691
44064
|
return {
|
|
43692
|
-
globalLockFile:
|
|
44065
|
+
globalLockFile: path5.join(remoteRoot2, "bin", ".sporades-host-helper.host-route.lock"),
|
|
43693
44066
|
routeLockFile: null,
|
|
43694
44067
|
domainDirectory,
|
|
43695
44068
|
bootstrapTrust
|
|
@@ -43700,7 +44073,7 @@ function managedRouteMutationLockIdentity(request) {
|
|
|
43700
44073
|
}
|
|
43701
44074
|
const remoteRoot = validateCanonicalHostRouteRoot(request);
|
|
43702
44075
|
return {
|
|
43703
|
-
globalLockFile:
|
|
44076
|
+
globalLockFile: path5.join(remoteRoot, "bin", ".sporades-host-helper.host-route.lock"),
|
|
43704
44077
|
routeLockFile: `${canonicalManagedRouteFile(request, remoteRoot)}.lock`,
|
|
43705
44078
|
domainDirectory: canonicalManagedRouteDomainDirectory(request, remoteRoot),
|
|
43706
44079
|
...actionCanProvisionCapsuleHttpLog(request.action) ? { routeLogTrust: capsuleHttpLogTrustManifest(request, remoteRoot) } : {}
|
|
@@ -43711,7 +44084,7 @@ function actionCanProvisionCapsuleHttpLog(action) {
|
|
|
43711
44084
|
}
|
|
43712
44085
|
function validateCanonicalHostRouteRoot(request) {
|
|
43713
44086
|
const remoteRoot = request.host.remoteRoot;
|
|
43714
|
-
if (typeof remoteRoot !== "string" || remoteRoot.length < 2 || remoteRoot.length > 4096 || !
|
|
44087
|
+
if (typeof remoteRoot !== "string" || remoteRoot.length < 2 || remoteRoot.length > 4096 || !path5.isAbsolute(remoteRoot) || /[\0\r\n]/.test(remoteRoot) || path5.normalize(remoteRoot) !== remoteRoot || remoteRoot === path5.parse(remoteRoot).root) {
|
|
43715
44088
|
throw helperError("Invalid Hosted Capsule route identity.", "Use a bounded absolute canonical Host remote root and retry.");
|
|
43716
44089
|
}
|
|
43717
44090
|
const domain = request.host.domain;
|
|
@@ -43730,22 +44103,22 @@ function canonicalManagedRouteFile(request, validatedRemoteRoot = validateCanoni
|
|
|
43730
44103
|
throw helperError("Invalid Hosted Capsule route identity.", "Use a canonical DNS-safe Capsule subname and retry.");
|
|
43731
44104
|
}
|
|
43732
44105
|
const domainDirectory = canonicalManagedRouteDomainDirectory(request, validatedRemoteRoot);
|
|
43733
|
-
const routeFile =
|
|
43734
|
-
if (
|
|
44106
|
+
const routeFile = path5.resolve(domainDirectory, `${subname}.caddy`);
|
|
44107
|
+
if (path5.dirname(routeFile) !== domainDirectory) {
|
|
43735
44108
|
throw helperError("Invalid Hosted Capsule route identity.", "Use a route identity within the configured Hosted domain directory and retry.");
|
|
43736
44109
|
}
|
|
43737
44110
|
return routeFile;
|
|
43738
44111
|
}
|
|
43739
44112
|
function canonicalManagedRouteDomainDirectory(request, validatedRemoteRoot = validateCanonicalHostRouteRoot(request)) {
|
|
43740
|
-
const hostsDirectory =
|
|
43741
|
-
const domainDirectory =
|
|
43742
|
-
if (
|
|
44113
|
+
const hostsDirectory = path5.resolve(validatedRemoteRoot, "caddy", "hosts");
|
|
44114
|
+
const domainDirectory = path5.resolve(hostsDirectory, request.host.domain);
|
|
44115
|
+
if (path5.dirname(domainDirectory) !== hostsDirectory) {
|
|
43743
44116
|
throw helperError("Invalid Hosted Capsule route identity.", "Use a route identity within the configured Hosted domain directory and retry.");
|
|
43744
44117
|
}
|
|
43745
44118
|
return domainDirectory;
|
|
43746
44119
|
}
|
|
43747
44120
|
async function captureManagedRouteTrust(lockIdentity, createMissing) {
|
|
43748
|
-
const remoteRoot =
|
|
44121
|
+
const remoteRoot = path5.dirname(path5.dirname(lockIdentity.globalLockFile));
|
|
43749
44122
|
const canonicalRemoteRoot = await canonicalTrustedTarget(remoteRoot);
|
|
43750
44123
|
const directoryIdentities = /* @__PURE__ */ new Map();
|
|
43751
44124
|
const captureDirectory = async (directory, caddyOwned = false) => {
|
|
@@ -43755,18 +44128,18 @@ async function captureManagedRouteTrust(lockIdentity, createMissing) {
|
|
|
43755
44128
|
}
|
|
43756
44129
|
};
|
|
43757
44130
|
await captureDirectory(remoteRoot);
|
|
43758
|
-
await captureDirectory(
|
|
44131
|
+
await captureDirectory(path5.join(canonicalRemoteRoot, "bin"));
|
|
43759
44132
|
let routeFile = null;
|
|
43760
44133
|
if (lockIdentity.routeLockFile) {
|
|
43761
44134
|
routeFile = lockIdentity.routeLockFile.slice(0, -5);
|
|
43762
|
-
const domainDirectory =
|
|
43763
|
-
await captureDirectory(
|
|
43764
|
-
await captureDirectory(
|
|
44135
|
+
const domainDirectory = path5.dirname(routeFile);
|
|
44136
|
+
await captureDirectory(path5.join(canonicalRemoteRoot, "caddy"));
|
|
44137
|
+
await captureDirectory(path5.join(canonicalRemoteRoot, "caddy", "hosts"));
|
|
43765
44138
|
await captureDirectory(await canonicalTrustedTarget(domainDirectory));
|
|
43766
44139
|
await assertTrustedRegularFileIfExists(routeFile);
|
|
43767
44140
|
} else {
|
|
43768
|
-
await captureDirectory(
|
|
43769
|
-
await captureDirectory(
|
|
44141
|
+
await captureDirectory(path5.join(canonicalRemoteRoot, "caddy"));
|
|
44142
|
+
await captureDirectory(path5.join(canonicalRemoteRoot, "caddy", "hosts"));
|
|
43770
44143
|
await captureDirectory(await canonicalTrustedTarget(lockIdentity.domainDirectory));
|
|
43771
44144
|
for (const directory of lockIdentity.bootstrapTrust?.directories ?? []) await captureDirectory(directory.path, directory.caddyOwned === true);
|
|
43772
44145
|
}
|
|
@@ -43790,34 +44163,34 @@ async function captureManagedRouteTrust(lockIdentity, createMissing) {
|
|
|
43790
44163
|
return { directories: [...directoryIdentities.values()], managedRoot: canonicalRemoteRoot, finalFiles, globalLockFile: lockIdentity.globalLockFile, routeLockFile: lockIdentity.routeLockFile, routeFile };
|
|
43791
44164
|
}
|
|
43792
44165
|
async function canonicalTrustedTarget(target) {
|
|
43793
|
-
if (!
|
|
44166
|
+
if (!path5.isAbsolute(target) || path5.normalize(target) !== target) throw routeTrustError();
|
|
43794
44167
|
return target;
|
|
43795
44168
|
}
|
|
43796
44169
|
async function trustedDirectoryChain(target, createMissing, managedRoot, targetOwner) {
|
|
43797
44170
|
const canonicalTarget = await canonicalTrustedTarget(target);
|
|
43798
|
-
const parsed =
|
|
43799
|
-
const components = canonicalTarget.slice(parsed.root.length).split(
|
|
44171
|
+
const parsed = path5.parse(canonicalTarget);
|
|
44172
|
+
const components = canonicalTarget.slice(parsed.root.length).split(path5.sep).filter(Boolean);
|
|
43800
44173
|
const identities = [];
|
|
43801
44174
|
let current2 = parsed.root;
|
|
43802
44175
|
for (const component of [null, ...components]) {
|
|
43803
|
-
if (component !== null) current2 =
|
|
44176
|
+
if (component !== null) current2 = path5.join(current2, component);
|
|
43804
44177
|
let details;
|
|
43805
44178
|
try {
|
|
43806
|
-
details = await
|
|
44179
|
+
details = await lstat2(current2);
|
|
43807
44180
|
} catch (error) {
|
|
43808
44181
|
if (!createMissing || errorDetails(error).code !== "ENOENT") throw routeTrustError();
|
|
43809
44182
|
try {
|
|
43810
|
-
await
|
|
44183
|
+
await mkdir2(current2, { mode: 493 });
|
|
43811
44184
|
} catch (mkdirError) {
|
|
43812
44185
|
if (errorDetails(mkdirError).code !== "EEXIST") throw routeTrustError();
|
|
43813
44186
|
}
|
|
43814
44187
|
try {
|
|
43815
|
-
details = await
|
|
44188
|
+
details = await lstat2(current2);
|
|
43816
44189
|
} catch {
|
|
43817
44190
|
throw routeTrustError();
|
|
43818
44191
|
}
|
|
43819
44192
|
}
|
|
43820
|
-
const managed = current2 === managedRoot || current2.startsWith(`${managedRoot}${
|
|
44193
|
+
const managed = current2 === managedRoot || current2.startsWith(`${managedRoot}${path5.sep}`);
|
|
43821
44194
|
const expectedOwner = current2 === canonicalTarget ? targetOwner : void 0;
|
|
43822
44195
|
if (!details.isDirectory() || details.isSymbolicLink() || !(expectedOwner ? trustedExactOwnerMetadata(details, expectedOwner) : managed ? trustedHostPathMetadata(details) : trustedAnchorPathMetadata(details))) throw routeTrustError();
|
|
43823
44196
|
identities.push({ path: current2, dev: details.dev, ino: details.ino, ...expectedOwner ? { expectedOwner } : {} });
|
|
@@ -43830,7 +44203,7 @@ function trustedExactOwnerMetadata(details, owner) {
|
|
|
43830
44203
|
async function expectedCaddyOwnerForExistingPath(target) {
|
|
43831
44204
|
let details;
|
|
43832
44205
|
try {
|
|
43833
|
-
details = await
|
|
44206
|
+
details = await lstat2(target);
|
|
43834
44207
|
} catch (error) {
|
|
43835
44208
|
if (errorDetails(error).code === "ENOENT") return void 0;
|
|
43836
44209
|
throw routeTrustError();
|
|
@@ -43860,7 +44233,7 @@ function trustedAnchorPathMetadata(details) {
|
|
|
43860
44233
|
async function assertTrustedRegularFileIfExists(file, expectedOwner) {
|
|
43861
44234
|
let details;
|
|
43862
44235
|
try {
|
|
43863
|
-
details = await
|
|
44236
|
+
details = await lstat2(file);
|
|
43864
44237
|
} catch (error) {
|
|
43865
44238
|
if (errorDetails(error).code === "ENOENT") return null;
|
|
43866
44239
|
throw routeTrustError();
|
|
@@ -43874,11 +44247,11 @@ async function assertActiveManagedRouteTrust(routeFile) {
|
|
|
43874
44247
|
for (const expected of trust.directories) {
|
|
43875
44248
|
let current2;
|
|
43876
44249
|
try {
|
|
43877
|
-
current2 = await
|
|
44250
|
+
current2 = await lstat2(expected.path);
|
|
43878
44251
|
} catch {
|
|
43879
44252
|
throw routeTrustError();
|
|
43880
44253
|
}
|
|
43881
|
-
const managed = expected.path === trust.managedRoot || expected.path.startsWith(`${trust.managedRoot}${
|
|
44254
|
+
const managed = expected.path === trust.managedRoot || expected.path.startsWith(`${trust.managedRoot}${path5.sep}`);
|
|
43882
44255
|
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
44256
|
}
|
|
43884
44257
|
await assertTrustedRegularFileIfExists(trust.globalLockFile);
|
|
@@ -43926,6 +44299,10 @@ async function main(request) {
|
|
|
43926
44299
|
await rollbackRelease(request);
|
|
43927
44300
|
return;
|
|
43928
44301
|
}
|
|
44302
|
+
if (request.action === "capsule.release.reconcile") {
|
|
44303
|
+
await reconcileReleaseAttempt(request);
|
|
44304
|
+
return;
|
|
44305
|
+
}
|
|
43929
44306
|
if (request.action === "capsule.start") {
|
|
43930
44307
|
await startCapsule(request);
|
|
43931
44308
|
return;
|
|
@@ -44049,6 +44426,7 @@ function versionHost(request) {
|
|
|
44049
44426
|
async function bootstrapHost(request) {
|
|
44050
44427
|
validateBootstrapRequest(request);
|
|
44051
44428
|
const bootstrap = normaliseBootstrap(request);
|
|
44429
|
+
await assertHostnamesAvailable(request.host.remoteRoot, [`host.${request.host.domain}`], `health:${request.host.domain}`);
|
|
44052
44430
|
await ensureBootstrapDirectories(bootstrap);
|
|
44053
44431
|
await validateBootstrapTls(request, bootstrap);
|
|
44054
44432
|
const network = ensureDockerNetwork(bootstrap.network);
|
|
@@ -44073,26 +44451,60 @@ async function bootstrapHost(request) {
|
|
|
44073
44451
|
}
|
|
44074
44452
|
async function registerCapsule(request) {
|
|
44075
44453
|
validateRegisterRequest(request);
|
|
44076
|
-
const
|
|
44454
|
+
const claimPath = path5.join(request.host.remoteRoot, "hosts", request.host.domain, "registry", "registration-claims", `${request.capsule.subname}.json`);
|
|
44455
|
+
const pendingClaim = await readFile4(claimPath, "utf8").then((contents) => JSON.parse(contents)).catch((error) => {
|
|
44456
|
+
if (errorDetails(error).code === "ENOENT") return null;
|
|
44457
|
+
throw error;
|
|
44458
|
+
});
|
|
44459
|
+
if (pendingClaim) {
|
|
44460
|
+
const aliases = validateAliasDomains(pendingClaim.aliasDomains);
|
|
44461
|
+
if (pendingClaim.domain !== request.host.domain || pendingClaim.subname !== request.capsule.subname || request.registration?.aliasDomains !== void 0 && !isDeepStrictEqual([...request.registration.aliasDomains].sort(), [...aliases].sort())) {
|
|
44462
|
+
throw helperError("Hosted Capsule registration recovery is required.", "Repair Caddy and retry the original registration with the same alias domains before changing its aliases.");
|
|
44463
|
+
}
|
|
44464
|
+
request = { ...request, registration: { ...request.registration, aliasDomains: aliases } };
|
|
44465
|
+
}
|
|
44466
|
+
let registration = normaliseRegistration(request);
|
|
44077
44467
|
await ensureHostedDomainBootstrapped(request, registration);
|
|
44078
44468
|
let reactivated = false;
|
|
44079
44469
|
let sealedServerEnv = null;
|
|
44080
44470
|
let priorRuntime = null;
|
|
44081
44471
|
let admissionError = null;
|
|
44472
|
+
let registrationCommitted = false;
|
|
44473
|
+
const recoveryErrors = [];
|
|
44474
|
+
let priorRoute = null;
|
|
44475
|
+
let routeAttempted = false;
|
|
44476
|
+
let claimWritten = Boolean(pendingClaim);
|
|
44477
|
+
const reserveClaim = async (previous = null) => {
|
|
44478
|
+
if (claimWritten) return;
|
|
44479
|
+
await mkdir2(path5.dirname(claimPath), { recursive: true });
|
|
44480
|
+
await publishHostHelperBytes(Buffer.from(JSON.stringify({
|
|
44481
|
+
domain: registration.domain,
|
|
44482
|
+
subname: registration.subname,
|
|
44483
|
+
aliasDomains: registration.aliasDomains,
|
|
44484
|
+
previousAliasDomains: validateAliasDomains(previous?.aliasDomains)
|
|
44485
|
+
})), claimPath, 384);
|
|
44486
|
+
claimWritten = true;
|
|
44487
|
+
};
|
|
44082
44488
|
try {
|
|
44083
|
-
await
|
|
44489
|
+
await mkdir2(path5.dirname(registryLockPath(request)), { recursive: true });
|
|
44084
44490
|
await withRegistryLock(request, async () => {
|
|
44085
44491
|
if (await pathExists(registration.registryRecord)) {
|
|
44086
44492
|
const existing = await readRegistryRecordForCapsule(request, "register");
|
|
44087
44493
|
assertRegistryRecordMatchesRequest(request, existing);
|
|
44088
|
-
if (existing.status === "unregistered") {
|
|
44494
|
+
if (existing.status === "unregistered" || pendingClaim) {
|
|
44495
|
+
registration = normaliseRegistration(request, existing);
|
|
44496
|
+
await assertHostnamesAvailable(request.host.remoteRoot, [registration.route.hostname, ...registration.aliasDomains], registration.remoteCapsuleId);
|
|
44497
|
+
priorRoute = await captureReleaseInstallRoute(request, existing);
|
|
44498
|
+
await reserveClaim(existing);
|
|
44089
44499
|
priorRuntime = captureCapsuleRuntimeSettlement(request, existing);
|
|
44090
44500
|
quiesceCapsuleRuntime(priorRuntime);
|
|
44091
|
-
await
|
|
44092
|
-
await
|
|
44501
|
+
await mkdir2(path5.dirname(registration.registryRecord), { recursive: true });
|
|
44502
|
+
await mkdir2(registration.directories.releases, { recursive: true });
|
|
44503
|
+
routeAttempted = true;
|
|
44093
44504
|
await writeUnavailableRoute(registration.lifecycle);
|
|
44094
44505
|
sealedServerEnv = await ensureHostSealedEnvKeyPair(registration, existing);
|
|
44095
|
-
await writeRegistryRecordAtomic(registration.registryRecord, reactivateRegistrationRecord(existing, sealedServerEnv));
|
|
44506
|
+
await writeRegistryRecordAtomic(registration.registryRecord, { ...reactivateRegistrationRecord(existing, sealedServerEnv), aliasDomains: registration.aliasDomains, route: { tls: registration.route.tls } });
|
|
44507
|
+
registrationCommitted = true;
|
|
44096
44508
|
reactivated = true;
|
|
44097
44509
|
return;
|
|
44098
44510
|
}
|
|
@@ -44101,17 +44513,50 @@ async function registerCapsule(request) {
|
|
|
44101
44513
|
`Choose a different Capsule subname for ${request.host.domain}.`
|
|
44102
44514
|
);
|
|
44103
44515
|
}
|
|
44104
|
-
await
|
|
44105
|
-
await
|
|
44106
|
-
await
|
|
44516
|
+
await assertHostnamesAvailable(request.host.remoteRoot, [registration.route.hostname, ...registration.aliasDomains], registration.remoteCapsuleId);
|
|
44517
|
+
priorRoute = await captureReleaseInstallRoute(request, null);
|
|
44518
|
+
await reserveClaim();
|
|
44519
|
+
await mkdir2(path5.dirname(registration.registryRecord), { recursive: true });
|
|
44520
|
+
await mkdir2(registration.directories.releases, { recursive: true });
|
|
44521
|
+
await mkdir2(registration.directories.logs, { recursive: true });
|
|
44522
|
+
routeAttempted = true;
|
|
44107
44523
|
await writeUnavailableRoute(registration.lifecycle);
|
|
44108
44524
|
sealedServerEnv = await ensureHostSealedEnvKeyPair(registration);
|
|
44109
44525
|
await writeRegistryRecordAtomic(registration.registryRecord, createRegistrationRecord(registration, sealedServerEnv));
|
|
44526
|
+
registrationCommitted = true;
|
|
44110
44527
|
});
|
|
44111
44528
|
} catch (error) {
|
|
44112
|
-
|
|
44529
|
+
if (registrationCommitted) {
|
|
44530
|
+
recoveryErrors.push(`Registry lock cleanup: ${errorDetails(error).message}`);
|
|
44531
|
+
} else {
|
|
44532
|
+
admissionError = error;
|
|
44533
|
+
}
|
|
44534
|
+
}
|
|
44535
|
+
if (admissionError && routeAttempted && priorRoute) {
|
|
44536
|
+
try {
|
|
44537
|
+
await restoreReleaseInstallRoute(priorRoute, true);
|
|
44538
|
+
} catch (error) {
|
|
44539
|
+
recoveryErrors.push(`Route rollback: ${errorDetails(error).message}`);
|
|
44540
|
+
}
|
|
44541
|
+
}
|
|
44542
|
+
try {
|
|
44543
|
+
await settleCapsuleRuntime(request, priorRuntime, admissionError);
|
|
44544
|
+
} catch (error) {
|
|
44545
|
+
recoveryErrors.push(`Runtime settlement: ${errorDetails(error).message}`);
|
|
44546
|
+
}
|
|
44547
|
+
if (claimWritten && recoveryErrors.length === 0 && (!admissionError || !pendingClaim)) {
|
|
44548
|
+
try {
|
|
44549
|
+
await rm2(claimPath);
|
|
44550
|
+
} catch (error) {
|
|
44551
|
+
recoveryErrors.push(`Reservation cleanup: ${errorDetails(error).message}`);
|
|
44552
|
+
}
|
|
44553
|
+
}
|
|
44554
|
+
if (recoveryErrors.length) {
|
|
44555
|
+
throw helperError(
|
|
44556
|
+
"Hosted Capsule registration recovery failed.",
|
|
44557
|
+
`${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.`
|
|
44558
|
+
);
|
|
44113
44559
|
}
|
|
44114
|
-
await settleCapsuleRuntime(request, priorRuntime, admissionError);
|
|
44115
44560
|
if (admissionError) throw admissionError;
|
|
44116
44561
|
writeEnvelope({
|
|
44117
44562
|
ok: true,
|
|
@@ -44123,6 +44568,8 @@ async function registerCapsule(request) {
|
|
|
44123
44568
|
subname: registration.subname,
|
|
44124
44569
|
domain: registration.domain,
|
|
44125
44570
|
hostedUrl: registration.hostedUrl,
|
|
44571
|
+
aliasDomains: registration.aliasDomains,
|
|
44572
|
+
aliasUrls: registration.aliasDomains.map((hostname) => `https://${hostname}`),
|
|
44126
44573
|
remoteCapsuleId: registration.remoteCapsuleId
|
|
44127
44574
|
},
|
|
44128
44575
|
registryRecord: registration.registryRecord,
|
|
@@ -44135,7 +44582,7 @@ async function registerCapsule(request) {
|
|
|
44135
44582
|
}
|
|
44136
44583
|
async function rotateCapsuleSealedEnvKey(request) {
|
|
44137
44584
|
validateSealedEnvRotationRequest(request);
|
|
44138
|
-
await
|
|
44585
|
+
await mkdir2(path5.dirname(registryLockPath(request)), { recursive: true });
|
|
44139
44586
|
let data2;
|
|
44140
44587
|
let priorRuntime = null;
|
|
44141
44588
|
let rotationError = null;
|
|
@@ -44151,7 +44598,7 @@ async function rotateCapsuleSealedEnvKey(request) {
|
|
|
44151
44598
|
}
|
|
44152
44599
|
priorRuntime = captureCapsuleRuntimeSettlement(request, record);
|
|
44153
44600
|
quiesceCapsuleRuntime(priorRuntime);
|
|
44154
|
-
const dataDirectory =
|
|
44601
|
+
const dataDirectory = path5.join(request.host.remoteRoot, "hosts", request.host.domain, "capsules", request.capsule.subname, "data");
|
|
44155
44602
|
const previousPublicKeyFingerprint = record.sealedServerEnv?.currentKeyFingerprint ?? null;
|
|
44156
44603
|
const sealedServerEnv = await generateHostSealedEnvKeyPair(dataDirectory);
|
|
44157
44604
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -44192,10 +44639,24 @@ async function rotateCapsuleSealedEnvKey(request) {
|
|
|
44192
44639
|
if (rotationError) throw rotationError;
|
|
44193
44640
|
writeEnvelope({ ok: true, data: data2, error: null });
|
|
44194
44641
|
}
|
|
44642
|
+
async function assertRegistrationRecoveryComplete(request) {
|
|
44643
|
+
const claimPath = path5.join(request.host.remoteRoot, "hosts", request.host.domain, "registry", "registration-claims", `${request.capsule.subname}.json`);
|
|
44644
|
+
const pending = await lstat2(claimPath).catch((error) => {
|
|
44645
|
+
if (errorDetails(error).code === "ENOENT") return null;
|
|
44646
|
+
throw error;
|
|
44647
|
+
});
|
|
44648
|
+
if (pending) {
|
|
44649
|
+
throw helperError(
|
|
44650
|
+
"Hosted Capsule registration recovery is required before teardown.",
|
|
44651
|
+
"Repair the reported Host services or storage problem and retry the original registration with the same alias domains before unregistering or deleting this Capsule."
|
|
44652
|
+
);
|
|
44653
|
+
}
|
|
44654
|
+
}
|
|
44195
44655
|
async function unregisterCapsule(request) {
|
|
44196
44656
|
validateUnregisterRequest(request);
|
|
44657
|
+
await assertRegistrationRecoveryComplete(request);
|
|
44197
44658
|
const unregister = normaliseUnregister(request);
|
|
44198
|
-
await
|
|
44659
|
+
await mkdir2(path5.dirname(registryLockPath(request)), { recursive: true });
|
|
44199
44660
|
let data2;
|
|
44200
44661
|
await withRegistryLock(request, async () => {
|
|
44201
44662
|
const record = await readRegistryRecordForCapsule(request, "unregister");
|
|
@@ -44231,8 +44692,9 @@ async function unregisterCapsule(request) {
|
|
|
44231
44692
|
}
|
|
44232
44693
|
async function deleteCapsule(request) {
|
|
44233
44694
|
validateDeleteRequest(request);
|
|
44695
|
+
await assertRegistrationRecoveryComplete(request);
|
|
44234
44696
|
const deletion = normaliseDeletion(request);
|
|
44235
|
-
await
|
|
44697
|
+
await mkdir2(path5.dirname(registryLockPath(request)), { recursive: true });
|
|
44236
44698
|
let data2;
|
|
44237
44699
|
await withRegistryLock(request, async () => {
|
|
44238
44700
|
const record = await readOptionalRegistryRecordForCapsule(request);
|
|
@@ -44265,6 +44727,7 @@ function deletionRequiresUnregisterError(request) {
|
|
|
44265
44727
|
`Run \`sporades host unregister ${request.capsule.subname} --host ${request.host.alias}\` before deleting Hosted Capsule storage.`
|
|
44266
44728
|
);
|
|
44267
44729
|
}
|
|
44730
|
+
var activePreservedAttempts = /* @__PURE__ */ new Set();
|
|
44268
44731
|
async function installRelease(request) {
|
|
44269
44732
|
validateInstallRequest(request);
|
|
44270
44733
|
const previousRecord = await verifyRegisteredCapsule(request);
|
|
@@ -44277,14 +44740,15 @@ async function installRelease(request) {
|
|
|
44277
44740
|
try {
|
|
44278
44741
|
await installClaimedRelease(request, previousRecord, { ...paths, release: paths.release }, claimedArchive);
|
|
44279
44742
|
} finally {
|
|
44280
|
-
|
|
44281
|
-
await
|
|
44743
|
+
activePreservedAttempts.delete(attemptJournalPath(hostedPreservedFilesRoot(paths)));
|
|
44744
|
+
await rm2(claimedArchive.path, { force: true });
|
|
44745
|
+
await rm2(request.release.remoteArchive, { force: true });
|
|
44282
44746
|
}
|
|
44283
44747
|
}
|
|
44284
44748
|
async function installClaimedRelease(request, previousRecord, paths, claimedArchive) {
|
|
44285
44749
|
const release = request.release;
|
|
44286
44750
|
const previousCurrentRelease = previousRecord.currentRelease?.id ? { id: previousRecord.currentRelease.id } : null;
|
|
44287
|
-
const previousRegistryContents = await
|
|
44751
|
+
const previousRegistryContents = await readFile4(registryPath(request), "utf8");
|
|
44288
44752
|
const previousCurrentTarget = await readlink(paths.currentLink).catch((error) => {
|
|
44289
44753
|
if (errorDetails(error).code === "ENOENT") return null;
|
|
44290
44754
|
throw error;
|
|
@@ -44296,20 +44760,20 @@ async function installClaimedRelease(request, previousRecord, paths, claimedArch
|
|
|
44296
44760
|
throw helperError("Hosted Capsule release archive ownership changed.", "Upload the release again so the Host helper can claim immutable archive bytes.");
|
|
44297
44761
|
}
|
|
44298
44762
|
validateSealedServerEnvPrivateKeyPath(release, paths);
|
|
44299
|
-
await
|
|
44763
|
+
await mkdir2(paths.releases, { recursive: true });
|
|
44300
44764
|
const dataRoot = await openCanonicalRuntimeDataDirectory(paths.data, true);
|
|
44301
44765
|
await dataRoot.close();
|
|
44302
|
-
await
|
|
44766
|
+
await mkdir2(paths.logs, { recursive: true });
|
|
44303
44767
|
const tempReleaseDirectory = `${paths.release}.tmp-${process.pid}`;
|
|
44304
44768
|
const tempCurrentLink = `${paths.currentLink}.tmp-${process.pid}`;
|
|
44305
|
-
await
|
|
44306
|
-
await
|
|
44307
|
-
await
|
|
44769
|
+
await rm2(tempReleaseDirectory, { recursive: true, force: true });
|
|
44770
|
+
await rm2(tempCurrentLink, { force: true });
|
|
44771
|
+
await mkdir2(tempReleaseDirectory, { recursive: true });
|
|
44308
44772
|
const extract = spawnSync2("tar", ["-xzf", claimedArchive.path, "-C", tempReleaseDirectory], {
|
|
44309
44773
|
encoding: "utf8"
|
|
44310
44774
|
});
|
|
44311
44775
|
if (extract.error || extract.status !== 0) {
|
|
44312
|
-
await
|
|
44776
|
+
await rm2(tempReleaseDirectory, { recursive: true, force: true });
|
|
44313
44777
|
throw helperError(
|
|
44314
44778
|
"Failed to extract Hosted Capsule release archive.",
|
|
44315
44779
|
"Upload the release again with `sporades host push` and check that tar is installed on the Host server."
|
|
@@ -44318,17 +44782,23 @@ async function installClaimedRelease(request, previousRecord, paths, claimedArch
|
|
|
44318
44782
|
let installedInventory;
|
|
44319
44783
|
try {
|
|
44320
44784
|
installedInventory = await validateExtractedReleaseTree(tempReleaseDirectory, validatedArchive.files);
|
|
44785
|
+
for (const file of resolveDeployFiles(release.deployFiles)) {
|
|
44786
|
+
await prepareHostedRuntimeFileAccess(path5.join(tempReleaseDirectory, file.path), 256, {
|
|
44787
|
+
message: "Unsafe additional release file.",
|
|
44788
|
+
hint: "Upload regular deployment files."
|
|
44789
|
+
});
|
|
44790
|
+
}
|
|
44321
44791
|
if (await releaseArchiveSha256(claimedArchive.path) !== claimedArchive.sha256) {
|
|
44322
44792
|
throw helperError("Hosted Capsule release archive ownership changed.", "Upload the release again so the Host helper can claim immutable archive bytes.");
|
|
44323
44793
|
}
|
|
44324
44794
|
} catch (error) {
|
|
44325
|
-
await
|
|
44795
|
+
await rm2(tempReleaseDirectory, { recursive: true, force: true });
|
|
44326
44796
|
throw error;
|
|
44327
44797
|
}
|
|
44328
44798
|
try {
|
|
44329
|
-
await
|
|
44799
|
+
await rename2(tempReleaseDirectory, paths.release);
|
|
44330
44800
|
} catch (error) {
|
|
44331
|
-
await
|
|
44801
|
+
await rm2(tempReleaseDirectory, { recursive: true, force: true });
|
|
44332
44802
|
const details = errorDetails(error);
|
|
44333
44803
|
if (details.code === "EEXIST" || details.code === "ENOTEMPTY") {
|
|
44334
44804
|
throw helperError(
|
|
@@ -44342,23 +44812,50 @@ async function installClaimedRelease(request, previousRecord, paths, claimedArch
|
|
|
44342
44812
|
try {
|
|
44343
44813
|
await installSealedServerEnvPrivateKey(release, paths);
|
|
44344
44814
|
} catch (error) {
|
|
44345
|
-
await
|
|
44815
|
+
await rm2(paths.release, { recursive: true, force: true });
|
|
44346
44816
|
throw error;
|
|
44347
44817
|
}
|
|
44348
44818
|
}
|
|
44819
|
+
const createdSeeds = [];
|
|
44820
|
+
let seedJournal;
|
|
44349
44821
|
try {
|
|
44350
|
-
await
|
|
44351
|
-
|
|
44352
|
-
await recordReleaseUploaded(request, release, installedInventory);
|
|
44822
|
+
seedJournal = await beginPreservedFileAttempt(hostedPreservedFilesRoot(paths), release.id, resolveDeployFiles(release.deployFiles).length > 0);
|
|
44823
|
+
if (seedJournal) activePreservedAttempts.add(seedJournal);
|
|
44353
44824
|
} catch (error) {
|
|
44354
|
-
await restoreCurrentReleasePointerTarget(paths.currentLink, previousCurrentTarget);
|
|
44355
44825
|
await removeInstalledReleasePrivateKey(release, paths);
|
|
44356
|
-
await
|
|
44826
|
+
await rm2(paths.release, { recursive: true, force: true });
|
|
44357
44827
|
throw error;
|
|
44358
44828
|
}
|
|
44829
|
+
try {
|
|
44830
|
+
await preparePreservedFiles(resolveDeployFiles(release.deployFiles), paths.release, hostedPreservedFilesRoot(paths), prepareRuntimeDataOwnershipHandle, createdSeeds, seedJournal);
|
|
44831
|
+
await symlink(paths.release, tempCurrentLink);
|
|
44832
|
+
await rename2(tempCurrentLink, paths.currentLink);
|
|
44833
|
+
await recordReleaseUploaded(request, release, installedInventory);
|
|
44834
|
+
} catch (error) {
|
|
44835
|
+
let pointerRestored = false;
|
|
44836
|
+
await rethrowAfterDeployCleanup(error, [
|
|
44837
|
+
async () => {
|
|
44838
|
+
await restoreCurrentReleasePointerTarget(paths.currentLink, previousCurrentTarget);
|
|
44839
|
+
pointerRestored = true;
|
|
44840
|
+
},
|
|
44841
|
+
async () => {
|
|
44842
|
+
if (pointerRestored) await removeInstalledReleasePrivateKey(release, paths);
|
|
44843
|
+
},
|
|
44844
|
+
async () => {
|
|
44845
|
+
if (pointerRestored) await rm2(paths.release, { recursive: true, force: true });
|
|
44846
|
+
},
|
|
44847
|
+
async () => {
|
|
44848
|
+
if (pointerRestored) {
|
|
44849
|
+
await rollbackPreservedFiles(createdSeeds);
|
|
44850
|
+
await finishPreservedFileAttempt(seedJournal);
|
|
44851
|
+
}
|
|
44852
|
+
}
|
|
44853
|
+
]);
|
|
44854
|
+
}
|
|
44359
44855
|
let restartResult = null;
|
|
44360
44856
|
let restartError = null;
|
|
44361
44857
|
let installRolledBack = false;
|
|
44858
|
+
let seedCleanupError = null;
|
|
44362
44859
|
const priorRuntime = release.restart ? captureCapsuleRuntimeSettlement(request, previousRecord) : null;
|
|
44363
44860
|
if (release.restart) {
|
|
44364
44861
|
try {
|
|
@@ -44382,11 +44879,18 @@ async function installClaimedRelease(request, previousRecord, paths, claimedArch
|
|
|
44382
44879
|
release
|
|
44383
44880
|
);
|
|
44384
44881
|
installRolledBack = true;
|
|
44882
|
+
try {
|
|
44883
|
+
await rollbackPreservedFiles(createdSeeds);
|
|
44884
|
+
await finishPreservedFileAttempt(seedJournal);
|
|
44885
|
+
} catch (error) {
|
|
44886
|
+
seedCleanupError = error;
|
|
44887
|
+
}
|
|
44385
44888
|
} catch (error) {
|
|
44386
44889
|
restartError = error;
|
|
44387
44890
|
}
|
|
44388
44891
|
}
|
|
44389
44892
|
}
|
|
44893
|
+
if (!release.restart || restartResult) await finishPreservedFileAttempt(seedJournal);
|
|
44390
44894
|
const data2 = {
|
|
44391
44895
|
installed: !installRolledBack,
|
|
44392
44896
|
restartRequested: Boolean(release.restart),
|
|
@@ -44408,6 +44912,9 @@ async function installClaimedRelease(request, previousRecord, paths, claimedArch
|
|
|
44408
44912
|
if (restartResult) {
|
|
44409
44913
|
data2.lifecycle = restartResult;
|
|
44410
44914
|
}
|
|
44915
|
+
if (seedCleanupError) {
|
|
44916
|
+
data2.cleanup = { complete: false, reason: "preserved-seed-cleanup-failed", message: errorDetails(seedCleanupError).message };
|
|
44917
|
+
}
|
|
44411
44918
|
if (installRolledBack) {
|
|
44412
44919
|
data2.rollback = { applied: true, previousCurrentRelease };
|
|
44413
44920
|
}
|
|
@@ -44454,34 +44961,34 @@ async function installClaimedRelease(request, previousRecord, paths, claimedArch
|
|
|
44454
44961
|
writeEnvelope({ ok: true, data: data2, error: null });
|
|
44455
44962
|
}
|
|
44456
44963
|
async function claimReleaseArchive(request) {
|
|
44457
|
-
const expectedIncoming =
|
|
44458
|
-
if (
|
|
44964
|
+
const expectedIncoming = path5.join(request.host.remoteRoot, "incoming", `${request.release.id}.tar.gz`);
|
|
44965
|
+
if (path5.resolve(request.release.remoteArchive) !== path5.resolve(expectedIncoming)) {
|
|
44459
44966
|
throw helperError("Invalid release install request.", "Upload the release to the canonical Host incoming path and retry `sporades host push`.");
|
|
44460
44967
|
}
|
|
44461
|
-
const claimsDirectory =
|
|
44462
|
-
await
|
|
44463
|
-
const claimsStats = await
|
|
44968
|
+
const claimsDirectory = path5.join(request.host.remoteRoot, ".release-claims");
|
|
44969
|
+
await mkdir2(claimsDirectory, { recursive: true, mode: 448 });
|
|
44970
|
+
const claimsStats = await lstat2(claimsDirectory);
|
|
44464
44971
|
if (!claimsStats.isDirectory() || claimsStats.isSymbolicLink() || typeof process.getuid === "function" && claimsStats.uid !== process.getuid()) {
|
|
44465
44972
|
throw helperError("Hosted Capsule release claim directory is unsafe.", "Repair Host helper ownership of the release claim directory and retry.");
|
|
44466
44973
|
}
|
|
44467
44974
|
await chmod(claimsDirectory, 448);
|
|
44468
|
-
const claimedPath =
|
|
44469
|
-
await
|
|
44975
|
+
const claimedPath = path5.join(claimsDirectory, `${request.release.id}-${process.pid}-${randomBytes(16).toString("hex")}.tar.gz`);
|
|
44976
|
+
await rename2(request.release.remoteArchive, claimedPath);
|
|
44470
44977
|
try {
|
|
44471
|
-
const stats = await
|
|
44978
|
+
const stats = await lstat2(claimedPath);
|
|
44472
44979
|
if (!stats.isFile() || stats.isSymbolicLink() || stats.nlink !== 1 || stats.size > HOST_RELEASE_ARCHIVE_LIMITS.compressedBytes) {
|
|
44473
44980
|
throw helperError("Hosted Capsule release archive is unsafe.", "Upload one bounded regular archive file and retry `sporades host push`.");
|
|
44474
44981
|
}
|
|
44475
44982
|
await chmod(claimedPath, 384);
|
|
44476
44983
|
return { path: claimedPath, sha256: await releaseArchiveSha256(claimedPath) };
|
|
44477
44984
|
} catch (error) {
|
|
44478
|
-
await
|
|
44985
|
+
await rm2(claimedPath, { force: true });
|
|
44479
44986
|
throw error;
|
|
44480
44987
|
}
|
|
44481
44988
|
}
|
|
44482
44989
|
async function releaseArchiveSha256(archivePath) {
|
|
44483
44990
|
return new Promise((resolve, reject) => {
|
|
44484
|
-
const hash2 =
|
|
44991
|
+
const hash2 = createHash2("sha256");
|
|
44485
44992
|
const stream = createReadStream(archivePath);
|
|
44486
44993
|
stream.on("data", (chunk) => hash2.update(chunk));
|
|
44487
44994
|
stream.on("error", reject);
|
|
@@ -44491,7 +44998,7 @@ async function releaseArchiveSha256(archivePath) {
|
|
|
44491
44998
|
async function maybeSwapUnclaimedArchiveForTest(release) {
|
|
44492
44999
|
const replacement = process.env.SPORADES_TEST_HOST_ARCHIVE_SWAP_PATH;
|
|
44493
45000
|
if (!replacement) return;
|
|
44494
|
-
await
|
|
45001
|
+
await rename2(replacement, release.remoteArchive);
|
|
44495
45002
|
}
|
|
44496
45003
|
async function validateExtractedReleaseTree(root, expectedFiles) {
|
|
44497
45004
|
const expected = new Map(expectedFiles.map((file) => [file.path, file]));
|
|
@@ -44500,16 +45007,16 @@ async function validateExtractedReleaseTree(root, expectedFiles) {
|
|
|
44500
45007
|
let totalBytes = 0;
|
|
44501
45008
|
const publicClaims = [];
|
|
44502
45009
|
async function visit(directory, prefix = "") {
|
|
44503
|
-
for (const entry of await
|
|
45010
|
+
for (const entry of await readdir3(directory, { withFileTypes: true })) {
|
|
44504
45011
|
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
44505
45012
|
const normalized = relative.normalize("NFC");
|
|
44506
|
-
const safe = relative.length > 0 && !relative.startsWith("/") && !relative.includes("\\") && !relative.includes("\0") &&
|
|
45013
|
+
const safe = relative.length > 0 && !relative.startsWith("/") && !relative.includes("\\") && !relative.includes("\0") && path5.posix.normalize(relative) === relative && Buffer.byteLength(relative, "utf8") <= HOST_RELEASE_ARCHIVE_LIMITS.pathBytes && relative.split("/").every((segment) => segment && segment !== "." && segment !== "..");
|
|
44507
45014
|
if (!safe || canonical.has(normalized)) {
|
|
44508
45015
|
throw helperError("Extracted Hosted Capsule release is unsafe.", "Upload a release with unique bounded relative paths.");
|
|
44509
45016
|
}
|
|
44510
45017
|
canonical.add(normalized);
|
|
44511
|
-
const entryPath =
|
|
44512
|
-
const stats = await
|
|
45018
|
+
const entryPath = path5.join(directory, entry.name);
|
|
45019
|
+
const stats = await lstat2(entryPath);
|
|
44513
45020
|
if (stats.isSymbolicLink()) {
|
|
44514
45021
|
throw helperError("Extracted Hosted Capsule release is unsafe.", "Upload regular release files without symbolic links.");
|
|
44515
45022
|
}
|
|
@@ -44538,7 +45045,7 @@ async function validateExtractedReleaseTree(root, expectedFiles) {
|
|
|
44538
45045
|
}
|
|
44539
45046
|
publicClaims.push({ path: publicPath, size: stats.size });
|
|
44540
45047
|
}
|
|
44541
|
-
actual.push({ path: relative, size: stats.size, sha256:
|
|
45048
|
+
actual.push({ path: relative, size: stats.size, sha256: createHash2("sha256").update(await readFile4(entryPath)).digest("hex") });
|
|
44542
45049
|
}
|
|
44543
45050
|
}
|
|
44544
45051
|
await visit(root);
|
|
@@ -44679,7 +45186,7 @@ function readVerificationHealthTimeoutMs(request) {
|
|
|
44679
45186
|
return Math.min(value, 18e4);
|
|
44680
45187
|
}
|
|
44681
45188
|
async function routeVerifiedFailureToUnavailable(request, releaseId, message) {
|
|
44682
|
-
const lifecycle = normaliseLifecycle(request);
|
|
45189
|
+
const lifecycle = normaliseLifecycle(request, await readRegistryRecordForCapsule(request, "lifecycle"));
|
|
44683
45190
|
stopAndRemoveContainer(lifecycle.container.name);
|
|
44684
45191
|
try {
|
|
44685
45192
|
await writeUnavailableRoute(lifecycle);
|
|
@@ -44731,9 +45238,9 @@ async function maybeFallbackToPreviousRelease(request, failedReleaseId, previous
|
|
|
44731
45238
|
}
|
|
44732
45239
|
async function switchCurrentReleaseLink(currentLink, releaseDirectory) {
|
|
44733
45240
|
const tempCurrentLink = `${currentLink}.tmp-${process.pid}`;
|
|
44734
|
-
await
|
|
45241
|
+
await rm2(tempCurrentLink, { force: true });
|
|
44735
45242
|
await symlink(releaseDirectory, tempCurrentLink);
|
|
44736
|
-
await
|
|
45243
|
+
await rename2(tempCurrentLink, currentLink);
|
|
44737
45244
|
}
|
|
44738
45245
|
async function restoreFailedReleaseAfterFallbackRestartFailure(request, failedReleaseId, fallbackReleaseId, reason, restartError) {
|
|
44739
45246
|
const failedPaths = canonicalRollbackPaths(request, failedReleaseId);
|
|
@@ -44749,7 +45256,7 @@ async function restoreFailedReleaseAfterFallbackRestartFailure(request, failedRe
|
|
|
44749
45256
|
reason
|
|
44750
45257
|
);
|
|
44751
45258
|
try {
|
|
44752
|
-
await writeUnavailableRoute(normaliseLifecycle(request));
|
|
45259
|
+
await writeUnavailableRoute(normaliseLifecycle(request, await readRegistryRecordForCapsule(request, "lifecycle")));
|
|
44753
45260
|
} catch {
|
|
44754
45261
|
}
|
|
44755
45262
|
}
|
|
@@ -44850,8 +45357,10 @@ async function startCapsule(request, options = {}) {
|
|
|
44850
45357
|
const lifecycle = normaliseLifecycle(
|
|
44851
45358
|
request,
|
|
44852
45359
|
registryRecord,
|
|
44853
|
-
options.trustedRegistryLifecycle === true ? { ignoreProvidedLifecycle: true } : {}
|
|
45360
|
+
{ ...options.trustedRegistryLifecycle === true ? { ignoreProvidedLifecycle: true } : {}, releaseId }
|
|
44854
45361
|
);
|
|
45362
|
+
const recordedRelease = normaliseReleaseHistory(registryRecord).find((entry) => entry.id === releaseId);
|
|
45363
|
+
await preparePreservedReleaseFiles(request, recordedRelease);
|
|
44855
45364
|
if (options.containerQuiesced !== true) stopAndRemoveContainer(lifecycle.container.name);
|
|
44856
45365
|
if (options.dataPrepared !== true) await prepareWritableDataPath(paths.data);
|
|
44857
45366
|
await recordReleaseStartAttempt(request, releaseId);
|
|
@@ -44985,7 +45494,7 @@ function releaseIncludesSealedServerEnvPrivateKey(release) {
|
|
|
44985
45494
|
return Boolean(release.sealedServerEnvIncluded && privateKey && privateKeyPath);
|
|
44986
45495
|
}
|
|
44987
45496
|
function releasePrivateKeyPath(paths, releaseId) {
|
|
44988
|
-
return
|
|
45497
|
+
return path5.join(paths.data, "sealed-server-env", "releases", `${releaseId}.private.pem`);
|
|
44989
45498
|
}
|
|
44990
45499
|
async function installSealedServerEnvPrivateKey(release, paths) {
|
|
44991
45500
|
if (!releaseIncludesSealedServerEnvPrivateKey(release)) return;
|
|
@@ -44993,9 +45502,9 @@ async function installSealedServerEnvPrivateKey(release, paths) {
|
|
|
44993
45502
|
const privateKeyPath = releasePrivateKeyPath(paths, release.id);
|
|
44994
45503
|
const dataHandle = await openCanonicalRuntimeDataDirectory(paths.data, true);
|
|
44995
45504
|
try {
|
|
44996
|
-
const rootHandle = await openOrCreateRuntimeDirectory(dataHandle,
|
|
45505
|
+
const rootHandle = await openOrCreateRuntimeDirectory(dataHandle, path5.join(paths.data, "sealed-server-env"));
|
|
44997
45506
|
try {
|
|
44998
|
-
const releasesHandle = await openOrCreateRuntimeDirectory(rootHandle,
|
|
45507
|
+
const releasesHandle = await openOrCreateRuntimeDirectory(rootHandle, path5.dirname(privateKeyPath));
|
|
44999
45508
|
try {
|
|
45000
45509
|
await publishRuntimeFile(releasesHandle, privateKeyPath, privateKey, 384, "release-private-key-publish");
|
|
45001
45510
|
} finally {
|
|
@@ -45010,15 +45519,18 @@ async function installSealedServerEnvPrivateKey(release, paths) {
|
|
|
45010
45519
|
}
|
|
45011
45520
|
async function removeInstalledReleasePrivateKey(release, paths) {
|
|
45012
45521
|
if (!releaseIncludesSealedServerEnvPrivateKey(release)) return;
|
|
45013
|
-
|
|
45522
|
+
await removeReleasePrivateKeyIfPresent(paths, release.id);
|
|
45523
|
+
}
|
|
45524
|
+
async function removeReleasePrivateKeyIfPresent(paths, releaseId) {
|
|
45525
|
+
const privateKeyPath = releasePrivateKeyPath(paths, releaseId);
|
|
45014
45526
|
const dataHandle = await openCanonicalRuntimeDataDirectory(paths.data, false);
|
|
45015
45527
|
try {
|
|
45016
|
-
const rootHandle = await openOrCreateRuntimeDirectory(dataHandle,
|
|
45528
|
+
const rootHandle = await openOrCreateRuntimeDirectory(dataHandle, path5.join(paths.data, "sealed-server-env"));
|
|
45017
45529
|
try {
|
|
45018
|
-
const releasesHandle = await openOrCreateRuntimeDirectory(rootHandle,
|
|
45530
|
+
const releasesHandle = await openOrCreateRuntimeDirectory(rootHandle, path5.dirname(privateKeyPath));
|
|
45019
45531
|
try {
|
|
45020
|
-
const descriptorPath = descriptorChildPath(releasesHandle.fd,
|
|
45021
|
-
const retained = await
|
|
45532
|
+
const descriptorPath = descriptorChildPath(releasesHandle.fd, path5.basename(privateKeyPath), privateKeyPath);
|
|
45533
|
+
const retained = await open2(descriptorPath, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW).catch((error) => {
|
|
45022
45534
|
if (errorDetails(error).code === "ENOENT") return null;
|
|
45023
45535
|
throw runtimeDataTrustError(privateKeyPath);
|
|
45024
45536
|
});
|
|
@@ -45027,7 +45539,7 @@ async function removeInstalledReleasePrivateKey(release, paths) {
|
|
|
45027
45539
|
const identity = await retained.stat();
|
|
45028
45540
|
if (!identity.isFile()) throw runtimeDataTrustError(privateKeyPath);
|
|
45029
45541
|
await assertRuntimeDataPathIdentity(privateKeyPath, { dev: identity.dev, ino: identity.ino }, false);
|
|
45030
|
-
await
|
|
45542
|
+
await rm2(descriptorPath, { force: true });
|
|
45031
45543
|
} finally {
|
|
45032
45544
|
await retained.close();
|
|
45033
45545
|
}
|
|
@@ -45044,7 +45556,7 @@ async function removeInstalledReleasePrivateKey(release, paths) {
|
|
|
45044
45556
|
async function captureReleaseInstallRoute(request, previousRecord) {
|
|
45045
45557
|
const lifecycle = normaliseLifecycle(request, previousRecord, { ignoreProvidedLifecycle: true });
|
|
45046
45558
|
const routeFile = lifecycle.routes.running.routeFile;
|
|
45047
|
-
const contents = await
|
|
45559
|
+
const contents = await readFile4(routeFile, "utf8").catch((error) => {
|
|
45048
45560
|
if (errorDetails(error).code === "ENOENT") return null;
|
|
45049
45561
|
throw error;
|
|
45050
45562
|
});
|
|
@@ -45052,21 +45564,22 @@ async function captureReleaseInstallRoute(request, previousRecord) {
|
|
|
45052
45564
|
}
|
|
45053
45565
|
async function restoreCurrentReleasePointerTarget(currentLink, previousTarget) {
|
|
45054
45566
|
const temporary = `${currentLink}.restore-${process.pid}-${randomBytes(8).toString("hex")}`;
|
|
45055
|
-
await
|
|
45567
|
+
await rm2(temporary, { force: true });
|
|
45056
45568
|
if (!previousTarget) {
|
|
45057
|
-
await
|
|
45569
|
+
await rm2(currentLink, { force: true });
|
|
45058
45570
|
return;
|
|
45059
45571
|
}
|
|
45060
45572
|
await symlink(previousTarget, temporary);
|
|
45061
|
-
await
|
|
45573
|
+
await rename2(temporary, currentLink);
|
|
45062
45574
|
}
|
|
45063
|
-
async function restoreReleaseInstallRoute(snapshot) {
|
|
45575
|
+
async function restoreReleaseInstallRoute(snapshot, reloadWhenAbsent = false) {
|
|
45064
45576
|
if (snapshot.contents !== null) {
|
|
45065
45577
|
await applyManagedRoute(snapshot.lifecycle, snapshot.routeFile, snapshot.contents);
|
|
45066
45578
|
return;
|
|
45067
45579
|
}
|
|
45068
45580
|
await withManagedRouteLock(snapshot.routeFile, async () => {
|
|
45069
45581
|
const removed = await removeManagedRouteLocked(snapshot.lifecycle, snapshot.routeFile);
|
|
45582
|
+
if (reloadWhenAbsent && !removed.removed) reloadCaddy(snapshot.lifecycle);
|
|
45070
45583
|
await finalizeRemovedRouteLocked(removed);
|
|
45071
45584
|
});
|
|
45072
45585
|
}
|
|
@@ -45096,7 +45609,7 @@ async function restoreFailedReleaseInstall(request, paths, previousRecord, previ
|
|
|
45096
45609
|
if (!runningRouteRestored && !missingRunningRuntimeSettled) await restoreReleaseInstallRoute(previousRoute);
|
|
45097
45610
|
if (!missingRunningRuntimeSettled) await writeRegistryContentsAtomic(registryPath(request), previousRegistryContents);
|
|
45098
45611
|
await removeInstalledReleasePrivateKey(release, paths);
|
|
45099
|
-
await
|
|
45612
|
+
await rm2(paths.release, { recursive: true, force: true });
|
|
45100
45613
|
} catch {
|
|
45101
45614
|
throw helperError(
|
|
45102
45615
|
"Hosted Capsule runtime restoration failed.",
|
|
@@ -45120,11 +45633,11 @@ function validateSealedServerEnvPrivateKeyPath(release, paths) {
|
|
|
45120
45633
|
);
|
|
45121
45634
|
}
|
|
45122
45635
|
const compatiblePrivateKeyPaths = /* @__PURE__ */ new Set([
|
|
45123
|
-
|
|
45124
|
-
|
|
45125
|
-
...fingerprint ? [
|
|
45636
|
+
path5.resolve(releasePrivateKeyPath(paths, release.id)),
|
|
45637
|
+
path5.resolve(path5.join(paths.data, "sealed-server-env", "server-env.private.pem")),
|
|
45638
|
+
...fingerprint ? [path5.resolve(path5.join(paths.data, "sealed-server-env", "keys", `${fingerprint}.private.pem`))] : []
|
|
45126
45639
|
]);
|
|
45127
|
-
if (typeof privateKeyPath !== "string" || !compatiblePrivateKeyPaths.has(
|
|
45640
|
+
if (typeof privateKeyPath !== "string" || !compatiblePrivateKeyPaths.has(path5.resolve(privateKeyPath))) {
|
|
45128
45641
|
throw helperError(
|
|
45129
45642
|
"Invalid Sealed Server env private key path.",
|
|
45130
45643
|
"Update the Sporades CLI and retry `sporades host push`."
|
|
@@ -45343,7 +45856,7 @@ async function routeRuntimeExhaustionToUnavailable(request, record, health) {
|
|
|
45343
45856
|
async function stopCapsule(request, options = {}) {
|
|
45344
45857
|
validateLifecycleRequest(request);
|
|
45345
45858
|
await verifyRegisteredCapsule(request, "lifecycle");
|
|
45346
|
-
const lifecycle = normaliseLifecycle(request);
|
|
45859
|
+
const lifecycle = normaliseLifecycle(request, await readRegistryRecordForCapsule(request, "lifecycle"));
|
|
45347
45860
|
stopAndRemoveContainer(lifecycle.container.name);
|
|
45348
45861
|
await writeUnavailableRoute(lifecycle);
|
|
45349
45862
|
await updateRegistryStatus(request, "stopped");
|
|
@@ -45366,10 +45879,9 @@ async function restartCapsule(request, options = {}) {
|
|
|
45366
45879
|
registryRecord,
|
|
45367
45880
|
options.trustedRegistryLifecycle === true ? { ignoreProvidedLifecycle: true } : {}
|
|
45368
45881
|
);
|
|
45369
|
-
if (options.containerQuiesced !== true) stopAndRemoveContainer(lifecycle.container.name);
|
|
45370
45882
|
const startResult = await startCapsule(request, {
|
|
45371
45883
|
write: false,
|
|
45372
|
-
containerQuiesced: true,
|
|
45884
|
+
containerQuiesced: options.containerQuiesced === true,
|
|
45373
45885
|
dataPrepared: options.dataPrepared === true,
|
|
45374
45886
|
trustedRegistryLifecycle: true
|
|
45375
45887
|
});
|
|
@@ -45518,6 +46030,7 @@ async function listReleases(request) {
|
|
|
45518
46030
|
subname: record.subname,
|
|
45519
46031
|
domain: record.domain,
|
|
45520
46032
|
hostedUrl: record.hostedUrl ?? `${request.host.scheme ?? "https"}://${record.subname}.${request.host.domain}`,
|
|
46033
|
+
aliasDomains: validateAliasDomains(record.aliasDomains),
|
|
45521
46034
|
remoteCapsuleId: record.remoteCapsuleId ?? `${request.host.domain}/${record.subname}`
|
|
45522
46035
|
},
|
|
45523
46036
|
currentRelease: publicCurrentRelease(record),
|
|
@@ -45558,9 +46071,9 @@ async function rollbackRelease(request) {
|
|
|
45558
46071
|
await assertRollbackReleaseFiles(request, paths.release, selectedRelease);
|
|
45559
46072
|
const previousCurrentRelease = record.currentRelease ?? null;
|
|
45560
46073
|
const tempCurrentLink = `${paths.currentLink}.tmp-${process.pid}`;
|
|
45561
|
-
await
|
|
46074
|
+
await rm2(tempCurrentLink, { force: true });
|
|
45562
46075
|
await symlink(paths.release, tempCurrentLink);
|
|
45563
|
-
await
|
|
46076
|
+
await rename2(tempCurrentLink, paths.currentLink);
|
|
45564
46077
|
await recordReleaseRollbackSelected(request, releaseId);
|
|
45565
46078
|
let lifecycle = null;
|
|
45566
46079
|
let restartError = null;
|
|
@@ -45586,7 +46099,7 @@ async function rollbackRelease(request) {
|
|
|
45586
46099
|
return;
|
|
45587
46100
|
}
|
|
45588
46101
|
try {
|
|
45589
|
-
await writeUnavailableRoute(normaliseLifecycle(request));
|
|
46102
|
+
await writeUnavailableRoute(normaliseLifecycle(request, await readRegistryRecordForCapsule(request, "lifecycle")));
|
|
45590
46103
|
} catch (error) {
|
|
45591
46104
|
restartError = restartError ?? error;
|
|
45592
46105
|
}
|
|
@@ -45665,6 +46178,7 @@ async function listCapsules(request) {
|
|
|
45665
46178
|
subname: record.subname,
|
|
45666
46179
|
domain: record.domain,
|
|
45667
46180
|
hostedUrl: record.hostedUrl ?? `${request.host.scheme ?? "https"}://${record.subname}.${request.host.domain}`,
|
|
46181
|
+
aliasDomains: validateAliasDomains(record.aliasDomains),
|
|
45668
46182
|
registry: {
|
|
45669
46183
|
remoteCapsuleId: record.remoteCapsuleId ?? `${request.host.domain}/${record.subname}`,
|
|
45670
46184
|
createdAt: record.createdAt ?? null,
|
|
@@ -45709,7 +46223,7 @@ function createUnregisterResult(request, unregister, record, idempotent, route =
|
|
|
45709
46223
|
registryRecord: unregister.registryRecord,
|
|
45710
46224
|
directories: unregister.directories,
|
|
45711
46225
|
preserved: {
|
|
45712
|
-
releases: record.currentRelease?.id ?
|
|
46226
|
+
releases: record.currentRelease?.id ? path5.join(unregister.directories.releases, record.currentRelease.id) : unregister.directories.releases,
|
|
45713
46227
|
data: unregister.directories.data
|
|
45714
46228
|
},
|
|
45715
46229
|
deleteAfter: record.deleteAfter ?? null,
|
|
@@ -45758,28 +46272,28 @@ function createDeleteResult(request, deletion, removals) {
|
|
|
45758
46272
|
};
|
|
45759
46273
|
}
|
|
45760
46274
|
function canonicalReleasePaths(request) {
|
|
45761
|
-
const capsule =
|
|
46275
|
+
const capsule = path5.join(
|
|
45762
46276
|
request.host.remoteRoot,
|
|
45763
46277
|
"hosts",
|
|
45764
46278
|
request.host.domain,
|
|
45765
46279
|
"capsules",
|
|
45766
46280
|
request.capsule.subname
|
|
45767
46281
|
);
|
|
45768
|
-
const releases =
|
|
46282
|
+
const releases = path5.join(capsule, "releases");
|
|
45769
46283
|
return {
|
|
45770
46284
|
capsule,
|
|
45771
46285
|
releases,
|
|
45772
|
-
release: request.release?.id ?
|
|
45773
|
-
data:
|
|
45774
|
-
logs:
|
|
45775
|
-
currentLink:
|
|
46286
|
+
release: request.release?.id ? path5.join(releases, request.release.id) : null,
|
|
46287
|
+
data: path5.join(capsule, "data"),
|
|
46288
|
+
logs: path5.join(capsule, "logs"),
|
|
46289
|
+
currentLink: path5.join(capsule, "current")
|
|
45776
46290
|
};
|
|
45777
46291
|
}
|
|
45778
46292
|
function canonicalRollbackPaths(request, releaseId) {
|
|
45779
46293
|
const paths = canonicalReleasePaths({ ...request, release: { id: releaseId, remoteArchive: "", files: [] } });
|
|
45780
46294
|
return {
|
|
45781
46295
|
...paths,
|
|
45782
|
-
release:
|
|
46296
|
+
release: path5.join(paths.releases, releaseId)
|
|
45783
46297
|
};
|
|
45784
46298
|
}
|
|
45785
46299
|
function normaliseLifecycle(request, registryRecord = null, options = {}) {
|
|
@@ -45806,12 +46320,12 @@ function normaliseLifecycle(request, registryRecord = null, options = {}) {
|
|
|
45806
46320
|
};
|
|
45807
46321
|
const defaultMounts = {
|
|
45808
46322
|
files: [
|
|
45809
|
-
{ host:
|
|
45810
|
-
{ host:
|
|
45811
|
-
{ host:
|
|
45812
|
-
{ host:
|
|
46323
|
+
{ host: path5.join(currentLink, "server.mjs"), container: "/app/server.mjs", mode: "ro" },
|
|
46324
|
+
{ host: path5.join(currentLink, "public"), container: "/app/public", mode: "ro" },
|
|
46325
|
+
{ host: path5.join(currentLink, "sporades.json"), container: "/app/sporades.json", mode: "ro" },
|
|
46326
|
+
{ host: path5.join(currentLink, ".env.sporades.server"), container: "/app/.env.sporades.server", mode: "ro", optional: true },
|
|
45813
46327
|
{
|
|
45814
|
-
host:
|
|
46328
|
+
host: path5.join(currentLink, ".sporades", "sealed-server-env", "server-env.sealed.json"),
|
|
45815
46329
|
container: "/app/.sporades/sealed-server-env/server-env.sealed.json",
|
|
45816
46330
|
mode: "ro",
|
|
45817
46331
|
optional: true
|
|
@@ -45827,6 +46341,8 @@ function normaliseLifecycle(request, registryRecord = null, options = {}) {
|
|
|
45827
46341
|
],
|
|
45828
46342
|
data: { host: paths.data, container: "/app/data", mode: "rw" }
|
|
45829
46343
|
};
|
|
46344
|
+
const deployRelease = normaliseReleaseHistory(registryRecord).find((entry) => entry.id === (options.releaseId ?? registryRecord?.currentRelease?.id));
|
|
46345
|
+
const additionalMounts = deployFileMounts(resolveDeployFiles(deployRelease?.source?.deployFiles), currentLink, path5.join(paths.capsule, "preserved-files"));
|
|
45830
46346
|
const fileMounts = authoritativeSshAuthorizedKeysMount(
|
|
45831
46347
|
authoritativeSealedServerEnvPrivateKeyMount(
|
|
45832
46348
|
provided.mounts?.files ?? defaultMounts.files,
|
|
@@ -45863,9 +46379,11 @@ function normaliseLifecycle(request, registryRecord = null, options = {}) {
|
|
|
45863
46379
|
...baseImageLabels(helperPackageBaseImage.updatePolicy.mode)
|
|
45864
46380
|
}
|
|
45865
46381
|
};
|
|
46382
|
+
const aliasDomains = validateAliasDomains(registryRecord?.aliasDomains);
|
|
45866
46383
|
const canonicalRoutes = {
|
|
45867
46384
|
running: {
|
|
45868
46385
|
hostname: `${subname}.${domain}`,
|
|
46386
|
+
...aliasDomains.length ? { aliasDomains } : {},
|
|
45869
46387
|
target: "container",
|
|
45870
46388
|
containerName,
|
|
45871
46389
|
port: 4e3,
|
|
@@ -45875,6 +46393,7 @@ function normaliseLifecycle(request, registryRecord = null, options = {}) {
|
|
|
45875
46393
|
},
|
|
45876
46394
|
unavailable: {
|
|
45877
46395
|
hostname: `${subname}.${domain}`,
|
|
46396
|
+
...aliasDomains.length ? { aliasDomains } : {},
|
|
45878
46397
|
target: "hosted-capsule-unavailable",
|
|
45879
46398
|
statusCode: 503,
|
|
45880
46399
|
routeFile,
|
|
@@ -45909,7 +46428,7 @@ function normaliseLifecycle(request, registryRecord = null, options = {}) {
|
|
|
45909
46428
|
},
|
|
45910
46429
|
remoteRoot: request.host.remoteRoot,
|
|
45911
46430
|
mounts: {
|
|
45912
|
-
files: fileMounts,
|
|
46431
|
+
files: [...fileMounts, ...additionalMounts],
|
|
45913
46432
|
data: defaultMounts.data
|
|
45914
46433
|
},
|
|
45915
46434
|
container: canonicalContainer,
|
|
@@ -45923,12 +46442,12 @@ function canonicalLifecycleRouteTls(request, registryRecord, provided) {
|
|
|
45923
46442
|
if (!(/* @__PURE__ */ new Set(["automatic", "cloudflare-origin"])).has(mode) || suppliedModes.some((value) => value !== mode)) {
|
|
45924
46443
|
throw invalidLifecycleAuthorityError();
|
|
45925
46444
|
}
|
|
45926
|
-
const directory =
|
|
46445
|
+
const directory = path5.join(request.host.remoteRoot, "hosts", request.host.domain, "tls");
|
|
45927
46446
|
return {
|
|
45928
46447
|
mode,
|
|
45929
46448
|
directory,
|
|
45930
|
-
certificate: mode === "cloudflare-origin" ?
|
|
45931
|
-
key: mode === "cloudflare-origin" ?
|
|
46449
|
+
certificate: mode === "cloudflare-origin" ? path5.join(directory, "origin.crt") : null,
|
|
46450
|
+
key: mode === "cloudflare-origin" ? path5.join(directory, "origin.key") : null
|
|
45932
46451
|
};
|
|
45933
46452
|
}
|
|
45934
46453
|
function invalidLifecycleAuthorityError() {
|
|
@@ -45971,14 +46490,14 @@ function assertCanonicalLifecycleAuthority(provided, canonical) {
|
|
|
45971
46490
|
const allowedByContainer = new Map(canonical.defaultMounts.files.map((mount) => [mount.container, mount]));
|
|
45972
46491
|
const privateKeyContainerPath = "/app/.sporades/sealed-server-env/server-env.private.pem";
|
|
45973
46492
|
const legacySealedServerEnvPrivateKeyMount = {
|
|
45974
|
-
host:
|
|
46493
|
+
host: path5.join(canonical.paths.data, "sealed-server-env", "server-env.private.pem"),
|
|
45975
46494
|
container: privateKeyContainerPath,
|
|
45976
46495
|
mode: "ro",
|
|
45977
46496
|
optional: true
|
|
45978
46497
|
};
|
|
45979
46498
|
const legacyAllowed = [
|
|
45980
|
-
{ host:
|
|
45981
|
-
{ host:
|
|
46499
|
+
{ host: path5.join(canonical.currentLink, "client.js"), container: "/app/client.js", mode: "ro" },
|
|
46500
|
+
{ host: path5.join(canonical.currentLink, "index.html"), container: "/app/index.html", mode: "ro" }
|
|
45982
46501
|
];
|
|
45983
46502
|
for (const mount of legacyAllowed) allowedByContainer.set(mount.container, mount);
|
|
45984
46503
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -46260,10 +46779,10 @@ function inspectContainerLifecycle(containerName) {
|
|
|
46260
46779
|
};
|
|
46261
46780
|
}
|
|
46262
46781
|
async function readCapsuleRegistryRecords(request) {
|
|
46263
|
-
const registryDirectory =
|
|
46782
|
+
const registryDirectory = path5.join(request.host.remoteRoot, "hosts", request.host.domain, "registry", "capsules");
|
|
46264
46783
|
let entries;
|
|
46265
46784
|
try {
|
|
46266
|
-
entries = await
|
|
46785
|
+
entries = await readdir3(registryDirectory, { withFileTypes: true });
|
|
46267
46786
|
} catch (error) {
|
|
46268
46787
|
if (errorDetails(error).code === "ENOENT") {
|
|
46269
46788
|
return [];
|
|
@@ -46273,10 +46792,10 @@ async function readCapsuleRegistryRecords(request) {
|
|
|
46273
46792
|
const records = [];
|
|
46274
46793
|
const files = entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => entry.name).sort();
|
|
46275
46794
|
for (const file of files) {
|
|
46276
|
-
const recordPath =
|
|
46795
|
+
const recordPath = path5.join(registryDirectory, file);
|
|
46277
46796
|
let record;
|
|
46278
46797
|
try {
|
|
46279
|
-
record = JSON.parse(await
|
|
46798
|
+
record = JSON.parse(await readFile4(recordPath, "utf8"));
|
|
46280
46799
|
} catch (error) {
|
|
46281
46800
|
if (error instanceof SyntaxError) {
|
|
46282
46801
|
throw helperError(
|
|
@@ -46404,19 +46923,21 @@ function normaliseProvidedBaseImage(value) {
|
|
|
46404
46923
|
version: provided.version ?? SPORADES_BASE_IMAGE.version
|
|
46405
46924
|
};
|
|
46406
46925
|
}
|
|
46407
|
-
function normaliseRegistration(request) {
|
|
46926
|
+
function normaliseRegistration(request, existing = null) {
|
|
46927
|
+
const aliasDomains = validateAliasDomains(request.registration?.aliasDomains ?? existing?.aliasDomains);
|
|
46408
46928
|
const subname = request.capsule.subname;
|
|
46409
46929
|
const domain = request.host.domain;
|
|
46410
46930
|
const remoteRoot = request.host.remoteRoot;
|
|
46411
46931
|
const scheme = request.host.scheme ?? "https";
|
|
46412
46932
|
const hostedUrl = `${scheme}://${subname}.${domain}`;
|
|
46413
46933
|
const remoteCapsuleId = `${domain}/${subname}`;
|
|
46414
|
-
const capsuleDirectory =
|
|
46415
|
-
const routeFile =
|
|
46934
|
+
const capsuleDirectory = path5.join(remoteRoot, "hosts", domain, "capsules", subname);
|
|
46935
|
+
const routeFile = path5.join(remoteRoot, "caddy", "hosts", domain, `${subname}.caddy`);
|
|
46416
46936
|
const routeTls = normaliseRegistrationTls(request);
|
|
46417
46937
|
const accessLog = canonicalCapsuleHttpLogPath(request, remoteRoot);
|
|
46418
46938
|
const route = {
|
|
46419
46939
|
hostname: `${subname}.${domain}`,
|
|
46940
|
+
...aliasDomains.length ? { aliasDomains } : {},
|
|
46420
46941
|
target: "hosted-capsule-unavailable",
|
|
46421
46942
|
statusCode: 503,
|
|
46422
46943
|
routeFile,
|
|
@@ -46429,12 +46950,13 @@ function normaliseRegistration(request) {
|
|
|
46429
46950
|
remoteRoot,
|
|
46430
46951
|
hostedUrl,
|
|
46431
46952
|
remoteCapsuleId,
|
|
46432
|
-
|
|
46953
|
+
aliasDomains,
|
|
46954
|
+
registryRecord: path5.join(remoteRoot, "hosts", domain, "registry", "capsules", `${subname}.json`),
|
|
46433
46955
|
directories: {
|
|
46434
46956
|
capsule: capsuleDirectory,
|
|
46435
|
-
releases:
|
|
46436
|
-
data:
|
|
46437
|
-
logs:
|
|
46957
|
+
releases: path5.join(capsuleDirectory, "releases"),
|
|
46958
|
+
data: path5.join(capsuleDirectory, "data"),
|
|
46959
|
+
logs: path5.join(capsuleDirectory, "logs")
|
|
46438
46960
|
},
|
|
46439
46961
|
baseImage: normaliseProvidedBaseImage(request.registration?.baseImage),
|
|
46440
46962
|
route,
|
|
@@ -46452,19 +46974,19 @@ function normaliseUnregister(request) {
|
|
|
46452
46974
|
const scheme = request.host.scheme ?? "https";
|
|
46453
46975
|
const hostedUrl = `${scheme}://${subname}.${domain}`;
|
|
46454
46976
|
const remoteCapsuleId = `${domain}/${subname}`;
|
|
46455
|
-
const capsuleDirectory =
|
|
46456
|
-
const routeFile =
|
|
46977
|
+
const capsuleDirectory = path5.join(remoteRoot, "hosts", domain, "capsules", subname);
|
|
46978
|
+
const routeFile = path5.join(remoteRoot, "caddy", "hosts", domain, `${subname}.caddy`);
|
|
46457
46979
|
const containerName = createHostedContainerName(domain, subname);
|
|
46458
46980
|
return {
|
|
46459
46981
|
subname,
|
|
46460
46982
|
domain,
|
|
46461
46983
|
hostedUrl,
|
|
46462
46984
|
remoteCapsuleId,
|
|
46463
|
-
registryRecord:
|
|
46985
|
+
registryRecord: path5.join(remoteRoot, "hosts", domain, "registry", "capsules", `${subname}.json`),
|
|
46464
46986
|
directories: {
|
|
46465
46987
|
capsule: capsuleDirectory,
|
|
46466
|
-
releases:
|
|
46467
|
-
data:
|
|
46988
|
+
releases: path5.join(capsuleDirectory, "releases"),
|
|
46989
|
+
data: path5.join(capsuleDirectory, "data")
|
|
46468
46990
|
},
|
|
46469
46991
|
container: {
|
|
46470
46992
|
name: containerName
|
|
@@ -46485,21 +47007,21 @@ function normaliseDeletion(request) {
|
|
|
46485
47007
|
const domain = request.host.domain;
|
|
46486
47008
|
const remoteRoot = request.host.remoteRoot;
|
|
46487
47009
|
const scheme = request.host.scheme ?? "https";
|
|
46488
|
-
const capsuleDirectory =
|
|
47010
|
+
const capsuleDirectory = path5.join(remoteRoot, "hosts", domain, "capsules", subname);
|
|
46489
47011
|
return {
|
|
46490
47012
|
subname,
|
|
46491
47013
|
domain,
|
|
46492
47014
|
hostedUrl: `${scheme}://${subname}.${domain}`,
|
|
46493
47015
|
remoteCapsuleId: `${domain}/${subname}`,
|
|
46494
|
-
registryRecord:
|
|
47016
|
+
registryRecord: path5.join(remoteRoot, "hosts", domain, "registry", "capsules", `${subname}.json`),
|
|
46495
47017
|
directories: {
|
|
46496
47018
|
capsule: capsuleDirectory,
|
|
46497
|
-
releases:
|
|
46498
|
-
data:
|
|
47019
|
+
releases: path5.join(capsuleDirectory, "releases"),
|
|
47020
|
+
data: path5.join(capsuleDirectory, "data")
|
|
46499
47021
|
},
|
|
46500
47022
|
route: {
|
|
46501
47023
|
hostname: `${subname}.${domain}`,
|
|
46502
|
-
routeFile:
|
|
47024
|
+
routeFile: path5.join(remoteRoot, "caddy", "hosts", domain, `${subname}.caddy`)
|
|
46503
47025
|
},
|
|
46504
47026
|
lifecycle: {
|
|
46505
47027
|
remoteRoot
|
|
@@ -46510,17 +47032,17 @@ function normaliseRegistrationTls(request) {
|
|
|
46510
47032
|
const remoteRoot = request.host.remoteRoot;
|
|
46511
47033
|
const domain = request.host.domain;
|
|
46512
47034
|
const tlsMode = request.registration?.bootstrap?.tls?.mode ?? request.bootstrap?.tls?.mode ?? "automatic";
|
|
46513
|
-
const tlsDirectory =
|
|
47035
|
+
const tlsDirectory = path5.join(remoteRoot, "hosts", domain, "tls");
|
|
46514
47036
|
return {
|
|
46515
47037
|
mode: tlsMode,
|
|
46516
47038
|
directory: tlsDirectory,
|
|
46517
|
-
certificate: tlsMode === "cloudflare-origin" ?
|
|
46518
|
-
key: tlsMode === "cloudflare-origin" ?
|
|
47039
|
+
certificate: tlsMode === "cloudflare-origin" ? path5.join(tlsDirectory, "origin.crt") : null,
|
|
47040
|
+
key: tlsMode === "cloudflare-origin" ? path5.join(tlsDirectory, "origin.key") : null
|
|
46519
47041
|
};
|
|
46520
47042
|
}
|
|
46521
47043
|
async function ensureHostedDomainBootstrapped(request, registration) {
|
|
46522
|
-
const caddyfile =
|
|
46523
|
-
const domainInclude =
|
|
47044
|
+
const caddyfile = path5.join(request.host.remoteRoot, "caddy", "Caddyfile");
|
|
47045
|
+
const domainInclude = path5.join(request.host.remoteRoot, "caddy", "hosts", `${request.host.domain}.caddy`);
|
|
46524
47046
|
const bootstrapped = await pathExists(caddyfile) && await pathExists(domainInclude);
|
|
46525
47047
|
if (bootstrapped) {
|
|
46526
47048
|
return;
|
|
@@ -46539,6 +47061,7 @@ function createRegistrationRecord(registration, sealedServerEnv = null) {
|
|
|
46539
47061
|
domain: registration.domain,
|
|
46540
47062
|
remoteCapsuleId: registration.remoteCapsuleId,
|
|
46541
47063
|
hostedUrl: registration.hostedUrl,
|
|
47064
|
+
aliasDomains: registration.aliasDomains,
|
|
46542
47065
|
status: "registered",
|
|
46543
47066
|
createdAt: now,
|
|
46544
47067
|
updatedAt: now,
|
|
@@ -46598,11 +47121,11 @@ async function generateHostSealedEnvKeyPair(dataDirectory) {
|
|
|
46598
47121
|
};
|
|
46599
47122
|
}
|
|
46600
47123
|
async function openOrCreateRuntimeDirectory(parentHandle, targetPath) {
|
|
46601
|
-
const descriptorPath = descriptorChildPath(parentHandle.fd,
|
|
46602
|
-
await
|
|
47124
|
+
const descriptorPath = descriptorChildPath(parentHandle.fd, path5.basename(targetPath), targetPath);
|
|
47125
|
+
await mkdir2(descriptorPath, { mode: 448 }).catch((error) => {
|
|
46603
47126
|
if (errorDetails(error).code !== "EEXIST") throw runtimeDataTrustError(targetPath);
|
|
46604
47127
|
});
|
|
46605
|
-
const handle = await
|
|
47128
|
+
const handle = await open2(
|
|
46606
47129
|
descriptorPath,
|
|
46607
47130
|
fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW | (fsConstants.O_DIRECTORY ?? 0)
|
|
46608
47131
|
).catch(() => {
|
|
@@ -46619,8 +47142,8 @@ async function openOrCreateRuntimeDirectory(parentHandle, targetPath) {
|
|
|
46619
47142
|
return handle;
|
|
46620
47143
|
}
|
|
46621
47144
|
async function writeExclusiveRuntimeFile(parentHandle, targetPath, contents, mode) {
|
|
46622
|
-
const descriptorPath = descriptorChildPath(parentHandle.fd,
|
|
46623
|
-
const handle = await
|
|
47145
|
+
const descriptorPath = descriptorChildPath(parentHandle.fd, path5.basename(targetPath), targetPath);
|
|
47146
|
+
const handle = await open2(
|
|
46624
47147
|
descriptorPath,
|
|
46625
47148
|
fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_NOFOLLOW,
|
|
46626
47149
|
mode
|
|
@@ -46642,24 +47165,24 @@ async function writeExclusiveRuntimeFile(parentHandle, targetPath, contents, mod
|
|
|
46642
47165
|
}
|
|
46643
47166
|
}
|
|
46644
47167
|
async function publishRuntimeFile(parentHandle, targetPath, contents, mode, boundary) {
|
|
46645
|
-
const temporaryPath =
|
|
47168
|
+
const temporaryPath = path5.join(path5.dirname(targetPath), `.${path5.basename(targetPath)}.tmp-${process.pid}-${randomBytes(8).toString("hex")}`);
|
|
46646
47169
|
await writeExclusiveRuntimeFile(parentHandle, temporaryPath, contents, mode);
|
|
46647
|
-
const temporaryDescriptor = descriptorChildPath(parentHandle.fd,
|
|
46648
|
-
const targetDescriptor = descriptorChildPath(parentHandle.fd,
|
|
47170
|
+
const temporaryDescriptor = descriptorChildPath(parentHandle.fd, path5.basename(temporaryPath), temporaryPath);
|
|
47171
|
+
const targetDescriptor = descriptorChildPath(parentHandle.fd, path5.basename(targetPath), targetPath);
|
|
46649
47172
|
try {
|
|
46650
47173
|
await pauseRuntimeTreePublication(boundary, targetPath);
|
|
46651
47174
|
const parentIdentity = await parentHandle.stat();
|
|
46652
|
-
await assertRuntimeDataPathIdentity(
|
|
47175
|
+
await assertRuntimeDataPathIdentity(path5.dirname(targetPath), { dev: parentIdentity.dev, ino: parentIdentity.ino }, true);
|
|
46653
47176
|
try {
|
|
46654
|
-
const existing = await
|
|
47177
|
+
const existing = await lstat2(targetPath);
|
|
46655
47178
|
if (!existing.isFile() || existing.isSymbolicLink()) throw runtimeDataTrustError(targetPath);
|
|
46656
47179
|
} catch (error) {
|
|
46657
47180
|
if (errorDetails(error).code !== "ENOENT") throw error;
|
|
46658
47181
|
}
|
|
46659
|
-
await
|
|
47182
|
+
await rename2(temporaryDescriptor, targetDescriptor).catch(() => {
|
|
46660
47183
|
throw runtimeDataTrustError(targetPath);
|
|
46661
47184
|
});
|
|
46662
|
-
const installed = await
|
|
47185
|
+
const installed = await open2(targetDescriptor, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW).catch(() => {
|
|
46663
47186
|
throw runtimeDataTrustError(targetPath);
|
|
46664
47187
|
});
|
|
46665
47188
|
try {
|
|
@@ -46670,7 +47193,7 @@ async function publishRuntimeFile(parentHandle, targetPath, contents, mode, boun
|
|
|
46670
47193
|
await installed.close();
|
|
46671
47194
|
}
|
|
46672
47195
|
} finally {
|
|
46673
|
-
await
|
|
47196
|
+
await rm2(temporaryDescriptor, { force: true }).catch(() => {
|
|
46674
47197
|
});
|
|
46675
47198
|
}
|
|
46676
47199
|
}
|
|
@@ -46692,13 +47215,13 @@ async function cleanupUnreferencedHostSealedEnvKeys(dataDirectory, referencedFin
|
|
|
46692
47215
|
try {
|
|
46693
47216
|
const keysIdentity = await keysHandle.stat();
|
|
46694
47217
|
const descriptorDirectory = process.platform === "linux" ? `/proc/self/fd/${keysHandle.fd}` : paths.keys;
|
|
46695
|
-
const entries = await
|
|
47218
|
+
const entries = await readdir3(descriptorDirectory);
|
|
46696
47219
|
for (const entry of entries) {
|
|
46697
47220
|
const match = /^([a-f0-9]{16})\.(private|public)\.pem$/.exec(entry);
|
|
46698
47221
|
if (!match || referencedFingerprints.has(match[1])) continue;
|
|
46699
|
-
const targetPath =
|
|
47222
|
+
const targetPath = path5.join(paths.keys, entry);
|
|
46700
47223
|
const descriptorPath = descriptorChildPath(keysHandle.fd, entry, targetPath);
|
|
46701
|
-
const retained = await
|
|
47224
|
+
const retained = await open2(descriptorPath, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW).catch(() => {
|
|
46702
47225
|
throw runtimeDataTrustError(targetPath);
|
|
46703
47226
|
});
|
|
46704
47227
|
try {
|
|
@@ -46707,7 +47230,7 @@ async function cleanupUnreferencedHostSealedEnvKeys(dataDirectory, referencedFin
|
|
|
46707
47230
|
await pauseRuntimeTreePublication("sealed-key-cleanup", targetPath);
|
|
46708
47231
|
await assertRuntimeDataPathIdentity(paths.keys, { dev: keysIdentity.dev, ino: keysIdentity.ino }, true);
|
|
46709
47232
|
await assertRuntimeDataPathIdentity(targetPath, { dev: details.dev, ino: details.ino }, false);
|
|
46710
|
-
await
|
|
47233
|
+
await rm2(descriptorPath, { force: true });
|
|
46711
47234
|
} finally {
|
|
46712
47235
|
await retained.close();
|
|
46713
47236
|
}
|
|
@@ -46754,7 +47277,7 @@ async function inspectHostSealedEnvKey(record, remoteRoot) {
|
|
|
46754
47277
|
if (typeof publicKeyFingerprint !== "string" || publicKeyFingerprint.length === 0) {
|
|
46755
47278
|
return null;
|
|
46756
47279
|
}
|
|
46757
|
-
const dataDirectory =
|
|
47280
|
+
const dataDirectory = path5.join(remoteRoot, "hosts", record.domain, "capsules", record.subname, "data");
|
|
46758
47281
|
const paths = hostSealedEnvKeyPaths(dataDirectory, publicKeyFingerprint);
|
|
46759
47282
|
const [publicKeyReadable, privateKeyReadable] = await Promise.all([
|
|
46760
47283
|
pathReadable(paths.publicKey),
|
|
@@ -46774,7 +47297,7 @@ async function inspectHostSealedEnvKey(record, remoteRoot) {
|
|
|
46774
47297
|
try {
|
|
46775
47298
|
return {
|
|
46776
47299
|
...base,
|
|
46777
|
-
publicKey: await
|
|
47300
|
+
publicKey: await readFile4(paths.publicKey, "utf8")
|
|
46778
47301
|
};
|
|
46779
47302
|
} catch (error) {
|
|
46780
47303
|
if (errorDetails(error).code === "ENOENT") {
|
|
@@ -46821,17 +47344,17 @@ function currentReleaseSealedServerEnvFingerprint(record) {
|
|
|
46821
47344
|
return typeof fingerprint === "string" ? fingerprint : null;
|
|
46822
47345
|
}
|
|
46823
47346
|
function hostSealedEnvKeyPaths(dataDirectory, fingerprint) {
|
|
46824
|
-
const root =
|
|
46825
|
-
const keys =
|
|
47347
|
+
const root = path5.join(dataDirectory, "sealed-server-env");
|
|
47348
|
+
const keys = path5.join(root, "keys");
|
|
46826
47349
|
return {
|
|
46827
47350
|
root,
|
|
46828
47351
|
keys,
|
|
46829
|
-
privateKey:
|
|
46830
|
-
publicKey:
|
|
47352
|
+
privateKey: path5.join(keys, `${fingerprint}.private.pem`),
|
|
47353
|
+
publicKey: path5.join(keys, `${fingerprint}.public.pem`)
|
|
46831
47354
|
};
|
|
46832
47355
|
}
|
|
46833
47356
|
function fingerprintPublicKey(publicKey) {
|
|
46834
|
-
return
|
|
47357
|
+
return createHash2("sha256").update(publicKey).digest("hex").slice(0, 16);
|
|
46835
47358
|
}
|
|
46836
47359
|
function reactivateRegistrationRecord(record, sealedServerEnv = null) {
|
|
46837
47360
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -46887,20 +47410,20 @@ function normaliseBootstrap(request) {
|
|
|
46887
47410
|
const provided = request.bootstrap ?? {};
|
|
46888
47411
|
const remoteRoot = request.host.remoteRoot;
|
|
46889
47412
|
const domain = request.host.domain;
|
|
46890
|
-
const caddyDirectory =
|
|
46891
|
-
const domainDirectory =
|
|
46892
|
-
const tlsDirectory =
|
|
47413
|
+
const caddyDirectory = path5.join(remoteRoot, "caddy");
|
|
47414
|
+
const domainDirectory = path5.join(remoteRoot, "hosts", domain);
|
|
47415
|
+
const tlsDirectory = path5.join(domainDirectory, "tls");
|
|
46893
47416
|
const expectedDirectories = {
|
|
46894
47417
|
remoteRoot,
|
|
46895
|
-
bin:
|
|
46896
|
-
incoming:
|
|
47418
|
+
bin: path5.join(remoteRoot, "bin"),
|
|
47419
|
+
incoming: path5.join(remoteRoot, "incoming"),
|
|
46897
47420
|
caddy: caddyDirectory,
|
|
46898
|
-
caddyHosts:
|
|
46899
|
-
hosts:
|
|
47421
|
+
caddyHosts: path5.join(caddyDirectory, "hosts"),
|
|
47422
|
+
hosts: path5.join(remoteRoot, "hosts"),
|
|
46900
47423
|
domain: domainDirectory,
|
|
46901
47424
|
tls: tlsDirectory,
|
|
46902
|
-
registry:
|
|
46903
|
-
capsules:
|
|
47425
|
+
registry: path5.join(domainDirectory, "registry"),
|
|
47426
|
+
capsules: path5.join(domainDirectory, "capsules")
|
|
46904
47427
|
};
|
|
46905
47428
|
for (const [name2, value] of Object.entries(provided.directories ?? {})) {
|
|
46906
47429
|
if (!(name2 in expectedDirectories) || value !== expectedDirectories[name2]) throw bootstrapPathError();
|
|
@@ -46910,8 +47433,8 @@ function normaliseBootstrap(request) {
|
|
|
46910
47433
|
const directories = {
|
|
46911
47434
|
...expectedDirectories
|
|
46912
47435
|
};
|
|
46913
|
-
const expectedCertificate =
|
|
46914
|
-
const expectedKey =
|
|
47436
|
+
const expectedCertificate = path5.join(directories.tls, "origin.crt");
|
|
47437
|
+
const expectedKey = path5.join(directories.tls, "origin.key");
|
|
46915
47438
|
if (provided.tls?.directory !== void 0 && provided.tls.directory !== directories.tls) throw bootstrapPathError();
|
|
46916
47439
|
if (provided.tls?.certificate != null && provided.tls.certificate !== expectedCertificate) throw bootstrapPathError();
|
|
46917
47440
|
if (provided.tls?.key != null && provided.tls.key !== expectedKey) throw bootstrapPathError();
|
|
@@ -46921,8 +47444,8 @@ function normaliseBootstrap(request) {
|
|
|
46921
47444
|
certificate: tlsMode === "cloudflare-origin" ? expectedCertificate : null,
|
|
46922
47445
|
key: tlsMode === "cloudflare-origin" ? expectedKey : null
|
|
46923
47446
|
};
|
|
46924
|
-
const expectedManagedInclude =
|
|
46925
|
-
const expectedDomainInclude =
|
|
47447
|
+
const expectedManagedInclude = path5.join(directories.caddy, "sporades-hosted-domains.caddy");
|
|
47448
|
+
const expectedDomainInclude = path5.join(directories.caddyHosts, `${domain}.caddy`);
|
|
46926
47449
|
const expectedAccessLog = defaultCaddyAccessLogPath(remoteRoot);
|
|
46927
47450
|
if (provided.caddy?.managedInclude !== void 0 && provided.caddy.managedInclude !== expectedManagedInclude) throw bootstrapPathError();
|
|
46928
47451
|
if (provided.caddy?.domainInclude !== void 0 && provided.caddy.domainInclude !== expectedDomainInclude) throw bootstrapPathError();
|
|
@@ -46937,11 +47460,11 @@ function normaliseBootstrap(request) {
|
|
|
46937
47460
|
tls,
|
|
46938
47461
|
network: provided.network ?? hostHelperConfig.hostedCapsule.dockerNetwork,
|
|
46939
47462
|
caddy: {
|
|
46940
|
-
caddyfile:
|
|
47463
|
+
caddyfile: path5.join(directories.caddy, "Caddyfile"),
|
|
46941
47464
|
managedInclude: expectedManagedInclude,
|
|
46942
47465
|
domainInclude: expectedDomainInclude,
|
|
46943
|
-
routesDirectory:
|
|
46944
|
-
healthRoute:
|
|
47466
|
+
routesDirectory: path5.join(directories.caddyHosts, domain),
|
|
47467
|
+
healthRoute: path5.join(directories.caddyHosts, domain, "host.caddy"),
|
|
46945
47468
|
accessLog: expectedAccessLog
|
|
46946
47469
|
}
|
|
46947
47470
|
};
|
|
@@ -46954,7 +47477,7 @@ function bootstrapPathError() {
|
|
|
46954
47477
|
}
|
|
46955
47478
|
function bootstrapTrustManifest(request) {
|
|
46956
47479
|
const bootstrap = normaliseBootstrap(request);
|
|
46957
|
-
const placeholder =
|
|
47480
|
+
const placeholder = path5.join(bootstrap.caddy.routesDirectory, ".sporades-placeholder.caddy");
|
|
46958
47481
|
const finalFiles = [
|
|
46959
47482
|
{ path: bootstrap.caddy.caddyfile },
|
|
46960
47483
|
{ path: bootstrap.caddy.managedInclude },
|
|
@@ -46969,8 +47492,8 @@ function bootstrapTrustManifest(request) {
|
|
|
46969
47492
|
return {
|
|
46970
47493
|
directories: [
|
|
46971
47494
|
...Object.values(bootstrap.directories).map((entry) => ({ path: entry })),
|
|
46972
|
-
{ path:
|
|
46973
|
-
{ path:
|
|
47495
|
+
{ path: path5.dirname(bootstrap.caddy.accessLog), caddyOwned: true },
|
|
47496
|
+
{ path: path5.join(bootstrap.directories.registry, "capsules") },
|
|
46974
47497
|
{ path: bootstrap.caddy.routesDirectory }
|
|
46975
47498
|
],
|
|
46976
47499
|
finalFiles
|
|
@@ -46982,24 +47505,24 @@ async function ensureBootstrapDirectories(bootstrap) {
|
|
|
46982
47505
|
bootstrap.directories.bin,
|
|
46983
47506
|
bootstrap.directories.incoming,
|
|
46984
47507
|
bootstrap.directories.caddy,
|
|
46985
|
-
|
|
47508
|
+
path5.dirname(bootstrap.caddy.accessLog),
|
|
46986
47509
|
bootstrap.directories.caddyHosts,
|
|
46987
47510
|
bootstrap.directories.hosts,
|
|
46988
47511
|
bootstrap.directories.domain,
|
|
46989
47512
|
bootstrap.directories.tls,
|
|
46990
47513
|
bootstrap.directories.registry,
|
|
46991
|
-
|
|
47514
|
+
path5.join(bootstrap.directories.registry, "capsules"),
|
|
46992
47515
|
bootstrap.directories.capsules,
|
|
46993
47516
|
bootstrap.caddy.routesDirectory
|
|
46994
47517
|
];
|
|
46995
47518
|
for (const directory of directories) {
|
|
46996
|
-
const details = await
|
|
47519
|
+
const details = await lstat2(directory);
|
|
46997
47520
|
if (!details.isDirectory() || details.isSymbolicLink()) throw routeTrustError();
|
|
46998
47521
|
}
|
|
46999
47522
|
}
|
|
47000
47523
|
async function provisionCaddyAccessLog(request, bootstrap) {
|
|
47001
47524
|
const logFile = bootstrap.caddy.accessLog;
|
|
47002
|
-
const logDirectory =
|
|
47525
|
+
const logDirectory = path5.dirname(logFile);
|
|
47003
47526
|
const caddyUser = resolveCaddyServiceUser();
|
|
47004
47527
|
if (!caddyUser) {
|
|
47005
47528
|
throw helperError(
|
|
@@ -47034,7 +47557,7 @@ async function provisionCaddyOwnedLogDescriptors(options) {
|
|
|
47034
47557
|
const fileIdentity = trust.finalFiles.find((entry) => entry.path === logFile && entry.caddyOwned);
|
|
47035
47558
|
if (!directoryIdentity || !fileIdentity) throw routeTrustError();
|
|
47036
47559
|
await assertManagedLogMutationBoundary(logFile);
|
|
47037
|
-
const directoryHandle = await
|
|
47560
|
+
const directoryHandle = await open2(
|
|
47038
47561
|
logDirectory,
|
|
47039
47562
|
fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW | (fsConstants.O_DIRECTORY ?? 0)
|
|
47040
47563
|
).catch(() => {
|
|
@@ -47043,17 +47566,17 @@ async function provisionCaddyOwnedLogDescriptors(options) {
|
|
|
47043
47566
|
try {
|
|
47044
47567
|
let directoryDetails = await directoryHandle.stat();
|
|
47045
47568
|
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}/${
|
|
47569
|
+
const descriptorFile = process.platform === "linux" ? `/proc/self/fd/${directoryHandle.fd}/${path5.basename(logFile)}` : logFile;
|
|
47047
47570
|
let fileHandle;
|
|
47048
47571
|
try {
|
|
47049
|
-
fileHandle = await
|
|
47572
|
+
fileHandle = await open2(
|
|
47050
47573
|
descriptorFile,
|
|
47051
47574
|
fsConstants.O_RDWR | fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_NOFOLLOW,
|
|
47052
47575
|
416
|
|
47053
47576
|
);
|
|
47054
47577
|
} catch (error) {
|
|
47055
47578
|
if (errorDetails(error).code !== "EEXIST") throw routeTrustError();
|
|
47056
|
-
fileHandle = await
|
|
47579
|
+
fileHandle = await open2(descriptorFile, fsConstants.O_RDWR | fsConstants.O_NOFOLLOW).catch(() => {
|
|
47057
47580
|
throw routeTrustError();
|
|
47058
47581
|
});
|
|
47059
47582
|
}
|
|
@@ -47085,10 +47608,10 @@ async function provisionCaddyOwnedLogDescriptors(options) {
|
|
|
47085
47608
|
directoryDetails = await directoryHandle.stat();
|
|
47086
47609
|
fileDetails = await fileHandle.stat();
|
|
47087
47610
|
}
|
|
47088
|
-
const directoryPathDetails = await
|
|
47611
|
+
const directoryPathDetails = await lstat2(logDirectory).catch(() => {
|
|
47089
47612
|
throw routeTrustError();
|
|
47090
47613
|
});
|
|
47091
|
-
const filePathDetails = await
|
|
47614
|
+
const filePathDetails = await lstat2(logFile).catch(() => {
|
|
47092
47615
|
throw routeTrustError();
|
|
47093
47616
|
});
|
|
47094
47617
|
if (!directoryPathDetails.isDirectory() || directoryPathDetails.isSymbolicLink() || !filePathDetails.isFile() || filePathDetails.isSymbolicLink() || directoryPathDetails.dev !== directoryDetails.dev || directoryPathDetails.ino !== directoryDetails.ino || filePathDetails.dev !== fileDetails.dev || filePathDetails.ino !== fileDetails.ino || !trustedExactOwnerMetadata(directoryDetails, owner) || !trustedExactOwnerMetadata(fileDetails, owner) || (Number(directoryDetails.mode) & 511) !== 488 || (Number(fileDetails.mode) & 511) !== 416) throw routeTrustError();
|
|
@@ -47120,7 +47643,7 @@ async function provisionRouteLogFile(route, mutationBoundary) {
|
|
|
47120
47643
|
if (!logFile) {
|
|
47121
47644
|
return;
|
|
47122
47645
|
}
|
|
47123
|
-
const logDirectory =
|
|
47646
|
+
const logDirectory = path5.dirname(logFile);
|
|
47124
47647
|
const caddyUser = resolveCaddyServiceUser() ?? {
|
|
47125
47648
|
uid: String(process.geteuid?.() ?? process.getuid?.() ?? 0),
|
|
47126
47649
|
gid: String(process.getegid?.() ?? process.getgid?.() ?? 0)
|
|
@@ -47188,15 +47711,15 @@ async function installCaddyBootstrapConfig(request, bootstrap) {
|
|
|
47188
47711
|
const caddyfile = bootstrap.caddy.caddyfile;
|
|
47189
47712
|
const managedInclude = bootstrap.caddy.managedInclude;
|
|
47190
47713
|
const domainInclude = bootstrap.caddy.domainInclude;
|
|
47191
|
-
const placeholderRoute =
|
|
47714
|
+
const placeholderRoute = path5.join(bootstrap.caddy.routesDirectory, ".sporades-placeholder.caddy");
|
|
47192
47715
|
await atomicPublishBootstrapFile(placeholderRoute, "# Sporades keeps this placeholder so Caddy route imports are valid before Capsules are registered.\n", "bootstrap-placeholder");
|
|
47193
47716
|
await atomicPublishBootstrapFile(bootstrap.caddy.healthRoute, renderHostHealthRoute(request.host.domain, bootstrap.tls), "bootstrap-health-route");
|
|
47194
47717
|
await writeManagedCaddyfile(caddyfile, `import ${managedInclude}`);
|
|
47195
47718
|
await atomicPublishBootstrapFile(managedInclude, `# Sporades-managed Hosted domain include list.
|
|
47196
|
-
import ${
|
|
47719
|
+
import ${path5.join(bootstrap.directories.caddyHosts, "*.caddy")}
|
|
47197
47720
|
`, "bootstrap-managed-include");
|
|
47198
47721
|
await atomicPublishBootstrapFile(domainInclude, `# Sporades-managed routes for ${request.host.domain}.
|
|
47199
|
-
import ${
|
|
47722
|
+
import ${path5.join(bootstrap.caddy.routesDirectory, "*.caddy")}
|
|
47200
47723
|
`, "bootstrap-domain-include");
|
|
47201
47724
|
await assertActiveManagedRouteTrust(null);
|
|
47202
47725
|
validateCaddyBootstrap(caddyfile);
|
|
@@ -47240,7 +47763,7 @@ ${end}
|
|
|
47240
47763
|
let existing = "";
|
|
47241
47764
|
await assertBootstrapMutationBoundary("bootstrap-caddyfile-read", [caddyfile]);
|
|
47242
47765
|
try {
|
|
47243
|
-
existing = await
|
|
47766
|
+
existing = await readFile4(caddyfile, "utf8");
|
|
47244
47767
|
} catch (error) {
|
|
47245
47768
|
if (errorDetails(error).code !== "ENOENT") {
|
|
47246
47769
|
throw error;
|
|
@@ -47263,11 +47786,11 @@ async function atomicPublishBootstrapFile(target, contents, boundary) {
|
|
|
47263
47786
|
await writeFile(temporary, contents, { flag: "wx", mode: 420 });
|
|
47264
47787
|
try {
|
|
47265
47788
|
await assertBootstrapMutationBoundary(`${boundary}-publish`, [target, temporary]);
|
|
47266
|
-
await
|
|
47789
|
+
await rename2(temporary, target);
|
|
47267
47790
|
await refreshTrustedBootstrapFinalFileIdentity(target);
|
|
47268
47791
|
} catch (error) {
|
|
47269
47792
|
await assertBootstrapMutationBoundary(`${boundary}-cleanup`, [target, temporary]);
|
|
47270
|
-
await
|
|
47793
|
+
await rm2(temporary, { force: true });
|
|
47271
47794
|
throw error;
|
|
47272
47795
|
}
|
|
47273
47796
|
}
|
|
@@ -47431,6 +47954,8 @@ async function dockerRunArgs(lifecycle, releaseId, runtimeProbe) {
|
|
|
47431
47954
|
"--env",
|
|
47432
47955
|
`SPORADES_PUBLIC_ORIGIN=${lifecycle.hostedUrl}`,
|
|
47433
47956
|
"--env",
|
|
47957
|
+
`SPORADES_PUBLIC_ALIASES=${JSON.stringify(validateAliasDomains(lifecycle.routes.running.aliasDomains).map((hostname) => `https://${hostname}`))}`,
|
|
47958
|
+
"--env",
|
|
47434
47959
|
`SPORADES_RELEASE_ID=${releaseId}`
|
|
47435
47960
|
);
|
|
47436
47961
|
args.push("--publish", `127.0.0.1::${lifecycle.routes.running.port ?? 4e3}`);
|
|
@@ -47509,7 +48034,7 @@ function ensureHostedBaseImage(lifecycle) {
|
|
|
47509
48034
|
if (pull.ok) {
|
|
47510
48035
|
return;
|
|
47511
48036
|
}
|
|
47512
|
-
const dockerfilePath =
|
|
48037
|
+
const dockerfilePath = path5.join(lifecycle.remoteRoot, "Dockerfile.base");
|
|
47513
48038
|
if (!pathExistsSync(dockerfilePath)) {
|
|
47514
48039
|
throw helperError(
|
|
47515
48040
|
"Unable to prepare the Sporades Base image.",
|
|
@@ -47599,7 +48124,7 @@ async function currentReleaseId(currentLink, request) {
|
|
|
47599
48124
|
}
|
|
47600
48125
|
throw error;
|
|
47601
48126
|
}
|
|
47602
|
-
return
|
|
48127
|
+
return path5.basename(target);
|
|
47603
48128
|
}
|
|
47604
48129
|
function loopbackRunningRoute(route, publishedPort) {
|
|
47605
48130
|
return {
|
|
@@ -47615,7 +48140,7 @@ async function refreshLoopbackRunningRoute(request, registryRecord, containerNam
|
|
|
47615
48140
|
return withManagedRouteLock(routeFile, async () => {
|
|
47616
48141
|
let currentRoute;
|
|
47617
48142
|
try {
|
|
47618
|
-
currentRoute = await
|
|
48143
|
+
currentRoute = await readFile4(routeFile, "utf8");
|
|
47619
48144
|
} catch (error) {
|
|
47620
48145
|
if (errorDetails(error).code === "ENOENT") {
|
|
47621
48146
|
return { ok: true, refreshed: false };
|
|
@@ -47642,6 +48167,17 @@ async function refreshLoopbackRunningRoute(request, registryRecord, containerNam
|
|
|
47642
48167
|
}
|
|
47643
48168
|
async function writeRunningRoute(lifecycle, route = lifecycle.routes.running) {
|
|
47644
48169
|
await provisionRouteLogFile(route, "capsule-running-http-log-descriptor-mutate");
|
|
48170
|
+
await applyManagedRoute(lifecycle, route.routeFile, routeVariants(route).map(renderRunningRoute).join("\n"));
|
|
48171
|
+
}
|
|
48172
|
+
function routeVariants(route) {
|
|
48173
|
+
return [route, ...validateAliasDomains(route.aliasDomains).map((hostname) => ({
|
|
48174
|
+
...route,
|
|
48175
|
+
hostname,
|
|
48176
|
+
aliasDomains: void 0,
|
|
48177
|
+
tls: { mode: "automatic" }
|
|
48178
|
+
}))];
|
|
48179
|
+
}
|
|
48180
|
+
function renderRunningRoute(route) {
|
|
47645
48181
|
const cloudflareOrigin = route.tls?.mode === "cloudflare-origin";
|
|
47646
48182
|
const proxyLine = [
|
|
47647
48183
|
`reverse_proxy ${route.upstream ?? `${route.containerName}:${route.port ?? 4e3}`} {`,
|
|
@@ -47654,11 +48190,7 @@ async function writeRunningRoute(lifecycle, route = lifecycle.routes.running) {
|
|
|
47654
48190
|
"respond @sporadesUntrustedCloudflareSource 403",
|
|
47655
48191
|
routeHandler
|
|
47656
48192
|
].join("\n ") : routeHandler;
|
|
47657
|
-
|
|
47658
|
-
lifecycle,
|
|
47659
|
-
route.routeFile,
|
|
47660
|
-
renderRoute(route, guardedHandler)
|
|
47661
|
-
);
|
|
48193
|
+
return renderRoute(route, guardedHandler);
|
|
47662
48194
|
}
|
|
47663
48195
|
function renderRunningRouteHandler(route, proxyLine) {
|
|
47664
48196
|
const probe = route.runtimeProbe;
|
|
@@ -47684,7 +48216,7 @@ async function writeUnavailableRoute(lifecycle) {
|
|
|
47684
48216
|
await applyManagedRoute(
|
|
47685
48217
|
lifecycle,
|
|
47686
48218
|
route.routeFile,
|
|
47687
|
-
renderRoute(
|
|
48219
|
+
routeVariants(route).map((variant) => renderRoute(variant, renderUnavailableRouteHandler(variant))).join("\n")
|
|
47688
48220
|
);
|
|
47689
48221
|
}
|
|
47690
48222
|
function renderUnavailableRouteHandler(route) {
|
|
@@ -47729,16 +48261,16 @@ async function applyManagedRouteLocked(lifecycle, routeFile, contents) {
|
|
|
47729
48261
|
const tempRouteFile = `${routeFile}.tmp`;
|
|
47730
48262
|
const previousRouteFile = `${routeFile}.previous-${process.pid}`;
|
|
47731
48263
|
await assertManagedRouteMutationBoundary(routeFile, "apply-remove-temp", [tempRouteFile, previousRouteFile]);
|
|
47732
|
-
await
|
|
48264
|
+
await rm2(tempRouteFile, { force: true });
|
|
47733
48265
|
await assertManagedRouteMutationBoundary(routeFile, "apply-remove-previous", [previousRouteFile]);
|
|
47734
|
-
await
|
|
48266
|
+
await rm2(previousRouteFile, { force: true });
|
|
47735
48267
|
await assertManagedRouteMutationBoundary(routeFile, "apply-write-temp", [tempRouteFile]);
|
|
47736
48268
|
await writeFile(tempRouteFile, contents, { flag: "wx", mode: 420 });
|
|
47737
48269
|
try {
|
|
47738
48270
|
validateCaddyRoute(tempRouteFile);
|
|
47739
48271
|
} catch (error) {
|
|
47740
48272
|
await assertManagedRouteMutationBoundary(routeFile, "apply-validation-cleanup", [tempRouteFile]);
|
|
47741
|
-
await
|
|
48273
|
+
await rm2(tempRouteFile, { force: true });
|
|
47742
48274
|
throw error;
|
|
47743
48275
|
}
|
|
47744
48276
|
const hadPreviousRoute = await pathExists(routeFile);
|
|
@@ -47746,20 +48278,20 @@ async function applyManagedRouteLocked(lifecycle, routeFile, contents) {
|
|
|
47746
48278
|
try {
|
|
47747
48279
|
if (hadPreviousRoute) {
|
|
47748
48280
|
await assertManagedRouteMutationBoundary(routeFile, "apply-move-current", [previousRouteFile]);
|
|
47749
|
-
await
|
|
48281
|
+
await rename2(routeFile, previousRouteFile);
|
|
47750
48282
|
previousRouteMoved = true;
|
|
47751
48283
|
}
|
|
47752
48284
|
await assertManagedRouteMutationBoundary(routeFile, "apply-publish-temp", [tempRouteFile, previousRouteFile]);
|
|
47753
|
-
await
|
|
48285
|
+
await rename2(tempRouteFile, routeFile);
|
|
47754
48286
|
reloadCaddy(lifecycle);
|
|
47755
48287
|
} catch (error) {
|
|
47756
48288
|
await assertManagedRouteMutationBoundary(routeFile, "apply-rollback-remove-temp", [tempRouteFile, previousRouteFile]);
|
|
47757
|
-
await
|
|
48289
|
+
await rm2(tempRouteFile, { force: true });
|
|
47758
48290
|
await assertManagedRouteMutationBoundary(routeFile, "apply-rollback-remove-current", [previousRouteFile]);
|
|
47759
|
-
await
|
|
48291
|
+
await rm2(routeFile, { force: true });
|
|
47760
48292
|
if (previousRouteMoved) {
|
|
47761
48293
|
await assertManagedRouteMutationBoundary(routeFile, "apply-rollback-restore", [previousRouteFile]);
|
|
47762
|
-
await
|
|
48294
|
+
await rename2(previousRouteFile, routeFile);
|
|
47763
48295
|
}
|
|
47764
48296
|
if (previousRouteMoved) {
|
|
47765
48297
|
try {
|
|
@@ -47774,23 +48306,23 @@ async function applyManagedRouteLocked(lifecycle, routeFile, contents) {
|
|
|
47774
48306
|
throw error;
|
|
47775
48307
|
}
|
|
47776
48308
|
await assertManagedRouteMutationBoundary(routeFile, "apply-finalize-previous", [previousRouteFile]);
|
|
47777
|
-
await
|
|
48309
|
+
await rm2(previousRouteFile, { force: true });
|
|
47778
48310
|
}
|
|
47779
48311
|
async function removeManagedRouteLocked(lifecycle, routeFile) {
|
|
47780
48312
|
const previousRouteFile = `${routeFile}.previous-${process.pid}`;
|
|
47781
48313
|
await assertManagedRouteMutationBoundary(routeFile, "remove-remove-previous", [previousRouteFile]);
|
|
47782
|
-
await
|
|
48314
|
+
await rm2(previousRouteFile, { force: true });
|
|
47783
48315
|
const hadRoute = await pathExists(routeFile);
|
|
47784
48316
|
if (!hadRoute) {
|
|
47785
48317
|
return { routeFile, removed: false };
|
|
47786
48318
|
}
|
|
47787
48319
|
await assertManagedRouteMutationBoundary(routeFile, "remove-move-current", [previousRouteFile]);
|
|
47788
|
-
await
|
|
48320
|
+
await rename2(routeFile, previousRouteFile);
|
|
47789
48321
|
try {
|
|
47790
48322
|
reloadCaddy(lifecycle);
|
|
47791
48323
|
} catch (error) {
|
|
47792
48324
|
await assertManagedRouteMutationBoundary(routeFile, "remove-rollback-restore", [previousRouteFile]);
|
|
47793
|
-
await
|
|
48325
|
+
await rename2(previousRouteFile, routeFile);
|
|
47794
48326
|
try {
|
|
47795
48327
|
reloadCaddy(lifecycle);
|
|
47796
48328
|
} catch {
|
|
@@ -47809,7 +48341,7 @@ async function removeManagedRouteLocked(lifecycle, routeFile) {
|
|
|
47809
48341
|
async function finalizeRemovedRouteLocked(route) {
|
|
47810
48342
|
if (route?.previousRouteFile) {
|
|
47811
48343
|
await assertManagedRouteMutationBoundary(route.routeFile, "remove-finalize-previous", [route.previousRouteFile]);
|
|
47812
|
-
await
|
|
48344
|
+
await rm2(route.previousRouteFile, { force: true });
|
|
47813
48345
|
}
|
|
47814
48346
|
}
|
|
47815
48347
|
async function restoreRemovedRouteLocked(lifecycle, route) {
|
|
@@ -47817,9 +48349,9 @@ async function restoreRemovedRouteLocked(lifecycle, route) {
|
|
|
47817
48349
|
return;
|
|
47818
48350
|
}
|
|
47819
48351
|
await assertManagedRouteMutationBoundary(route.routeFile, "restore-remove-current", [route.previousRouteFile]);
|
|
47820
|
-
await
|
|
48352
|
+
await rm2(route.routeFile, { force: true });
|
|
47821
48353
|
await assertManagedRouteMutationBoundary(route.routeFile, "restore-publish-previous", [route.previousRouteFile]);
|
|
47822
|
-
await
|
|
48354
|
+
await rename2(route.previousRouteFile, route.routeFile);
|
|
47823
48355
|
reloadCaddy(lifecycle);
|
|
47824
48356
|
}
|
|
47825
48357
|
async function assertManagedRouteMutationBoundary(routeFile, boundary, relatedFiles = []) {
|
|
@@ -47865,7 +48397,7 @@ async function processRetainsOsFlock(lockFile) {
|
|
|
47865
48397
|
for (const descriptorRoot of ["/proc/self/fd", "/dev/fd"]) {
|
|
47866
48398
|
let entries;
|
|
47867
48399
|
try {
|
|
47868
|
-
entries = await
|
|
48400
|
+
entries = await readdir3(descriptorRoot);
|
|
47869
48401
|
} catch {
|
|
47870
48402
|
continue;
|
|
47871
48403
|
}
|
|
@@ -47877,7 +48409,7 @@ async function processRetainsOsFlock(lockFile) {
|
|
|
47877
48409
|
for (const descriptor of descriptorNumbers) {
|
|
47878
48410
|
for (const descriptorRoot of ["/proc/self/fd", "/dev/fd"]) {
|
|
47879
48411
|
try {
|
|
47880
|
-
const descriptorStat = await stat(
|
|
48412
|
+
const descriptorStat = await stat(path5.join(descriptorRoot, descriptor));
|
|
47881
48413
|
if (!routeLockFileIdentityMatches(descriptorStat, expected)) continue;
|
|
47882
48414
|
if (process.platform === "linux") {
|
|
47883
48415
|
if (descriptorStat.dev !== expected.dev) continue;
|
|
@@ -47894,7 +48426,7 @@ async function processRetainsOsFlock(lockFile) {
|
|
|
47894
48426
|
async function linuxDescriptorOwnsFlock(descriptor, expected) {
|
|
47895
48427
|
let fdinfo;
|
|
47896
48428
|
try {
|
|
47897
|
-
fdinfo = await
|
|
48429
|
+
fdinfo = await readFile4(`/proc/self/fdinfo/${descriptor}`, "utf8");
|
|
47898
48430
|
} catch {
|
|
47899
48431
|
return false;
|
|
47900
48432
|
}
|
|
@@ -47952,7 +48484,7 @@ function managedRouteLockTimeoutMs() {
|
|
|
47952
48484
|
async function readManagedRouteProtocolOwner(protocolFile) {
|
|
47953
48485
|
let parsed;
|
|
47954
48486
|
try {
|
|
47955
|
-
parsed = JSON.parse(await
|
|
48487
|
+
parsed = JSON.parse(await readFile4(protocolFile, "utf8"));
|
|
47956
48488
|
} catch (error) {
|
|
47957
48489
|
if (errorDetails(error).code === "ENOENT") {
|
|
47958
48490
|
return { state: "absent", owner: null };
|
|
@@ -47981,15 +48513,15 @@ function managedRouteProtocolOwnerIsLive(owner) {
|
|
|
47981
48513
|
return currentIdentity === null || currentIdentity === owner.processIdentity;
|
|
47982
48514
|
}
|
|
47983
48515
|
async function cleanupManagedRouteProtocolArtifacts(lockFile) {
|
|
47984
|
-
const directory =
|
|
47985
|
-
const base =
|
|
48516
|
+
const directory = path5.dirname(lockFile);
|
|
48517
|
+
const base = path5.basename(lockFile);
|
|
47986
48518
|
const artifact = new RegExp(`^${escapeRegExp(base)}\\.(?:claim|stale|reclaim)-[a-f0-9]{32}$`);
|
|
47987
|
-
const entries = (await
|
|
48519
|
+
const entries = (await readdir3(directory)).filter((entry) => artifact.test(entry)).sort().slice(0, 100);
|
|
47988
48520
|
for (const entry of entries) {
|
|
47989
|
-
const artifactPath =
|
|
48521
|
+
const artifactPath = path5.join(directory, entry);
|
|
47990
48522
|
const owner = await readManagedRouteProtocolOwner(artifactPath);
|
|
47991
48523
|
if (owner.state === "owner" && !managedRouteProtocolOwnerIsLive(owner.owner)) {
|
|
47992
|
-
await
|
|
48524
|
+
await rm2(artifactPath, { force: true });
|
|
47993
48525
|
}
|
|
47994
48526
|
}
|
|
47995
48527
|
}
|
|
@@ -48004,7 +48536,7 @@ async function removePathIfPresent(targetPath, options = {}) {
|
|
|
48004
48536
|
if (!existed) {
|
|
48005
48537
|
return { path: targetPath, removed: false };
|
|
48006
48538
|
}
|
|
48007
|
-
await
|
|
48539
|
+
await rm2(targetPath, { recursive: Boolean(options.recursive), force: true });
|
|
48008
48540
|
return { path: targetPath, removed: true };
|
|
48009
48541
|
}
|
|
48010
48542
|
async function prepareWritableDataPath(targetPath) {
|
|
@@ -48019,11 +48551,11 @@ async function prepareWritableDataPath(targetPath) {
|
|
|
48019
48551
|
}
|
|
48020
48552
|
async function openCanonicalRuntimeDataDirectory(targetPath, createData) {
|
|
48021
48553
|
const managedRoot = activeManagedRouteTrust?.managedRoot;
|
|
48022
|
-
if (!managedRoot || !
|
|
48023
|
-
const relative =
|
|
48024
|
-
const components = relative.split(
|
|
48025
|
-
if (relative.startsWith("..") ||
|
|
48026
|
-
let handle = await
|
|
48554
|
+
if (!managedRoot || !path5.isAbsolute(targetPath) || path5.normalize(targetPath) !== targetPath) throw runtimeDataTrustError(targetPath);
|
|
48555
|
+
const relative = path5.relative(managedRoot, targetPath);
|
|
48556
|
+
const components = relative.split(path5.sep).filter(Boolean);
|
|
48557
|
+
if (relative.startsWith("..") || path5.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);
|
|
48558
|
+
let handle = await open2(
|
|
48027
48559
|
managedRoot,
|
|
48028
48560
|
fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW | (fsConstants.O_DIRECTORY ?? 0)
|
|
48029
48561
|
).catch(() => {
|
|
@@ -48032,14 +48564,14 @@ async function openCanonicalRuntimeDataDirectory(targetPath, createData) {
|
|
|
48032
48564
|
let logical = managedRoot;
|
|
48033
48565
|
try {
|
|
48034
48566
|
for (let index = 0; index < components.length; index += 1) {
|
|
48035
|
-
logical =
|
|
48567
|
+
logical = path5.join(logical, components[index]);
|
|
48036
48568
|
const descriptorPath = descriptorChildPath(handle.fd, components[index], logical);
|
|
48037
48569
|
if (createData && index === components.length - 1) {
|
|
48038
|
-
await
|
|
48570
|
+
await mkdir2(descriptorPath, { mode: 448 }).catch((error) => {
|
|
48039
48571
|
if (errorDetails(error).code !== "EEXIST") throw runtimeDataTrustError(logical);
|
|
48040
48572
|
});
|
|
48041
48573
|
}
|
|
48042
|
-
const child = await
|
|
48574
|
+
const child = await open2(
|
|
48043
48575
|
descriptorPath,
|
|
48044
48576
|
fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW | (fsConstants.O_DIRECTORY ?? 0)
|
|
48045
48577
|
).catch(() => {
|
|
@@ -48062,7 +48594,7 @@ async function openCanonicalRuntimeDataDirectory(targetPath, createData) {
|
|
|
48062
48594
|
}
|
|
48063
48595
|
}
|
|
48064
48596
|
function descriptorChildPath(parentFd, name2, fallbackPath) {
|
|
48065
|
-
if (name2.length === 0 || name2 === "." || name2 === ".." || name2.includes(
|
|
48597
|
+
if (name2.length === 0 || name2 === "." || name2 === ".." || name2.includes(path5.sep)) throw runtimeDataTrustError(fallbackPath);
|
|
48066
48598
|
return process.platform === "linux" ? `/proc/self/fd/${parentFd}/${name2}` : fallbackPath;
|
|
48067
48599
|
}
|
|
48068
48600
|
async function prepareWritableDataHandle(handle, targetPath, directory) {
|
|
@@ -48078,15 +48610,15 @@ async function prepareWritableDataHandle(handle, targetPath, directory) {
|
|
|
48078
48610
|
await assertRuntimeDataPathIdentity(targetPath, identity, directory);
|
|
48079
48611
|
if (!directory) return;
|
|
48080
48612
|
const descriptorDirectory = process.platform === "linux" ? `/proc/self/fd/${handle.fd}` : targetPath;
|
|
48081
|
-
const entries = await
|
|
48613
|
+
const entries = await readdir3(descriptorDirectory, { withFileTypes: true }).catch(() => {
|
|
48082
48614
|
throw runtimeDataTrustError(targetPath);
|
|
48083
48615
|
});
|
|
48084
48616
|
for (const entry of entries) {
|
|
48085
|
-
if (entry.name === "." || entry.name === ".." || entry.name.includes(
|
|
48086
|
-
const childPath =
|
|
48617
|
+
if (entry.name === "." || entry.name === ".." || entry.name.includes(path5.sep)) throw runtimeDataTrustError(targetPath);
|
|
48618
|
+
const childPath = path5.join(targetPath, entry.name);
|
|
48087
48619
|
const descriptorPath = descriptorChildPath(handle.fd, entry.name, childPath);
|
|
48088
48620
|
const flags = fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW | (entry.isDirectory() ? fsConstants.O_DIRECTORY ?? 0 : 0);
|
|
48089
|
-
const child = await
|
|
48621
|
+
const child = await open2(descriptorPath, flags).catch(() => {
|
|
48090
48622
|
throw runtimeDataTrustError(childPath);
|
|
48091
48623
|
});
|
|
48092
48624
|
try {
|
|
@@ -48097,7 +48629,7 @@ async function prepareWritableDataHandle(handle, targetPath, directory) {
|
|
|
48097
48629
|
}
|
|
48098
48630
|
}
|
|
48099
48631
|
async function assertRuntimeDataPathIdentity(targetPath, identity, directory) {
|
|
48100
|
-
const details = await
|
|
48632
|
+
const details = await lstat2(targetPath).catch(() => {
|
|
48101
48633
|
throw runtimeDataTrustError(targetPath);
|
|
48102
48634
|
});
|
|
48103
48635
|
if (details.isSymbolicLink() || details.dev !== identity.dev || details.ino !== identity.ino || (directory ? !details.isDirectory() : !details.isFile())) throw runtimeDataTrustError(targetPath);
|
|
@@ -48151,7 +48683,7 @@ function validateCaddyRoute(routeFile) {
|
|
|
48151
48683
|
}
|
|
48152
48684
|
}
|
|
48153
48685
|
function reloadCaddy(lifecycle) {
|
|
48154
|
-
const configPath =
|
|
48686
|
+
const configPath = path5.join(lifecycle.remoteRoot, "caddy", "Caddyfile");
|
|
48155
48687
|
const result = spawnSync2("caddy", ["reload", "--config", configPath, "--adapter", "caddyfile"], { encoding: "utf8" });
|
|
48156
48688
|
if (result.error || result.status !== 0) {
|
|
48157
48689
|
throw helperError(
|
|
@@ -48195,6 +48727,7 @@ async function recordReleaseUploaded(request, release, fileInventory) {
|
|
|
48195
48727
|
hostedUrl: release.hostedUrl ?? entry.source?.hostedUrl ?? null,
|
|
48196
48728
|
remoteCapsuleId: release.remoteCapsuleId ?? entry.source?.remoteCapsuleId ?? null,
|
|
48197
48729
|
files: Array.isArray(release.files) ? [...release.files] : [],
|
|
48730
|
+
deployFiles: resolveDeployFiles(release.deployFiles),
|
|
48198
48731
|
fileInventory: fileInventory.map((file) => ({ ...file })),
|
|
48199
48732
|
serverEnvIncluded: Boolean(release.serverEnvIncluded),
|
|
48200
48733
|
inspection: Array.isArray(release.inspection?.requiredInspectors) ? { requiredInspectors: [...release.inspection.requiredInspectors] } : void 0,
|
|
@@ -48467,12 +49000,12 @@ function releaseSealedServerEnvPrivateKeyMount(registryRecord, paths) {
|
|
|
48467
49000
|
const fingerprint = release?.source?.sealedServerEnv?.publicKeyFingerprint;
|
|
48468
49001
|
if (typeof fingerprint === "string" && /^[a-f0-9]{16}$/.test(fingerprint)) {
|
|
48469
49002
|
return {
|
|
48470
|
-
host:
|
|
49003
|
+
host: path5.join(paths.data, "sealed-server-env", "keys", `${fingerprint}.private.pem`),
|
|
48471
49004
|
fingerprint
|
|
48472
49005
|
};
|
|
48473
49006
|
}
|
|
48474
49007
|
return {
|
|
48475
|
-
host:
|
|
49008
|
+
host: path5.join(paths.data, "sealed-server-env", "server-env.private.pem"),
|
|
48476
49009
|
fingerprint: null
|
|
48477
49010
|
};
|
|
48478
49011
|
}
|
|
@@ -48484,7 +49017,7 @@ function releaseSshAuthorizedKeysMount(registryRecord, paths) {
|
|
|
48484
49017
|
return null;
|
|
48485
49018
|
}
|
|
48486
49019
|
return {
|
|
48487
|
-
host:
|
|
49020
|
+
host: path5.join(paths.currentLink, ".sporades", "ssh", "authorized_keys"),
|
|
48488
49021
|
container: "/run/sporades/ssh/authorized_keys",
|
|
48489
49022
|
mode: "ro",
|
|
48490
49023
|
optional: false
|
|
@@ -48596,7 +49129,7 @@ function compareReleasesNewestFirst(left, right) {
|
|
|
48596
49129
|
async function readRegistryRecordForCapsule(request, purpose) {
|
|
48597
49130
|
const registryRecordPath = registryPath(request);
|
|
48598
49131
|
try {
|
|
48599
|
-
return JSON.parse(await
|
|
49132
|
+
return JSON.parse(await readFile4(registryRecordPath, "utf8"));
|
|
48600
49133
|
} catch (error) {
|
|
48601
49134
|
if (errorDetails(error).code === "ENOENT") {
|
|
48602
49135
|
throw helperError(
|
|
@@ -48636,7 +49169,7 @@ function assertRegistryRecordMatchesRequest(request, record) {
|
|
|
48636
49169
|
async function mutateRegistryRecord(request, mutate) {
|
|
48637
49170
|
return withRegistryLock(request, async () => {
|
|
48638
49171
|
const registryRecordPath = registryPath(request);
|
|
48639
|
-
const record = JSON.parse(await
|
|
49172
|
+
const record = JSON.parse(await readFile4(registryRecordPath, "utf8"));
|
|
48640
49173
|
await writeRegistryRecordAtomic(registryRecordPath, mutate(record));
|
|
48641
49174
|
});
|
|
48642
49175
|
}
|
|
@@ -48654,9 +49187,9 @@ async function writeRegistryContentsAtomic(registryRecordPath, contents) {
|
|
|
48654
49187
|
"Check Host server disk permissions and free space, then retry the command."
|
|
48655
49188
|
);
|
|
48656
49189
|
}
|
|
48657
|
-
await
|
|
49190
|
+
await rename2(tempPath, registryRecordPath);
|
|
48658
49191
|
} catch (error) {
|
|
48659
|
-
await
|
|
49192
|
+
await rm2(tempPath, { force: true });
|
|
48660
49193
|
throw error;
|
|
48661
49194
|
}
|
|
48662
49195
|
}
|
|
@@ -48666,7 +49199,7 @@ async function withRegistryLock(request, fn) {
|
|
|
48666
49199
|
const startedAt = Date.now();
|
|
48667
49200
|
while (true) {
|
|
48668
49201
|
try {
|
|
48669
|
-
await
|
|
49202
|
+
await mkdir2(lockDir, { recursive: false });
|
|
48670
49203
|
break;
|
|
48671
49204
|
} catch (error) {
|
|
48672
49205
|
if (errorDetails(error).code !== "EEXIST") {
|
|
@@ -48684,11 +49217,11 @@ async function withRegistryLock(request, fn) {
|
|
|
48684
49217
|
try {
|
|
48685
49218
|
return await fn();
|
|
48686
49219
|
} finally {
|
|
48687
|
-
await
|
|
49220
|
+
await rm2(lockDir, { recursive: true, force: true });
|
|
48688
49221
|
}
|
|
48689
49222
|
}
|
|
48690
49223
|
function registryPath(request) {
|
|
48691
|
-
return
|
|
49224
|
+
return path5.join(
|
|
48692
49225
|
request.host.remoteRoot,
|
|
48693
49226
|
"hosts",
|
|
48694
49227
|
request.host.domain,
|
|
@@ -48698,7 +49231,7 @@ function registryPath(request) {
|
|
|
48698
49231
|
);
|
|
48699
49232
|
}
|
|
48700
49233
|
function registryLockPath(request) {
|
|
48701
|
-
return
|
|
49234
|
+
return path5.join(request.host.remoteRoot, "hosts", request.host.domain, "registry", ".lock");
|
|
48702
49235
|
}
|
|
48703
49236
|
function capsuleData(request, lifecycle) {
|
|
48704
49237
|
return {
|
|
@@ -48750,7 +49283,7 @@ async function readManagedCaddyAccessLog(logs) {
|
|
|
48750
49283
|
}
|
|
48751
49284
|
return null;
|
|
48752
49285
|
}
|
|
48753
|
-
const contents = await
|
|
49286
|
+
const contents = await readFile4(logs.file, "utf8");
|
|
48754
49287
|
return lastLogEntries(contents, logs.lines);
|
|
48755
49288
|
}
|
|
48756
49289
|
function readCaddyJournalLogs(logs) {
|
|
@@ -48800,14 +49333,14 @@ function createHostedContainerName(domain, subname) {
|
|
|
48800
49333
|
return `sporades-${domain.replace(/[^a-z0-9]+/gi, "-").replace(/^-+|-+$/g, "").toLowerCase()}-${subname}`;
|
|
48801
49334
|
}
|
|
48802
49335
|
function defaultCaddyAccessLogPath(remoteRoot) {
|
|
48803
|
-
return
|
|
49336
|
+
return path5.join(remoteRoot, "caddy", "logs", "access.log");
|
|
48804
49337
|
}
|
|
48805
49338
|
function defaultCapsuleHttpLogPath(remoteRoot, domain, subname) {
|
|
48806
|
-
return
|
|
49339
|
+
return path5.join(remoteRoot, "hosts", domain, "capsules", subname, "logs", "http.log");
|
|
48807
49340
|
}
|
|
48808
49341
|
function canonicalCapsuleHttpLogPath(request, validatedRemoteRoot = validateCanonicalHostRouteRoot(request)) {
|
|
48809
49342
|
canonicalManagedRouteFile(request, validatedRemoteRoot);
|
|
48810
|
-
const expectedDirectory =
|
|
49343
|
+
const expectedDirectory = path5.resolve(
|
|
48811
49344
|
validatedRemoteRoot,
|
|
48812
49345
|
"hosts",
|
|
48813
49346
|
request.host.domain,
|
|
@@ -48815,8 +49348,8 @@ function canonicalCapsuleHttpLogPath(request, validatedRemoteRoot = validateCano
|
|
|
48815
49348
|
request.capsule.subname,
|
|
48816
49349
|
"logs"
|
|
48817
49350
|
);
|
|
48818
|
-
const expected =
|
|
48819
|
-
if (
|
|
49351
|
+
const expected = path5.resolve(expectedDirectory, "http.log");
|
|
49352
|
+
if (path5.dirname(expected) !== expectedDirectory) throw invalidCapsuleHttpLogPathError();
|
|
48820
49353
|
const lifecycle = request.lifecycle;
|
|
48821
49354
|
const supplied = [
|
|
48822
49355
|
request.registration?.route?.log?.file,
|
|
@@ -48831,7 +49364,7 @@ function canonicalCapsuleHttpLogPath(request, validatedRemoteRoot = validateCano
|
|
|
48831
49364
|
function capsuleHttpLogTrustManifest(request, validatedRemoteRoot) {
|
|
48832
49365
|
const logFile = canonicalCapsuleHttpLogPath(request, validatedRemoteRoot);
|
|
48833
49366
|
return {
|
|
48834
|
-
directories: [{ path:
|
|
49367
|
+
directories: [{ path: path5.dirname(logFile), caddyOwned: true }],
|
|
48835
49368
|
finalFiles: [{ path: logFile, caddyOwned: true }]
|
|
48836
49369
|
};
|
|
48837
49370
|
}
|
|
@@ -48841,7 +49374,81 @@ function invalidCapsuleHttpLogPathError() {
|
|
|
48841
49374
|
"Use the canonical Capsule-scoped Host HTTP log path and retry the lifecycle command."
|
|
48842
49375
|
);
|
|
48843
49376
|
}
|
|
49377
|
+
function hostedPreservedFilesRoot(paths) {
|
|
49378
|
+
return path5.join(paths.capsule, "preserved-files");
|
|
49379
|
+
}
|
|
49380
|
+
async function prepareHostedRuntimeFileAccess(target, mode, failure) {
|
|
49381
|
+
const handle = await open2(target, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW | fsConstants.O_NONBLOCK);
|
|
49382
|
+
try {
|
|
49383
|
+
const identity = await handle.stat();
|
|
49384
|
+
if (!identity.isFile() || identity.nlink !== 1) throw helperError(failure.message, failure.hint);
|
|
49385
|
+
await prepareRuntimeDataOwnershipHandle(handle, target, identity);
|
|
49386
|
+
await handle.chmod(mode);
|
|
49387
|
+
} finally {
|
|
49388
|
+
await handle.close();
|
|
49389
|
+
}
|
|
49390
|
+
}
|
|
49391
|
+
async function preparePreservedReleaseFiles(request, recordedRelease) {
|
|
49392
|
+
const paths = canonicalReleasePaths(request);
|
|
49393
|
+
const journal = attemptJournalPath(hostedPreservedFilesRoot(paths));
|
|
49394
|
+
const interrupted = !activePreservedAttempts.has(journal) && await pathExists(journal);
|
|
49395
|
+
if (!recordedRelease) {
|
|
49396
|
+
if (interrupted) throw helperError("Current Hosted release is not recorded.", "Run `sporades host reconcile <subname>` to settle the interrupted release install before starting the Capsule.");
|
|
49397
|
+
return;
|
|
49398
|
+
}
|
|
49399
|
+
if (interrupted) {
|
|
49400
|
+
throw helperError("Interrupted deploy.files attempt requires recovery.", "Run `sporades host reconcile <subname>` to settle the interrupted release install before starting, restarting or selecting a release.");
|
|
49401
|
+
}
|
|
49402
|
+
for (const file of resolveDeployFiles(recordedRelease.source?.deployFiles)) {
|
|
49403
|
+
if (file.update !== "preserve") continue;
|
|
49404
|
+
const target = await assertPreservedDeployFile(hostedPreservedFilesRoot(paths), file.path);
|
|
49405
|
+
await prepareHostedRuntimeFileAccess(target, 384, { message: "Unsafe preserved release file.", hint: "Restore a regular preserved file before restarting." });
|
|
49406
|
+
}
|
|
49407
|
+
}
|
|
49408
|
+
async function reconcileReleaseAttempt(request) {
|
|
49409
|
+
validateLifecycleRequest(request);
|
|
49410
|
+
const record = await readRegistryRecordForCapsule(request, "reconcile");
|
|
49411
|
+
assertRegistryRecordMatchesRequest(request, record);
|
|
49412
|
+
const paths = canonicalReleasePaths(request);
|
|
49413
|
+
const journal = attemptJournalPath(hostedPreservedFilesRoot(paths));
|
|
49414
|
+
const attempt = await readPreservedFileAttempt(journal);
|
|
49415
|
+
const lifecycle = normaliseLifecycle(request, record, { ignoreProvidedLifecycle: true });
|
|
49416
|
+
const data2 = { capsule: capsuleData(request, lifecycle), journal, reconciled: false, committed: null, release: null, actions: [] };
|
|
49417
|
+
if (!attempt) {
|
|
49418
|
+
writeEnvelope({ ok: true, data: data2, error: null });
|
|
49419
|
+
return;
|
|
49420
|
+
}
|
|
49421
|
+
if (!/^\d{8}T\d{6}Z-[a-f0-9]{8}$/.test(attempt.release)) {
|
|
49422
|
+
throw helperError("Hosted deploy.files journal names an invalid release.", `Inspect ${journal} before reconciling manually.`);
|
|
49423
|
+
}
|
|
49424
|
+
const recorded = normaliseReleaseHistory(record).some((entry) => entry.id === attempt.release);
|
|
49425
|
+
const committed = recorded && record.currentRelease?.id === attempt.release;
|
|
49426
|
+
const actions = [];
|
|
49427
|
+
if (!recorded) {
|
|
49428
|
+
await rollbackPreservedFiles(attempt.seeds);
|
|
49429
|
+
if (attempt.seeds.length) actions.push("seeds-rolled-back");
|
|
49430
|
+
const recordedTarget = record.currentRelease?.id ? path5.join(paths.releases, record.currentRelease.id) : null;
|
|
49431
|
+
const currentTarget = await readlink(paths.currentLink).catch((error) => {
|
|
49432
|
+
if (errorDetails(error).code === "ENOENT") return null;
|
|
49433
|
+
throw error;
|
|
49434
|
+
});
|
|
49435
|
+
if (currentTarget !== recordedTarget) {
|
|
49436
|
+
await restoreCurrentReleasePointerTarget(paths.currentLink, recordedTarget);
|
|
49437
|
+
actions.push("current-pointer-restored");
|
|
49438
|
+
}
|
|
49439
|
+
const candidate = canonicalRollbackPaths(request, attempt.release);
|
|
49440
|
+
await removeReleasePrivateKeyIfPresent(candidate, attempt.release);
|
|
49441
|
+
if (await pathExists(candidate.release)) {
|
|
49442
|
+
await rm2(candidate.release, { recursive: true, force: true });
|
|
49443
|
+
actions.push("candidate-release-removed");
|
|
49444
|
+
}
|
|
49445
|
+
}
|
|
49446
|
+
await finishPreservedFileAttempt(journal);
|
|
49447
|
+
actions.push("journal-removed");
|
|
49448
|
+
writeEnvelope({ ok: true, data: { ...data2, reconciled: true, committed, release: attempt.release, actions }, error: null });
|
|
49449
|
+
}
|
|
48844
49450
|
async function assertRollbackReleaseFiles(request, releaseDirectory, recordedRelease = null) {
|
|
49451
|
+
await preparePreservedReleaseFiles(request, recordedRelease);
|
|
48845
49452
|
try {
|
|
48846
49453
|
const expected = await recordedReleaseFileClaims(releaseDirectory, recordedRelease);
|
|
48847
49454
|
const actual = await validateExtractedReleaseTree(releaseDirectory, expected);
|
|
@@ -48898,7 +49505,7 @@ async function recordedReleaseFileClaims(releaseDirectory, recordedRelease) {
|
|
|
48898
49505
|
const normalized = file.normalize("NFC");
|
|
48899
49506
|
if (canonical.has(normalized)) throw helperError("Hosted Capsule release inventory is invalid.", "Choose another recorded release or push a replacement.");
|
|
48900
49507
|
canonical.add(normalized);
|
|
48901
|
-
const stats = await
|
|
49508
|
+
const stats = await lstat2(path5.join(releaseDirectory, ...file.split("/")));
|
|
48902
49509
|
if (!stats.isFile() || stats.isSymbolicLink() || stats.nlink !== 1) {
|
|
48903
49510
|
throw helperError("Hosted Capsule release files are missing.", "Choose another complete immutable release.");
|
|
48904
49511
|
}
|
|
@@ -48909,13 +49516,13 @@ async function recordedReleaseFileClaims(releaseDirectory, recordedRelease) {
|
|
|
48909
49516
|
async function deriveReleaseFileClaims(root) {
|
|
48910
49517
|
const claims = [];
|
|
48911
49518
|
async function visit(directory, prefix = "") {
|
|
48912
|
-
for (const entry of await
|
|
49519
|
+
for (const entry of await readdir3(directory, { withFileTypes: true })) {
|
|
48913
49520
|
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
48914
49521
|
if (!safeRecordedReleasePath(relative)) {
|
|
48915
49522
|
throw helperError("Hosted Capsule release inventory is invalid.", "Choose another recorded release or push a replacement.");
|
|
48916
49523
|
}
|
|
48917
|
-
const entryPath =
|
|
48918
|
-
const stats = await
|
|
49524
|
+
const entryPath = path5.join(directory, entry.name);
|
|
49525
|
+
const stats = await lstat2(entryPath);
|
|
48919
49526
|
if (stats.isSymbolicLink()) {
|
|
48920
49527
|
throw helperError("Hosted Capsule release inventory is invalid.", "Choose another recorded release or push a replacement.");
|
|
48921
49528
|
}
|
|
@@ -48938,7 +49545,7 @@ function validRecordedReleaseIdentity(file) {
|
|
|
48938
49545
|
return file && safeRecordedReleasePath(file.path) && Number.isSafeInteger(file.size) && file.size >= 0 && typeof file.sha256 === "string" && /^[a-f0-9]{64}$/.test(file.sha256);
|
|
48939
49546
|
}
|
|
48940
49547
|
function safeRecordedReleasePath(file) {
|
|
48941
|
-
return file.length > 0 && !file.startsWith("/") && !file.includes("\\") && !file.includes("\0") &&
|
|
49548
|
+
return file.length > 0 && !file.startsWith("/") && !file.includes("\\") && !file.includes("\0") && path5.posix.normalize(file) === file && Buffer.byteLength(file, "utf8") <= HOST_RELEASE_ARCHIVE_LIMITS.pathBytes && file.split("/").every((segment) => segment && segment !== "." && segment !== "..");
|
|
48942
49549
|
}
|
|
48943
49550
|
async function verifyRegisteredCapsule(request, purpose = "push") {
|
|
48944
49551
|
const record = await readRegistryRecordForCapsule(request, purpose);
|