openmates 0.12.0-alpha.28 → 0.12.0-alpha.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-TTQIZ476.js → chunk-FCSXUBOU.js} +165 -8
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
|
@@ -8964,7 +8964,7 @@ function formatTs(ts) {
|
|
|
8964
8964
|
}
|
|
8965
8965
|
|
|
8966
8966
|
// src/server.ts
|
|
8967
|
-
import { execSync, spawn as nodeSpawn } from "child_process";
|
|
8967
|
+
import { execFileSync as execFileSync2, execSync, spawn as nodeSpawn } from "child_process";
|
|
8968
8968
|
import { createHash as createHash6, randomBytes as randomBytes2 } from "crypto";
|
|
8969
8969
|
import { chmodSync as chmodSync2, copyFileSync, cpSync, existsSync as existsSync5, mkdirSync as mkdirSync3, mkdtempSync, readFileSync as readFileSync5, readdirSync, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
8970
8970
|
import { createInterface as createInterface2 } from "readline";
|
|
@@ -9104,6 +9104,49 @@ function planRestore(input) {
|
|
|
9104
9104
|
steps: input.yes === true ? ["stop", "restore", "start", "health-check"] : ["confirm", "stop", "restore", "start", "health-check"]
|
|
9105
9105
|
};
|
|
9106
9106
|
}
|
|
9107
|
+
function parseSecretEnvKey(envKey) {
|
|
9108
|
+
if (!envKey.startsWith("SECRET__")) return null;
|
|
9109
|
+
const parts = envKey.slice("SECRET__".length).split("__", 2);
|
|
9110
|
+
if (parts.length !== 2 || !parts[0] || !parts[1]) return null;
|
|
9111
|
+
return {
|
|
9112
|
+
envKey,
|
|
9113
|
+
vaultPath: `kv/data/providers/${parts[0].toLowerCase()}`,
|
|
9114
|
+
vaultKey: parts[1].toLowerCase()
|
|
9115
|
+
};
|
|
9116
|
+
}
|
|
9117
|
+
function summarizeSecretPreflight(input) {
|
|
9118
|
+
const inlineSecretEnvKeys = [];
|
|
9119
|
+
const importedSecretEnvKeys = [];
|
|
9120
|
+
const emptySecretEnvKeys = [];
|
|
9121
|
+
const importedVaultPresent = [];
|
|
9122
|
+
const importedVaultMissing = [];
|
|
9123
|
+
const importedVaultUnavailable = [];
|
|
9124
|
+
for (const [envKey, rawValue] of Object.entries(input.env).sort(([a], [b]) => a.localeCompare(b))) {
|
|
9125
|
+
if (!parseSecretEnvKey(envKey)) continue;
|
|
9126
|
+
const value = rawValue.trim();
|
|
9127
|
+
if (!value) {
|
|
9128
|
+
emptySecretEnvKeys.push(envKey);
|
|
9129
|
+
continue;
|
|
9130
|
+
}
|
|
9131
|
+
if (value !== "IMPORTED_TO_VAULT") {
|
|
9132
|
+
inlineSecretEnvKeys.push(envKey);
|
|
9133
|
+
continue;
|
|
9134
|
+
}
|
|
9135
|
+
importedSecretEnvKeys.push(envKey);
|
|
9136
|
+
const presence = input.vaultPresence?.[envKey];
|
|
9137
|
+
if (presence === "present") importedVaultPresent.push(envKey);
|
|
9138
|
+
else if (presence === "missing") importedVaultMissing.push(envKey);
|
|
9139
|
+
else importedVaultUnavailable.push(envKey);
|
|
9140
|
+
}
|
|
9141
|
+
return {
|
|
9142
|
+
inlineSecretEnvKeys,
|
|
9143
|
+
importedSecretEnvKeys,
|
|
9144
|
+
emptySecretEnvKeys,
|
|
9145
|
+
importedVaultPresent,
|
|
9146
|
+
importedVaultMissing,
|
|
9147
|
+
importedVaultUnavailable
|
|
9148
|
+
};
|
|
9149
|
+
}
|
|
9107
9150
|
function planCaddyCommand(input) {
|
|
9108
9151
|
const role = parseServerRole(input.role);
|
|
9109
9152
|
const templatePath = `templates/caddy/${role}/Caddyfile`;
|
|
@@ -9796,6 +9839,89 @@ function missingRequiredEnvKeys(installPath, role) {
|
|
|
9796
9839
|
const env = readEnvMap(installPath);
|
|
9797
9840
|
return requiredRuntimeEnvKeys(role).filter((key) => !env[key]);
|
|
9798
9841
|
}
|
|
9842
|
+
function vaultCheckContainerForRole(role) {
|
|
9843
|
+
if (role === "core") return "api";
|
|
9844
|
+
if (role === "upload") return "app-uploads";
|
|
9845
|
+
return null;
|
|
9846
|
+
}
|
|
9847
|
+
function readImportedVaultSecretPresence(role, importedSecrets) {
|
|
9848
|
+
const checks = importedSecrets.filter((item) => item !== null);
|
|
9849
|
+
if (checks.length === 0) return { presence: {}, checked: true, unavailableReason: null };
|
|
9850
|
+
const container = vaultCheckContainerForRole(role);
|
|
9851
|
+
if (!container) {
|
|
9852
|
+
return { presence: {}, checked: false, unavailableReason: `${role} role does not expose a Vault-backed app container for secret verification.` };
|
|
9853
|
+
}
|
|
9854
|
+
const script = `
|
|
9855
|
+
import json
|
|
9856
|
+
import os
|
|
9857
|
+
import urllib.error
|
|
9858
|
+
import urllib.request
|
|
9859
|
+
|
|
9860
|
+
checks = json.loads(os.environ["OPENMATES_SECRET_CHECKS"])
|
|
9861
|
+
token_path = "/vault-data/api.token"
|
|
9862
|
+
try:
|
|
9863
|
+
with open(token_path, "r", encoding="utf-8") as token_file:
|
|
9864
|
+
token = token_file.read().strip()
|
|
9865
|
+
except Exception as exc:
|
|
9866
|
+
print(json.dumps({"ok": False, "reason": f"could not read {token_path}: {exc}", "presence": {}}))
|
|
9867
|
+
raise SystemExit(0)
|
|
9868
|
+
|
|
9869
|
+
presence = {}
|
|
9870
|
+
for item in checks:
|
|
9871
|
+
request = urllib.request.Request(
|
|
9872
|
+
f"http://vault:8200/v1/{item['vaultPath']}",
|
|
9873
|
+
headers={"X-Vault-Token": token},
|
|
9874
|
+
)
|
|
9875
|
+
try:
|
|
9876
|
+
with urllib.request.urlopen(request, timeout=3) as response:
|
|
9877
|
+
payload = json.loads(response.read().decode("utf-8"))
|
|
9878
|
+
value = payload.get("data", {}).get("data", {}).get(item["vaultKey"])
|
|
9879
|
+
presence[item["envKey"]] = "present" if value else "missing"
|
|
9880
|
+
except urllib.error.HTTPError as exc:
|
|
9881
|
+
presence[item["envKey"]] = "missing" if exc.code == 404 else "unavailable"
|
|
9882
|
+
except Exception:
|
|
9883
|
+
presence[item["envKey"]] = "unavailable"
|
|
9884
|
+
|
|
9885
|
+
print(json.dumps({"ok": True, "presence": presence}))
|
|
9886
|
+
`;
|
|
9887
|
+
try {
|
|
9888
|
+
const output = execFileSync2(
|
|
9889
|
+
"docker",
|
|
9890
|
+
["exec", "-e", `OPENMATES_SECRET_CHECKS=${JSON.stringify(checks)}`, container, "python", "-c", script],
|
|
9891
|
+
{ encoding: "utf-8", timeout: 1e4, stdio: ["pipe", "pipe", "pipe"] }
|
|
9892
|
+
).trim();
|
|
9893
|
+
const parsed = JSON.parse(output);
|
|
9894
|
+
if (parsed.ok === false) return { presence: {}, checked: false, unavailableReason: parsed.reason ?? "Vault check failed." };
|
|
9895
|
+
return { presence: parsed.presence ?? {}, checked: true, unavailableReason: null };
|
|
9896
|
+
} catch (error) {
|
|
9897
|
+
return {
|
|
9898
|
+
presence: {},
|
|
9899
|
+
checked: false,
|
|
9900
|
+
unavailableReason: `Vault check unavailable. Is the ${container} container running? ${error instanceof Error ? error.message : String(error)}`
|
|
9901
|
+
};
|
|
9902
|
+
}
|
|
9903
|
+
}
|
|
9904
|
+
function runtimeSecretPreflight(installPath, role) {
|
|
9905
|
+
const env = readEnvMap(installPath);
|
|
9906
|
+
const importedSecrets = Object.entries(env).filter(([, value]) => value.trim() === "IMPORTED_TO_VAULT").map(([envKey]) => parseSecretEnvKey(envKey)).filter((item) => item !== null);
|
|
9907
|
+
const vault = readImportedVaultSecretPresence(role, importedSecrets);
|
|
9908
|
+
return {
|
|
9909
|
+
...summarizeSecretPreflight({ env, vaultPresence: vault.presence }),
|
|
9910
|
+
vaultChecked: vault.checked,
|
|
9911
|
+
vaultUnavailableReason: vault.unavailableReason
|
|
9912
|
+
};
|
|
9913
|
+
}
|
|
9914
|
+
function formatSecretPreflight(preflight) {
|
|
9915
|
+
const parts = [
|
|
9916
|
+
`${preflight.importedVaultPresent.length}/${preflight.importedSecretEnvKeys.length} imported verified`
|
|
9917
|
+
];
|
|
9918
|
+
if (preflight.importedVaultMissing.length) parts.push(`missing in Vault: ${preflight.importedVaultMissing.join(", ")}`);
|
|
9919
|
+
if (preflight.importedVaultUnavailable.length) parts.push(`unverified: ${preflight.importedVaultUnavailable.join(", ")}`);
|
|
9920
|
+
if (preflight.inlineSecretEnvKeys.length) parts.push(`inline SECRET entries: ${preflight.inlineSecretEnvKeys.length}`);
|
|
9921
|
+
if (preflight.emptySecretEnvKeys.length) parts.push(`empty SECRET entries: ${preflight.emptySecretEnvKeys.length}`);
|
|
9922
|
+
if (preflight.vaultUnavailableReason) parts.push(preflight.vaultUnavailableReason);
|
|
9923
|
+
return parts.join("; ");
|
|
9924
|
+
}
|
|
9799
9925
|
function writeChecksums(rootDir) {
|
|
9800
9926
|
const lines = [];
|
|
9801
9927
|
const walk = (dir) => {
|
|
@@ -10339,6 +10465,12 @@ async function serverUpdate(rest, flags) {
|
|
|
10339
10465
|
const filterRequested = hasServiceFilter(flags);
|
|
10340
10466
|
const selectedServices = filterRequested ? selectedComposeServices(role, flags) : [];
|
|
10341
10467
|
const missingEnvKeys = missingRequiredEnvKeys(installPath, role);
|
|
10468
|
+
const secretPreflight = runtimeSecretPreflight(installPath, role);
|
|
10469
|
+
const missingOrUnverifiedSecrets = [
|
|
10470
|
+
...missingEnvKeys,
|
|
10471
|
+
...secretPreflight.importedVaultMissing,
|
|
10472
|
+
...secretPreflight.importedVaultUnavailable
|
|
10473
|
+
];
|
|
10342
10474
|
if (installMode === "source" && (flags["image-tag"] !== void 0 || flags.channel !== void 0)) {
|
|
10343
10475
|
throw new Error("--image-tag and --channel only apply to image-mode installs. Source-mode installs update from Git.");
|
|
10344
10476
|
}
|
|
@@ -10348,7 +10480,7 @@ async function serverUpdate(rest, flags) {
|
|
|
10348
10480
|
const currentTag = getImageTagFromEnv(installPath, config);
|
|
10349
10481
|
const target = resolveTargetImageTag(flags, currentTag, getPackageVersion());
|
|
10350
10482
|
const templateRef = templateRefForImageTag(target.tag, getPackageVersion());
|
|
10351
|
-
const safetyPlan = planUpdate({ role, selectedServices, dryRun, skipBackup: flags["skip-backup"] === true, continuous: false, missingRequiredSecrets:
|
|
10483
|
+
const safetyPlan = planUpdate({ role, selectedServices, dryRun, skipBackup: flags["skip-backup"] === true, continuous: false, missingRequiredSecrets: missingOrUnverifiedSecrets });
|
|
10352
10484
|
const plan = {
|
|
10353
10485
|
command: "update",
|
|
10354
10486
|
role,
|
|
@@ -10362,6 +10494,7 @@ async function serverUpdate(rest, flags) {
|
|
|
10362
10494
|
steps: safetyPlan.steps,
|
|
10363
10495
|
backupName: safetyPlan.backupName,
|
|
10364
10496
|
missingRequiredEnvKeys: missingEnvKeys,
|
|
10497
|
+
secretPreflight,
|
|
10365
10498
|
blocked: safetyPlan.blocked,
|
|
10366
10499
|
blockReason: safetyPlan.blockReason,
|
|
10367
10500
|
dryRun
|
|
@@ -10380,13 +10513,16 @@ async function serverUpdate(rest, flags) {
|
|
|
10380
10513
|
console.log(` Backup: ${safetyPlan.backupName ?? "none"}`);
|
|
10381
10514
|
console.log(` Steps: ${safetyPlan.steps.join(" -> ")}`);
|
|
10382
10515
|
console.log(` Env preflight: ${missingEnvKeys.length ? `missing ${missingEnvKeys.join(", ")}` : "ok"}`);
|
|
10516
|
+
console.log(` Vault secrets: ${formatSecretPreflight(secretPreflight)}`);
|
|
10383
10517
|
console.log(" Commands: refresh compose, docker compose pull, docker compose up -d, health checks");
|
|
10384
10518
|
}
|
|
10385
10519
|
return;
|
|
10386
10520
|
}
|
|
10387
10521
|
if (safetyPlan.blocked) throw new Error(safetyPlan.blockReason ?? "Update blocked by preflight.");
|
|
10388
|
-
if (
|
|
10389
|
-
throw new Error(
|
|
10522
|
+
if (missingOrUnverifiedSecrets.length && flags.yes !== true) {
|
|
10523
|
+
throw new Error(
|
|
10524
|
+
`Required runtime secret checks failed: ${missingOrUnverifiedSecrets.join(", ")}. Add missing non-SECRET values to .env, ensure IMPORTED_TO_VAULT markers exist in Vault, or rerun with --yes after reviewing.`
|
|
10525
|
+
);
|
|
10390
10526
|
}
|
|
10391
10527
|
console.error(`Mode: image`);
|
|
10392
10528
|
console.error(`Current image tag: ${currentTag || "unknown"}`);
|
|
@@ -10939,8 +11075,14 @@ async function serverPreflight(flags) {
|
|
|
10939
11075
|
const config = loadConfigForInstallPath(installPath);
|
|
10940
11076
|
const role = getServerRole(flags, config);
|
|
10941
11077
|
const services = hasServiceFilter(flags) ? selectedComposeServices(role, flags) : [];
|
|
10942
|
-
const updatePlan = planUpdate({ role, selectedServices: services, dryRun: true });
|
|
10943
11078
|
const missingEnvKeys = missingRequiredEnvKeys(installPath, role);
|
|
11079
|
+
const secretPreflight = runtimeSecretPreflight(installPath, role);
|
|
11080
|
+
const missingOrUnverifiedSecrets = [
|
|
11081
|
+
...missingEnvKeys,
|
|
11082
|
+
...secretPreflight.importedVaultMissing,
|
|
11083
|
+
...secretPreflight.importedVaultUnavailable
|
|
11084
|
+
];
|
|
11085
|
+
const updatePlan = planUpdate({ role, selectedServices: services, dryRun: true, continuous: true, missingRequiredSecrets: missingOrUnverifiedSecrets });
|
|
10944
11086
|
const runtimePlan = planServerRuntime({
|
|
10945
11087
|
role,
|
|
10946
11088
|
profile: getCoreProfile(flags, config),
|
|
@@ -10958,6 +11100,10 @@ async function serverPreflight(flags) {
|
|
|
10958
11100
|
updateSteps: updatePlan.steps,
|
|
10959
11101
|
backupName: updatePlan.backupName,
|
|
10960
11102
|
missingRequiredEnvKeys: missingEnvKeys,
|
|
11103
|
+
secretPreflight,
|
|
11104
|
+
missingOrUnverifiedSecrets,
|
|
11105
|
+
blocked: updatePlan.blocked,
|
|
11106
|
+
blockReason: updatePlan.blockReason,
|
|
10961
11107
|
caddy: caddyPlan
|
|
10962
11108
|
};
|
|
10963
11109
|
if (flags.json === true) {
|
|
@@ -10970,6 +11116,8 @@ async function serverPreflight(flags) {
|
|
|
10970
11116
|
console.log(` Services: ${hasServiceFilter(flags) ? services.join(", ") : "all"}`);
|
|
10971
11117
|
console.log(` Backup: ${updatePlan.backupName ?? "none"}`);
|
|
10972
11118
|
console.log(` Env preflight: ${missingEnvKeys.length ? `missing ${missingEnvKeys.join(", ")}` : "ok"}`);
|
|
11119
|
+
console.log(` Vault secrets: ${formatSecretPreflight(secretPreflight)}`);
|
|
11120
|
+
if (updatePlan.blocked) console.log(` Blocked: ${updatePlan.blockReason}`);
|
|
10973
11121
|
console.log(` Health checks: ${runtimePlan.healthChecks.join(", ")}`);
|
|
10974
11122
|
console.log(` Caddy steps: ${caddyPlan.steps.join(" -> ")}`);
|
|
10975
11123
|
}
|
|
@@ -11022,14 +11170,16 @@ WARNING: This will replace ${appliedPath} with the packaged ${role} Caddyfile.`)
|
|
|
11022
11170
|
}
|
|
11023
11171
|
}
|
|
11024
11172
|
const backupPath = `${appliedPath}.openmates-backup-${nowStamp()}`;
|
|
11173
|
+
const hadExistingCaddyfile = existsSync5(appliedPath);
|
|
11025
11174
|
try {
|
|
11026
|
-
|
|
11175
|
+
mkdirSync3(dirname(appliedPath), { recursive: true });
|
|
11176
|
+
if (hadExistingCaddyfile) copyFileSync(appliedPath, backupPath);
|
|
11027
11177
|
copyFileSync(templatePath, appliedPath);
|
|
11028
11178
|
execSync("systemctl reload caddy", { stdio: "inherit" });
|
|
11029
11179
|
} catch (error) {
|
|
11030
11180
|
throw new Error(`Could not apply Caddyfile. Run with sudo or use --config <writable path>. ${error instanceof Error ? error.message : String(error)}`);
|
|
11031
11181
|
}
|
|
11032
|
-
console.log(`Applied Caddyfile for ${role}. Backup: ${backupPath}`);
|
|
11182
|
+
console.log(`Applied Caddyfile for ${role}. ${hadExistingCaddyfile ? `Backup: ${backupPath}` : "No previous Caddyfile existed."}`);
|
|
11033
11183
|
return;
|
|
11034
11184
|
}
|
|
11035
11185
|
console.log(`Caddy ${action} plan:`);
|
|
@@ -25307,7 +25457,8 @@ version_number: 1`,
|
|
|
25307
25457
|
],
|
|
25308
25458
|
metadata: {
|
|
25309
25459
|
featured: true,
|
|
25310
|
-
order: 105
|
|
25460
|
+
order: 105,
|
|
25461
|
+
content_embed_examples: ["mindmaps.mindmap"]
|
|
25311
25462
|
}
|
|
25312
25463
|
};
|
|
25313
25464
|
|
|
@@ -28033,6 +28184,12 @@ Only output the final Markdown table. Do NOT include explanations, notes, or any
|
|
|
28033
28184
|
continue: {
|
|
28034
28185
|
text: "Continue"
|
|
28035
28186
|
},
|
|
28187
|
+
skip: {
|
|
28188
|
+
text: "Skip"
|
|
28189
|
+
},
|
|
28190
|
+
select_interests: {
|
|
28191
|
+
text: "Select interests"
|
|
28192
|
+
},
|
|
28036
28193
|
software_development: {
|
|
28037
28194
|
text: "software development"
|
|
28038
28195
|
},
|
package/dist/cli.js
CHANGED
package/dist/index.js
CHANGED