@askexenow/exe-os 0.9.411 → 0.9.412
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/delivery/catalog.json +29 -38
- package/deploy/compose/.env.customer.example +5 -5
- package/deploy/compose/.env.default +4 -4
- package/deploy/compose/.env.example +5 -5
- package/deploy/compose/docker-compose.yml +1 -1
- package/deploy/stack-manifests/v0.9.json +263 -2
- package/dist/.dist-verified.json +3 -3
- package/dist/bin/cli.js +3 -3
- package/dist/bin/registry-proxy.js +1 -1
- package/dist/bin/stack-update.js +39 -14
- package/dist/bin/verify-stack.js +1 -1
- package/dist/bin/vps-health-gate.js +1 -1
- package/dist/build-info.json +3 -3
- package/dist/build-manifest.json +46 -46
- package/dist/{chunk-WC44PNZU.js → chunk-5DLZJ3MY.js} +1 -1
- package/dist/{chunk-HWP3WCLC.js → chunk-OPG237ZQ.js} +154 -50
- package/dist/{chunk-WHUQ44F2.js → chunk-RIKFXHLE.js} +246 -30
- package/dist/{chunk-DRHTRVDT.js → chunk-VBYSAQJD.js} +1 -1
- package/dist/{chunk-54STF27U.js → chunk-W3ABSZ6G.js} +1 -1
- package/dist/hooks/manifest.json +1 -1
- package/dist/lib/registry-proxy.js +1 -1
- package/dist/{stack-generate-manifest-SU6OFFY7.js → stack-generate-manifest-MVJDTU4F.js} +11 -35
- package/dist/{stack-promote-MJF436JD.js → stack-promote-BMGEY6SQ.js} +2 -2
- package/dist/{stack-release-S37R57XC.js → stack-release-64UJ6Y4W.js} +15 -32
- package/dist/{stack-update-XXDRKGGR.js → stack-update-EBATTQPP.js} +3 -1
- package/package.json +1 -1
- package/release-notes.json +19 -23
- package/stack.release.json +5 -5
package/dist/bin/stack-update.js
CHANGED
|
@@ -28,10 +28,12 @@ import {
|
|
|
28
28
|
parseEnv,
|
|
29
29
|
parseRetainedContainerHealth,
|
|
30
30
|
patchEnv,
|
|
31
|
+
planLegacyWorkerMigration,
|
|
31
32
|
printDanglingVolumes,
|
|
32
33
|
printVolumeCleanupResult,
|
|
33
34
|
readCurrentStackVersion,
|
|
34
35
|
readRetainedComposeFiles,
|
|
36
|
+
readStagedComposeTemplate,
|
|
35
37
|
reconcileStackLockFromContainers,
|
|
36
38
|
removeDanglingVolumes,
|
|
37
39
|
reportStackManifestSource,
|
|
@@ -50,11 +52,11 @@ import {
|
|
|
50
52
|
verifyManifestImagesAvailable,
|
|
51
53
|
warnUnmanagedManifestServices,
|
|
52
54
|
withStackUpdateOperationLock
|
|
53
|
-
} from "../chunk-
|
|
55
|
+
} from "../chunk-RIKFXHLE.js";
|
|
54
56
|
import "../chunk-EC5H53YD.js";
|
|
55
57
|
import {
|
|
56
58
|
runVerifyStack
|
|
57
|
-
} from "../chunk-
|
|
59
|
+
} from "../chunk-OPG237ZQ.js";
|
|
58
60
|
import {
|
|
59
61
|
assertPreflightGate,
|
|
60
62
|
runPreflightOnManifest
|
|
@@ -66,7 +68,7 @@ import {
|
|
|
66
68
|
import {
|
|
67
69
|
logResult,
|
|
68
70
|
runHealthGate
|
|
69
|
-
} from "../chunk-
|
|
71
|
+
} from "../chunk-VBYSAQJD.js";
|
|
70
72
|
import "../chunk-L25OAFOU.js";
|
|
71
73
|
import "../chunk-RT57D7C4.js";
|
|
72
74
|
import "../chunk-FTZET5YX.js";
|
|
@@ -2325,6 +2327,9 @@ function reportPartialRun(result) {
|
|
|
2325
2327
|
}
|
|
2326
2328
|
async function main(args = process.argv.slice(2)) {
|
|
2327
2329
|
const opts = parseArgs(args);
|
|
2330
|
+
if (opts.rollback && (opts.check || opts.dryRun || opts.verifyImages)) {
|
|
2331
|
+
throw new Error("Rollback cannot be combined with --check, --dry-run or --verify-images; no rollback was performed.");
|
|
2332
|
+
}
|
|
2328
2333
|
if (opts.retainCompose && (!opts.targetVersion || !opts.serviceName || !opts.noBootstrap || !opts.skipHardening || opts.configOnly || opts.rollback || opts.resume || opts.addService || opts.removeService || opts.services?.length || opts.cleanupVolumes || opts.reconcileLock || opts.adoptDeployedPins || opts.allowRed || opts.allowUnmanifestedServices || opts.breakGlassReason || opts.strictDigest === false || !opts.verifyManifestSignature)) {
|
|
2329
2334
|
throw new Error("--retain-compose requires explicit --target, one --service, --no-bootstrap, --skip-hardening, and no selection/config/rollback/cleanup/bypass mode");
|
|
2330
2335
|
}
|
|
@@ -2401,10 +2406,10 @@ async function main(args = process.argv.slice(2)) {
|
|
|
2401
2406
|
return;
|
|
2402
2407
|
}
|
|
2403
2408
|
let usingPackagedCheckTemplates = false;
|
|
2404
|
-
if (opts.check && !opts.noBootstrap && !existsForCli(opts.composeFile) && !existsForCli(opts.envFile)) {
|
|
2405
|
-
const
|
|
2406
|
-
opts.composeFile =
|
|
2407
|
-
opts.envFile = path3.join(
|
|
2409
|
+
if ((opts.check || opts.dryRun || opts.verifyImages || !opts.yes) && !opts.noBootstrap && !existsForCli(opts.composeFile) && !existsForCli(opts.envFile)) {
|
|
2410
|
+
const packaged = readStagedComposeTemplate(opts, "");
|
|
2411
|
+
opts.composeFile = packaged.templatePath;
|
|
2412
|
+
opts.envFile = path3.join(path3.dirname(packaged.templatePath), ".env.customer.example");
|
|
2408
2413
|
opts.noBootstrap = true;
|
|
2409
2414
|
usingPackagedCheckTemplates = true;
|
|
2410
2415
|
}
|
|
@@ -2418,7 +2423,7 @@ async function main(args = process.argv.slice(2)) {
|
|
|
2418
2423
|
return;
|
|
2419
2424
|
}
|
|
2420
2425
|
let hostReport;
|
|
2421
|
-
if (!opts.noBootstrap) {
|
|
2426
|
+
if (!opts.noBootstrap && opts.yes && !opts.check && !opts.dryRun && !opts.verifyImages) {
|
|
2422
2427
|
hostReport = bootstrapStackHost({ ...opts, installDocker: opts.yes, domain: opts.domain, adminEmail: opts.adminEmail });
|
|
2423
2428
|
printHostReport(hostReport);
|
|
2424
2429
|
if (!opts.check && !opts.dryRun) assertHostReadyForApply(hostReport);
|
|
@@ -2440,6 +2445,7 @@ async function main(args = process.argv.slice(2)) {
|
|
|
2440
2445
|
);
|
|
2441
2446
|
reportStackManifestSource(manifestLoad);
|
|
2442
2447
|
const manifest = manifestLoad.manifest;
|
|
2448
|
+
let previewServiceSelection;
|
|
2443
2449
|
if (opts.addService || opts.removeService) {
|
|
2444
2450
|
const effectiveVersion = opts.targetVersion ?? manifest.latest;
|
|
2445
2451
|
const manifestRelease = manifest.stacks[effectiveVersion];
|
|
@@ -2463,15 +2469,20 @@ async function main(args = process.argv.slice(2)) {
|
|
|
2463
2469
|
currentServices.delete(opts.removeService);
|
|
2464
2470
|
console.log(`Removed service: ${opts.removeService}`);
|
|
2465
2471
|
}
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2472
|
+
if (opts.check || opts.dryRun || opts.verifyImages) {
|
|
2473
|
+
previewServiceSelection = runScopedServiceSelection([...currentServices]);
|
|
2474
|
+
console.log(`Planned service selection (not saved): ${[...currentServices].join(", ")}`);
|
|
2475
|
+
} else {
|
|
2476
|
+
saveServiceSelection(opts.envFile, [...currentServices]);
|
|
2477
|
+
console.log(`Saved service selection: ${[...currentServices].join(", ")}`);
|
|
2478
|
+
}
|
|
2479
|
+
if (!opts.yes && !opts.check && !opts.dryRun && !opts.verifyImages) {
|
|
2469
2480
|
console.log("Service selection updated. Re-run with --yes to apply.");
|
|
2470
2481
|
return;
|
|
2471
2482
|
}
|
|
2472
2483
|
}
|
|
2473
2484
|
const isRunScoped = Boolean(opts.services && opts.services.length > 0);
|
|
2474
|
-
const serviceSelection = isRunScoped ? runScopedServiceSelection(opts.services) : loadServiceSelection(opts.envFile);
|
|
2485
|
+
const serviceSelection = isRunScoped ? runScopedServiceSelection(opts.services) : previewServiceSelection ?? loadServiceSelection(opts.envFile);
|
|
2475
2486
|
if (isRunScoped) {
|
|
2476
2487
|
console.log(
|
|
2477
2488
|
`Service scope for THIS RUN ONLY: ${opts.services.join(", ")} (the managed set in ${serviceSelectionPath(opts.envFile)} is unchanged)`
|
|
@@ -2533,7 +2544,22 @@ async function main(args = process.argv.slice(2)) {
|
|
|
2533
2544
|
}
|
|
2534
2545
|
assertDeploymentScopeAllowed(plan, opts.deploymentPersona);
|
|
2535
2546
|
const plannedEnvRaw = patchEnv(envRaw, Object.fromEntries(plan.changes.map((c) => [c.key, c.after])));
|
|
2536
|
-
const
|
|
2547
|
+
const currentComposeRaw = readFileSync3(opts.composeFile, "utf8");
|
|
2548
|
+
const hostState = listHostComposeServices(composeArgsForStack(opts.composeFile, opts.envFile));
|
|
2549
|
+
const overrideFile = path3.join(path3.dirname(opts.composeFile), "docker-compose.override.yml");
|
|
2550
|
+
const workerMigration = opts.retainCompose ? { raw: currentComposeRaw, changed: false } : planLegacyWorkerMigration({
|
|
2551
|
+
composeRaw: currentComposeRaw,
|
|
2552
|
+
targetComposeRaw: () => {
|
|
2553
|
+
const staged = readStagedComposeTemplate(opts, currentComposeRaw);
|
|
2554
|
+
return staged.templateFound ? staged.raw : null;
|
|
2555
|
+
},
|
|
2556
|
+
env: new Map(["COMPOSE_PROFILES", "EXE_OS_WORKER_IMAGE_TAG"].map((key) => [key, readKeyFromEnvFile(opts.envFile, key) ?? ""])),
|
|
2557
|
+
hostServices: hostState.services,
|
|
2558
|
+
hostServicesUnavailable: hostState.unavailable,
|
|
2559
|
+
overrideRaw: existsSync3(overrideFile) ? readFileSync3(overrideFile, "utf8") : null
|
|
2560
|
+
});
|
|
2561
|
+
const composeRawForGate = workerMigration.raw;
|
|
2562
|
+
if (workerMigration.changed) console.log("Planned configuration migration: clear the unused legacy embeddings-worker image default (no image will be deployed).");
|
|
2537
2563
|
assertProductionDeployGate(plan, plannedEnvRaw, composeRawForGate, {
|
|
2538
2564
|
breakGlassReason: opts.breakGlassReason,
|
|
2539
2565
|
breakGlassAuditFile: opts.breakGlassAuditFile,
|
|
@@ -2541,7 +2567,6 @@ async function main(args = process.argv.slice(2)) {
|
|
|
2541
2567
|
envFile: opts.envFile
|
|
2542
2568
|
});
|
|
2543
2569
|
{
|
|
2544
|
-
const hostState = listHostComposeServices(composeArgsForStack(opts.composeFile, opts.envFile));
|
|
2545
2570
|
assertSignedManifestDeliveryGate({
|
|
2546
2571
|
release: manifest.stacks[plan.targetVersion],
|
|
2547
2572
|
targetVersion: plan.targetVersion,
|
package/dist/bin/verify-stack.js
CHANGED
package/dist/build-info.json
CHANGED
package/dist/build-manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 2,
|
|
3
|
-
"generated_at": "2026-10-
|
|
3
|
+
"generated_at": "2026-10-14T20:24:44.903Z",
|
|
4
4
|
"files": [
|
|
5
5
|
{
|
|
6
6
|
"path": "active-agent-3YCLS7GB.js",
|
|
@@ -179,7 +179,7 @@
|
|
|
179
179
|
},
|
|
180
180
|
{
|
|
181
181
|
"path": "bin/cli.js",
|
|
182
|
-
"sha256": "
|
|
182
|
+
"sha256": "ecc4054b893c658689be2664e4106425497519d98d1b8ab220f05a87a88d21a0",
|
|
183
183
|
"bytes": 69826
|
|
184
184
|
},
|
|
185
185
|
{
|
|
@@ -529,7 +529,7 @@
|
|
|
529
529
|
},
|
|
530
530
|
{
|
|
531
531
|
"path": "bin/registry-proxy.js",
|
|
532
|
-
"sha256": "
|
|
532
|
+
"sha256": "6246f910ac5dc7ee8e424276768ce69834acc34cb1ded671b67e14402449aca9",
|
|
533
533
|
"bytes": 1885
|
|
534
534
|
},
|
|
535
535
|
{
|
|
@@ -554,8 +554,8 @@
|
|
|
554
554
|
},
|
|
555
555
|
{
|
|
556
556
|
"path": "bin/stack-update.js",
|
|
557
|
-
"sha256": "
|
|
558
|
-
"bytes":
|
|
557
|
+
"sha256": "d99802bc590930932894066d40e57bdd844e200a07f4f09834795f9dcb623bcc",
|
|
558
|
+
"bytes": 135594
|
|
559
559
|
},
|
|
560
560
|
{
|
|
561
561
|
"path": "bin/update.js",
|
|
@@ -564,7 +564,7 @@
|
|
|
564
564
|
},
|
|
565
565
|
{
|
|
566
566
|
"path": "bin/verify-stack.js",
|
|
567
|
-
"sha256": "
|
|
567
|
+
"sha256": "eb579bed1ecc499124d9b64ee92f12e39402faa459370d48bb8e1468c123e11b",
|
|
568
568
|
"bytes": 588
|
|
569
569
|
},
|
|
570
570
|
{
|
|
@@ -574,7 +574,7 @@
|
|
|
574
574
|
},
|
|
575
575
|
{
|
|
576
576
|
"path": "bin/vps-health-gate.js",
|
|
577
|
-
"sha256": "
|
|
577
|
+
"sha256": "c94514d9a30b1996dfd90b7f55ef7472e8b3fe02106306e39bcb985504d61329",
|
|
578
578
|
"bytes": 333
|
|
579
579
|
},
|
|
580
580
|
{
|
|
@@ -624,7 +624,7 @@
|
|
|
624
624
|
},
|
|
625
625
|
{
|
|
626
626
|
"path": "build-info.json",
|
|
627
|
-
"sha256": "
|
|
627
|
+
"sha256": "020a97e2fcb7cbf2bd9dc5ca75e8a598377e057ddfb4629c20d79bca406dfbbf",
|
|
628
628
|
"bytes": 96
|
|
629
629
|
},
|
|
630
630
|
{
|
|
@@ -787,11 +787,6 @@
|
|
|
787
787
|
"sha256": "29a6acbbaa627ae4d7852dd16a33f341809f3d82940b40c67c35d39d99fdf2c4",
|
|
788
788
|
"bytes": 1090
|
|
789
789
|
},
|
|
790
|
-
{
|
|
791
|
-
"path": "chunk-54STF27U.js",
|
|
792
|
-
"sha256": "6247938bc9df3b4c7c59de0cd0e8524952728545a5e28c22e3b53723966beec6",
|
|
793
|
-
"bytes": 44776
|
|
794
|
-
},
|
|
795
790
|
{
|
|
796
791
|
"path": "chunk-557C2IGL.js",
|
|
797
792
|
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
@@ -802,6 +797,11 @@
|
|
|
802
797
|
"sha256": "be62387389c3e7572937bbb4dd16d7b1b7d8412519da9e5184970e0e2903dc41",
|
|
803
798
|
"bytes": 22775
|
|
804
799
|
},
|
|
800
|
+
{
|
|
801
|
+
"path": "chunk-5DLZJ3MY.js",
|
|
802
|
+
"sha256": "92f2a77f33866d1a5948b5977783676a45820ba6b44908b873776912e0001380",
|
|
803
|
+
"bytes": 3766
|
|
804
|
+
},
|
|
805
805
|
{
|
|
806
806
|
"path": "chunk-5DMAMQNU.js",
|
|
807
807
|
"sha256": "19a9706b415bb25b177a37e1192d9df7d2cfc60cf3c0d997fc5d87c5001e59b1",
|
|
@@ -1132,11 +1132,6 @@
|
|
|
1132
1132
|
"sha256": "72d6557a872c40e78d45a4e3adc793ac24d437e567b133b701f04203aea5a859",
|
|
1133
1133
|
"bytes": 12949
|
|
1134
1134
|
},
|
|
1135
|
-
{
|
|
1136
|
-
"path": "chunk-DRHTRVDT.js",
|
|
1137
|
-
"sha256": "d3452468dcb0a27330ff6807ccbad6f957a6427a099d9bd2a867743d34b10258",
|
|
1138
|
-
"bytes": 7587
|
|
1139
|
-
},
|
|
1140
1135
|
{
|
|
1141
1136
|
"path": "chunk-DRTJCG6B.js",
|
|
1142
1137
|
"sha256": "cf3ce4cf6c9cdba17a8a5e3d9f0c78ae42f1cb72f1a06608cc8da1fb27f06a76",
|
|
@@ -1342,11 +1337,6 @@
|
|
|
1342
1337
|
"sha256": "a73bf84b9ddbd1b17f015dfb536fa2c87a05759bd7f8a33ec99bf37435ecdcdf",
|
|
1343
1338
|
"bytes": 37638
|
|
1344
1339
|
},
|
|
1345
|
-
{
|
|
1346
|
-
"path": "chunk-HWP3WCLC.js",
|
|
1347
|
-
"sha256": "c6bd6a6824ccad9848e5dd323301b8bfe09222bc4f02516f220588bf5e359447",
|
|
1348
|
-
"bytes": 39986
|
|
1349
|
-
},
|
|
1350
1340
|
{
|
|
1351
1341
|
"path": "chunk-IC2AONBH.js",
|
|
1352
1342
|
"sha256": "a36b4578fc5a22358234f1268894f828ba992cc68300413d4c2ac09d5b6a78e5",
|
|
@@ -1642,6 +1632,11 @@
|
|
|
1642
1632
|
"sha256": "49c15064d81c686c9f3e36c08b8f49c41380ee87ed91a2c87b5280584ba61f45",
|
|
1643
1633
|
"bytes": 15398
|
|
1644
1634
|
},
|
|
1635
|
+
{
|
|
1636
|
+
"path": "chunk-OPG237ZQ.js",
|
|
1637
|
+
"sha256": "bb7a843a54a6f1593d03b51729042108856b25e48fa7e555e234cb89f3deaee7",
|
|
1638
|
+
"bytes": 45241
|
|
1639
|
+
},
|
|
1645
1640
|
{
|
|
1646
1641
|
"path": "chunk-ORCCI2VV.js",
|
|
1647
1642
|
"sha256": "a37bac62b7a21c982e0b3452ecedc0d6701215fece2336e45834cf1ba7f6fc03",
|
|
@@ -1777,6 +1772,11 @@
|
|
|
1777
1772
|
"sha256": "c20bef017d05b8cc9c956d0a506eff0c31b14c3c1befdacf31e7f4fa008fbbdb",
|
|
1778
1773
|
"bytes": 1549
|
|
1779
1774
|
},
|
|
1775
|
+
{
|
|
1776
|
+
"path": "chunk-RIKFXHLE.js",
|
|
1777
|
+
"sha256": "ec22ba327e38c90d0f3bf3211f2b789a5addb0e378a0a0112ee36457540a09dd",
|
|
1778
|
+
"bytes": 286000
|
|
1779
|
+
},
|
|
1780
1780
|
{
|
|
1781
1781
|
"path": "chunk-RJ67XAJU.js",
|
|
1782
1782
|
"sha256": "48fb50b39022099eec6ec9ab7183d5d4e67b7f777d2eb3c2db1ea75af89c404a",
|
|
@@ -1942,6 +1942,11 @@
|
|
|
1942
1942
|
"sha256": "2177b5e9109243a74fbaf9d67367db6a7b029b9aa7347cc232e3e354ad6c8768",
|
|
1943
1943
|
"bytes": 7289
|
|
1944
1944
|
},
|
|
1945
|
+
{
|
|
1946
|
+
"path": "chunk-VBYSAQJD.js",
|
|
1947
|
+
"sha256": "dd5853a20a5092445dc86cc33fdc0e8f45777980fd98dd56c8fbea4d0a7e84a1",
|
|
1948
|
+
"bytes": 7587
|
|
1949
|
+
},
|
|
1945
1950
|
{
|
|
1946
1951
|
"path": "chunk-VGMACSEV.js",
|
|
1947
1952
|
"sha256": "4a51d1fb50f1389160ec2d06ebeb702976a647212f7731ab58f562de3f53f215",
|
|
@@ -1967,6 +1972,11 @@
|
|
|
1967
1972
|
"sha256": "5b4f1603c33ffbae21010d78cca899592e9f46576fa96be79fa044354506b221",
|
|
1968
1973
|
"bytes": 19195
|
|
1969
1974
|
},
|
|
1975
|
+
{
|
|
1976
|
+
"path": "chunk-W3ABSZ6G.js",
|
|
1977
|
+
"sha256": "6380ccbf98ed850ed45daff314a6d85607e246ab333502d969e1d885e2da7b11",
|
|
1978
|
+
"bytes": 44776
|
|
1979
|
+
},
|
|
1970
1980
|
{
|
|
1971
1981
|
"path": "chunk-W4H2MZAS.js",
|
|
1972
1982
|
"sha256": "cce67b1fec0b0c19a2099d1d8ae4f10a1097f59d70317570efa85d04400ed6f5",
|
|
@@ -1982,21 +1992,11 @@
|
|
|
1982
1992
|
"sha256": "1b9948f2debbef6df967904c57797a46739dfbffe17493f3e5786d6e05d71701",
|
|
1983
1993
|
"bytes": 50904
|
|
1984
1994
|
},
|
|
1985
|
-
{
|
|
1986
|
-
"path": "chunk-WC44PNZU.js",
|
|
1987
|
-
"sha256": "ddaee681898a3a9899fb8344b85f05f47fe2b2d7b8f48308e494b63e042c5052",
|
|
1988
|
-
"bytes": 3766
|
|
1989
|
-
},
|
|
1990
1995
|
{
|
|
1991
1996
|
"path": "chunk-WCGNCH3Q.js",
|
|
1992
1997
|
"sha256": "4a905397d309c73ed4ad5d1ec488309541be3eec2fc6021d275f433ca92cc7a9",
|
|
1993
1998
|
"bytes": 2265
|
|
1994
1999
|
},
|
|
1995
|
-
{
|
|
1996
|
-
"path": "chunk-WHUQ44F2.js",
|
|
1997
|
-
"sha256": "25b05355f18e18f5f5c80dc465fce50869af66825e6ca7be14ed595d9926ffd6",
|
|
1998
|
-
"bytes": 275037
|
|
1999
|
-
},
|
|
2000
2000
|
{
|
|
2001
2001
|
"path": "chunk-WJWRQQMS.js",
|
|
2002
2002
|
"sha256": "288c40cefdb07850759e0d9bb48d5fd7b3e8bdff9e128b44780c626b0e0ef2a9",
|
|
@@ -2604,7 +2604,7 @@
|
|
|
2604
2604
|
},
|
|
2605
2605
|
{
|
|
2606
2606
|
"path": "hooks/manifest.json",
|
|
2607
|
-
"sha256": "
|
|
2607
|
+
"sha256": "cb346441423b072873c35a530d1b78f8a4c2a9894838113efb272b070eca2493",
|
|
2608
2608
|
"bytes": 1841
|
|
2609
2609
|
},
|
|
2610
2610
|
{
|
|
@@ -2919,7 +2919,7 @@
|
|
|
2919
2919
|
},
|
|
2920
2920
|
{
|
|
2921
2921
|
"path": "lib/registry-proxy.js",
|
|
2922
|
-
"sha256": "
|
|
2922
|
+
"sha256": "8c19b7462382a7b98887843ce2bb5f19b1b6e236819c95841f4b26ad4924d460",
|
|
2923
2923
|
"bytes": 749
|
|
2924
2924
|
},
|
|
2925
2925
|
{
|
|
@@ -3513,9 +3513,9 @@
|
|
|
3513
3513
|
"bytes": 111
|
|
3514
3514
|
},
|
|
3515
3515
|
{
|
|
3516
|
-
"path": "stack-generate-manifest-
|
|
3517
|
-
"sha256": "
|
|
3518
|
-
"bytes":
|
|
3516
|
+
"path": "stack-generate-manifest-MVJDTU4F.js",
|
|
3517
|
+
"sha256": "08e90e042fb2c3a3891b667d0e5edd757ae308ee56835c70b502b2a2bd8973a5",
|
|
3518
|
+
"bytes": 54817
|
|
3519
3519
|
},
|
|
3520
3520
|
{
|
|
3521
3521
|
"path": "stack-init-5VKL5PL5.js",
|
|
@@ -3523,19 +3523,19 @@
|
|
|
3523
3523
|
"bytes": 4761
|
|
3524
3524
|
},
|
|
3525
3525
|
{
|
|
3526
|
-
"path": "stack-promote-
|
|
3527
|
-
"sha256": "
|
|
3526
|
+
"path": "stack-promote-BMGEY6SQ.js",
|
|
3527
|
+
"sha256": "77f706cfcd451b7a82cbfbe7cb9b1e884aee6f1f6824046bc244fb6d753bf945",
|
|
3528
3528
|
"bytes": 7425
|
|
3529
3529
|
},
|
|
3530
3530
|
{
|
|
3531
|
-
"path": "stack-release-
|
|
3532
|
-
"sha256": "
|
|
3533
|
-
"bytes":
|
|
3531
|
+
"path": "stack-release-64UJ6Y4W.js",
|
|
3532
|
+
"sha256": "736e14e7a747a039b8464fd322f967b4a4eb026d77191ec558bc2ce97449dc3a",
|
|
3533
|
+
"bytes": 115927
|
|
3534
3534
|
},
|
|
3535
3535
|
{
|
|
3536
|
-
"path": "stack-update-
|
|
3537
|
-
"sha256": "
|
|
3538
|
-
"bytes":
|
|
3536
|
+
"path": "stack-update-EBATTQPP.js",
|
|
3537
|
+
"sha256": "48c4711a26259d079b26e7d675d3c086f1daadba511e13c4efddfe6211e80f44",
|
|
3538
|
+
"bytes": 8289
|
|
3539
3539
|
},
|
|
3540
3540
|
{
|
|
3541
3541
|
"path": "stalled-dispatch-escalation-MHLYH2PT.js",
|
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
|
|
14
14
|
// src/bin/verify-stack.ts
|
|
15
15
|
import { execSync } from "child_process";
|
|
16
|
-
import { createHash } from "crypto";
|
|
17
16
|
import { readFileSync, existsSync } from "fs";
|
|
18
17
|
import http from "http";
|
|
19
18
|
import path from "path";
|
|
@@ -36,6 +35,18 @@ function evaluateProduct(probes) {
|
|
|
36
35
|
const layer = gatingLayerOf(probes);
|
|
37
36
|
const findings = [];
|
|
38
37
|
const at = (p) => `landed on ${p.finalHost} (status ${p.status}, ${p.hops} hop(s))`;
|
|
38
|
+
for (const [scenario, probe] of Object.entries({
|
|
39
|
+
anonymous: probes.anonymous,
|
|
40
|
+
"forged-session": probes.forgedSession,
|
|
41
|
+
"sentinel-only": probes.sentinelOnly,
|
|
42
|
+
"real-session": probes.realSession
|
|
43
|
+
})) {
|
|
44
|
+
if (probe?.admissionError) findings.push({
|
|
45
|
+
scenario,
|
|
46
|
+
severity: "violation",
|
|
47
|
+
detail: probe.admissionError
|
|
48
|
+
});
|
|
49
|
+
}
|
|
39
50
|
if (layer === "client-gated") {
|
|
40
51
|
findings.push({
|
|
41
52
|
scenario: "anonymous",
|
|
@@ -68,11 +79,12 @@ function evaluateProduct(probes) {
|
|
|
68
79
|
}
|
|
69
80
|
const real = probes.realSession;
|
|
70
81
|
if (real) {
|
|
71
|
-
|
|
82
|
+
const limit = probes.app === "exe-erp" && real.verifiedNativeSession ? 4 : MAX_SSO_HANDOFF_HOPS;
|
|
83
|
+
if (real.hops > limit) {
|
|
72
84
|
findings.push({
|
|
73
85
|
scenario: "real-session",
|
|
74
86
|
severity: "violation",
|
|
75
|
-
detail: `a real apex session took ${real.hops} redirect(s) reaching ${probes.host}, over the contract's ${
|
|
87
|
+
detail: `a real apex session took ${real.hops} redirect(s) reaching ${probes.host}, over the contract's ${limit} \u2014 ${at(real)}. An unbounded hop count here is the CRM livelock (bug 83ba9546 regression): the edge and the app are disagreeing about the handoff and bouncing the user between them.`
|
|
76
88
|
});
|
|
77
89
|
} else if (real.finalHost !== probes.host) {
|
|
78
90
|
findings.push({
|
|
@@ -135,6 +147,130 @@ function parseApexCookies(setCookieLines) {
|
|
|
135
147
|
return jar;
|
|
136
148
|
}
|
|
137
149
|
|
|
150
|
+
// src/lib/sso-admission-probe.ts
|
|
151
|
+
import { createHash } from "crypto";
|
|
152
|
+
var CALLBACK = "/api/method/erpnext.exe_auth.api.gotrue_login_callback";
|
|
153
|
+
var START = "/api/method/erpnext.exe_auth.api.gotrue_login_start";
|
|
154
|
+
var loopback = (u) => u.hostname === "127.0.0.1" || u.hostname === "localhost";
|
|
155
|
+
async function probeAdmission(url, jar, timeoutMs, options) {
|
|
156
|
+
const app = new URL(url), auth = new URL(options.authUrl);
|
|
157
|
+
let current = app, hops = 0, resumed = false, started = false;
|
|
158
|
+
const local = {};
|
|
159
|
+
let actorEmail;
|
|
160
|
+
const safe = (u) => !u.username && !u.password && (u.origin === app.origin || u.origin === auth.origin) && (u.protocol === "https:" || u.protocol === "http:" && loopback(u));
|
|
161
|
+
const headers = (u, apexOnly = false) => ({
|
|
162
|
+
accept: "text/html,application/xhtml+xml",
|
|
163
|
+
"user-agent": "exe-os-verify-stack/sso-session-contract",
|
|
164
|
+
cookie: cookieHeader({
|
|
165
|
+
...jar,
|
|
166
|
+
...u.origin === app.origin && !apexOnly ? local : {}
|
|
167
|
+
})
|
|
168
|
+
});
|
|
169
|
+
const result = (status, body, admissionError) => ({
|
|
170
|
+
finalHost: current.host,
|
|
171
|
+
hops,
|
|
172
|
+
status,
|
|
173
|
+
sawPasswordField: htmlHasPasswordField(body),
|
|
174
|
+
bodyFingerprint: createHash("sha256").update(body).digest("hex"),
|
|
175
|
+
...admissionError ? { admissionError } : {},
|
|
176
|
+
...resumed && actorEmail && !admissionError ? { verifiedNativeSession: true } : {}
|
|
177
|
+
});
|
|
178
|
+
for (; ; ) {
|
|
179
|
+
if (!safe(current))
|
|
180
|
+
return result(0, "", "SSO redirect left the trusted HTTPS origins");
|
|
181
|
+
const res = await fetch(current, {
|
|
182
|
+
headers: headers(current),
|
|
183
|
+
redirect: "manual",
|
|
184
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
185
|
+
});
|
|
186
|
+
if (options.service === "exe-erp" && current.origin === app.origin) {
|
|
187
|
+
for (const line of res.headers.getSetCookie()) {
|
|
188
|
+
const [pair = "", ...attrs] = line.split(";");
|
|
189
|
+
const eq = pair.indexOf("="), name = pair.slice(0, eq).trim();
|
|
190
|
+
if (eq < 1 || !["sid", "exe_sso_state"].includes(name)) continue;
|
|
191
|
+
const domain = attrs.find((a) => /^\s*domain=/i.test(a))?.split("=")[1]?.trim().replace(/^\./, "");
|
|
192
|
+
if (domain && domain !== app.hostname) continue;
|
|
193
|
+
local[name] = pair.slice(eq + 1).trim();
|
|
194
|
+
}
|
|
195
|
+
if (current.pathname === START) started = true;
|
|
196
|
+
}
|
|
197
|
+
if (res.status >= 300 && res.status < 400 && res.headers.get("location")) {
|
|
198
|
+
current = new URL(res.headers.get("location"), current);
|
|
199
|
+
if (++hops < 6) continue;
|
|
200
|
+
return result(res.status, "", "SSO redirect budget exhausted");
|
|
201
|
+
}
|
|
202
|
+
if (options.service === "exe-erp" && options.email && jar.exe_sess && !resumed && started && current.origin === auth.origin && current.pathname === "/login" && res.ok && current.searchParams.has("redirect")) {
|
|
203
|
+
const callback = new URL(current.searchParams.get("redirect"), current);
|
|
204
|
+
if (!safe(callback) || callback.origin !== app.origin || callback.pathname !== CALLBACK || !local.exe_sso_state || callback.searchParams.get("state") !== local.exe_sso_state || [...callback.searchParams.keys()].some((k) => k !== "state")) {
|
|
205
|
+
return result(
|
|
206
|
+
res.status,
|
|
207
|
+
"",
|
|
208
|
+
"ERP callback origin, path or state did not match its initiating cookie"
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
const me = await fetch(new URL("/auth/me", auth), {
|
|
212
|
+
headers: headers(auth),
|
|
213
|
+
redirect: "manual",
|
|
214
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
215
|
+
});
|
|
216
|
+
const actor = me.ok ? await me.json() : null;
|
|
217
|
+
if (!actor || actor.ok !== true || typeof actor.sub !== "string" || !actor.sub || typeof actor.email !== "string" || !options.email || actor.email.toLowerCase() !== options.email.toLowerCase()) {
|
|
218
|
+
return result(
|
|
219
|
+
me.status,
|
|
220
|
+
"",
|
|
221
|
+
"Auth did not validate the expected SSO identity"
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
actorEmail = actor.email;
|
|
225
|
+
resumed = true;
|
|
226
|
+
current = callback;
|
|
227
|
+
if (++hops < 6) continue;
|
|
228
|
+
return result(0, "", "SSO redirect budget exhausted");
|
|
229
|
+
}
|
|
230
|
+
const body = await res.text();
|
|
231
|
+
if (options.email && !res.ok) {
|
|
232
|
+
return result(res.status, body, "Real-session product request was not successful");
|
|
233
|
+
}
|
|
234
|
+
if (resumed && current.origin === app.origin && res.ok) {
|
|
235
|
+
const identity = await fetch(
|
|
236
|
+
new URL("/api/method/frappe.auth.get_logged_user", app),
|
|
237
|
+
{
|
|
238
|
+
headers: headers(app),
|
|
239
|
+
redirect: "manual",
|
|
240
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
241
|
+
}
|
|
242
|
+
);
|
|
243
|
+
const user = identity.ok ? await identity.json() : null;
|
|
244
|
+
if (!local.sid || local.sid === "Guest" || !user || user.message !== actorEmail) {
|
|
245
|
+
return result(
|
|
246
|
+
identity.status,
|
|
247
|
+
body,
|
|
248
|
+
"ERP native session did not match the validated Auth identity"
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
const control = await fetch(new URL("/api/method/frappe.auth.get_logged_user", app), {
|
|
252
|
+
headers: headers(app, true),
|
|
253
|
+
redirect: "manual",
|
|
254
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
255
|
+
});
|
|
256
|
+
const guest = control.ok ? await control.json() : null;
|
|
257
|
+
const location = control.headers.get("location");
|
|
258
|
+
const refusal = location ? new URL(location, app) : null;
|
|
259
|
+
const refused = control.status >= 300 && control.status < 400 && refusal && safe(refusal) && (refusal.origin === auth.origin || refusal.origin === app.origin && [START, "/login"].includes(refusal.pathname));
|
|
260
|
+
if (!(guest?.message === "Guest" || [401, 403].includes(control.status) || refused)) {
|
|
261
|
+
return result(control.status, body, "ERP identity was not independently bound to its native session");
|
|
262
|
+
}
|
|
263
|
+
} else if (resumed) {
|
|
264
|
+
return result(
|
|
265
|
+
res.status,
|
|
266
|
+
body,
|
|
267
|
+
"ERP callback did not finish on the expected product"
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
return result(res.status, body);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
138
274
|
// src/bin/verify-stack.ts
|
|
139
275
|
var DEFAULT_COMPOSE = "/opt/exe-stack/docker-compose.yml";
|
|
140
276
|
var DEFAULT_ENV = "/opt/exe-stack/.env";
|
|
@@ -518,57 +654,11 @@ async function checkSsoCookieContract(deployed, urlOverrides) {
|
|
|
518
654
|
message: `${Object.keys(urls).length} product(s) free of retired SSO cookie names`
|
|
519
655
|
};
|
|
520
656
|
}
|
|
521
|
-
var MAX_PROBE_HOPS = 6;
|
|
522
657
|
var SSO_ADMISSION_APPS = {
|
|
523
658
|
"exe-crm": "crm",
|
|
524
659
|
"exe-erp": "erp",
|
|
525
660
|
"exe-wiki": "wiki"
|
|
526
661
|
};
|
|
527
|
-
async function probeAdmission(url, jar, timeoutMs) {
|
|
528
|
-
let current = url;
|
|
529
|
-
let hops = 0;
|
|
530
|
-
const headers = {
|
|
531
|
-
// Some edges vary their gate on Accept; ask for a document like a browser does.
|
|
532
|
-
accept: "text/html,application/xhtml+xml",
|
|
533
|
-
"user-agent": "exe-os-verify-stack/sso-session-contract"
|
|
534
|
-
};
|
|
535
|
-
const cookie = cookieHeader(jar);
|
|
536
|
-
if (cookie) headers.cookie = cookie;
|
|
537
|
-
for (; ; ) {
|
|
538
|
-
const res = await fetch(current, {
|
|
539
|
-
headers,
|
|
540
|
-
redirect: "manual",
|
|
541
|
-
signal: AbortSignal.timeout(timeoutMs)
|
|
542
|
-
});
|
|
543
|
-
if (res.status >= 300 && res.status < 400) {
|
|
544
|
-
const location = res.headers.get("location");
|
|
545
|
-
if (location) {
|
|
546
|
-
hops += 1;
|
|
547
|
-
current = new URL(location, current).toString();
|
|
548
|
-
if (hops < MAX_PROBE_HOPS) continue;
|
|
549
|
-
}
|
|
550
|
-
return {
|
|
551
|
-
finalHost: new URL(current).host,
|
|
552
|
-
hops,
|
|
553
|
-
status: res.status,
|
|
554
|
-
sawPasswordField: false,
|
|
555
|
-
bodyFingerprint: `redirect:${res.status}:${location ?? ""}`
|
|
556
|
-
};
|
|
557
|
-
}
|
|
558
|
-
let body = "";
|
|
559
|
-
try {
|
|
560
|
-
body = await res.text();
|
|
561
|
-
} catch {
|
|
562
|
-
}
|
|
563
|
-
return {
|
|
564
|
-
finalHost: new URL(current).host,
|
|
565
|
-
hops,
|
|
566
|
-
status: res.status,
|
|
567
|
-
sawPasswordField: htmlHasPasswordField(body),
|
|
568
|
-
bodyFingerprint: createHash("sha256").update(body).digest("hex")
|
|
569
|
-
};
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
662
|
async function loginForApexCookies(authUrl, email, password, timeoutMs) {
|
|
573
663
|
let res;
|
|
574
664
|
try {
|
|
@@ -642,8 +732,22 @@ async function checkSsoSessionContract(env, deployed, overrides) {
|
|
|
642
732
|
const settled = await Promise.all(
|
|
643
733
|
pairs.map(async (pair) => {
|
|
644
734
|
try {
|
|
645
|
-
return { pair, probe: await probeAdmission(pair.target.baseUrl, pair.jar, CHECK_TIMEOUT_MS * 2
|
|
735
|
+
return { pair, probe: await probeAdmission(pair.target.baseUrl, pair.jar, CHECK_TIMEOUT_MS * 2, {
|
|
736
|
+
authUrl,
|
|
737
|
+
service: pair.target.service,
|
|
738
|
+
email: pair.scenario === "real-session" ? email : void 0
|
|
739
|
+
}) };
|
|
646
740
|
} catch (err) {
|
|
741
|
+
if (pair.scenario === "real-session") {
|
|
742
|
+
return { pair, probe: {
|
|
743
|
+
finalHost: pair.target.host,
|
|
744
|
+
hops: 0,
|
|
745
|
+
status: 0,
|
|
746
|
+
sawPasswordField: false,
|
|
747
|
+
bodyFingerprint: "",
|
|
748
|
+
admissionError: "Real-session admission probe could not complete safely"
|
|
749
|
+
} };
|
|
750
|
+
}
|
|
647
751
|
return { pair, error: err instanceof Error ? err.message : String(err) };
|
|
648
752
|
}
|
|
649
753
|
})
|