@namewta/speculo 1.0.6 → 1.0.8
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/src/ops-resources.js +1 -1
- package/dist/src/ops-resources.js.map +1 -1
- package/package.json +1 -1
- package/template/workflows/ops/D-project-deploy/D-project-deploy.md +1 -1
- package/template/workflows/ops/H-host-manage/H-host-manage.md +5 -1
- package/template/workflows/ops/I-initialize/I-initialize.md +2 -2
- package/template/workflows/ops/README.md +8 -5
- package/template/workflows/ops/common/CAPABILITIES.md +2 -2
- package/template/workflows/ops/common/USAGE.md +25 -25
- package/template/workflows/ops/common/examples/README.md +1 -1
- package/template/workflows/ops/common/examples/register.example.json +1 -1
- package/template/workflows/ops/common/rules/persistence-and-secrets.md +1 -1
- package/template/workflows/ops/common/schemas/host.schema.json +65 -2
- package/template/workflows/ops/common/schemas/plan.schema.json +315 -3
- package/template/workflows/ops/common/schemas/spec.schema.json +137 -1
- package/template/workflows/ops/common/schemas/status.schema.json +143 -1
- package/template/workflows/ops/common/service-profiles/elasticsearch.md +13 -0
- package/template/workflows/ops/common/service-profiles/redis.md +4 -0
- package/template/workflows/ops/common/templates/CONTROLLER-RECORD.md +16 -2
- package/template/workflows/ops/common/templates/HOST-README.md +12 -2
- package/template/workflows/ops/common/tests/test_ops.mjs +982 -0
- package/template/workflows/ops/common/tools/bootstrap.ps1 +2 -2
- package/template/workflows/ops/common/tools/bootstrap.sh +2 -2
- package/template/workflows/ops/common/tools/demo-local.mjs +101 -0
- package/template/workflows/ops/common/tools/ops.mjs +4 -0
- package/template/workflows/ops/common/tools/opslib/agent.mjs +904 -0
- package/template/workflows/ops/common/tools/opslib/cli.mjs +311 -0
- package/template/workflows/ops/common/tools/opslib/control_files.mjs +57 -0
- package/template/workflows/ops/common/tools/opslib/core.mjs +393 -0
- package/template/workflows/ops/common/tools/opslib/docs.mjs +436 -0
- package/template/workflows/ops/common/tools/opslib/execution.mjs +396 -0
- package/template/workflows/ops/common/tools/opslib/host_recipes.mjs +109 -0
- package/template/workflows/ops/common/tools/opslib/model.mjs +272 -0
- package/template/workflows/ops/common/tools/opslib/{native_windows.py → native_windows.mjs} +16 -12
- package/template/workflows/ops/common/tools/opslib/planner.mjs +781 -0
- package/template/workflows/ops/common/tools/opslib/services.mjs +76 -0
- package/template/workflows/ops/common/tools/opslib/sources.mjs +56 -0
- package/template/workflows/ops/common/tools/opslib/transport.mjs +127 -0
- package/template/workflows/ops/common/tools/validate-ops.mjs +43 -30
- package/template/workflows/ops/common/tests/test_ops.py +0 -392
- package/template/workflows/ops/common/tools/demo-local.py +0 -64
- package/template/workflows/ops/common/tools/ops.py +0 -7
- package/template/workflows/ops/common/tools/opslib/__init__.py +0 -2
- package/template/workflows/ops/common/tools/opslib/__pycache__/__init__.cpython-312.pyc +0 -0
- package/template/workflows/ops/common/tools/opslib/__pycache__/core.cpython-312.pyc +0 -0
- package/template/workflows/ops/common/tools/opslib/__pycache__/model.cpython-312.pyc +0 -0
- package/template/workflows/ops/common/tools/opslib/agent.py +0 -510
- package/template/workflows/ops/common/tools/opslib/cli.py +0 -172
- package/template/workflows/ops/common/tools/opslib/core.py +0 -199
- package/template/workflows/ops/common/tools/opslib/docs.py +0 -199
- package/template/workflows/ops/common/tools/opslib/execution.py +0 -248
- package/template/workflows/ops/common/tools/opslib/host_recipes.py +0 -67
- package/template/workflows/ops/common/tools/opslib/model.py +0 -199
- package/template/workflows/ops/common/tools/opslib/planner.py +0 -497
- package/template/workflows/ops/common/tools/opslib/services.py +0 -47
- package/template/workflows/ops/common/tools/opslib/sources.py +0 -27
- package/template/workflows/ops/common/tools/opslib/transport.py +0 -55
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
/** OPS resource runtime primitives. Node standard library only. */
|
|
2
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
+
import {
|
|
4
|
+
chmodSync, closeSync, existsSync, fsyncSync, linkSync, lstatSync, mkdirSync,
|
|
5
|
+
openSync, readFileSync, renameSync, rmSync, unlinkSync, writeSync,
|
|
6
|
+
} from "node:fs";
|
|
7
|
+
import { hostname } from "node:os";
|
|
8
|
+
import { dirname, join, posix, win32 } from "node:path";
|
|
9
|
+
import { spawnSync } from "node:child_process";
|
|
10
|
+
|
|
11
|
+
export const VERSION = "2.2.0";
|
|
12
|
+
const ID_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
13
|
+
export const SECRET_RE = /\{\{credential:([a-z0-9-]+)@([1-9][0-9]*):([A-Za-z_][A-Za-z0-9_]*)\}\}/g;
|
|
14
|
+
|
|
15
|
+
export class OpsError extends Error {
|
|
16
|
+
constructor(message) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "OpsError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class UnknownResult extends OpsError {
|
|
23
|
+
constructor(message) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = "UnknownResult";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function now() {
|
|
30
|
+
return new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function sortKeys(value) {
|
|
34
|
+
if (Array.isArray(value)) return value.map(sortKeys);
|
|
35
|
+
if (value && typeof value === "object") {
|
|
36
|
+
return Object.fromEntries(Object.keys(value).sort().map((k) => [k, sortKeys(value[k])]));
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function assertJsonSafe(value) {
|
|
42
|
+
if (typeof value === "number" && !Number.isFinite(value)) {
|
|
43
|
+
throw new TypeError("Out of range float values are not JSON compliant");
|
|
44
|
+
}
|
|
45
|
+
if (Array.isArray(value)) value.forEach(assertJsonSafe);
|
|
46
|
+
else if (value && typeof value === "object") Object.values(value).forEach(assertJsonSafe);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function canonical(value) {
|
|
50
|
+
assertJsonSafe(value);
|
|
51
|
+
return Buffer.from(JSON.stringify(sortKeys(value)), "utf8");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function digest(value) {
|
|
55
|
+
const data = Buffer.isBuffer(value) ? value : canonical(value);
|
|
56
|
+
return createHash("sha256").update(data).digest("hex");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function newId(prefix) {
|
|
60
|
+
const d = new Date();
|
|
61
|
+
const ts = [
|
|
62
|
+
d.getUTCFullYear(),
|
|
63
|
+
String(d.getUTCMonth() + 1).padStart(2, "0"),
|
|
64
|
+
String(d.getUTCDate()).padStart(2, "0"),
|
|
65
|
+
String(d.getUTCHours()).padStart(2, "0"),
|
|
66
|
+
String(d.getUTCMinutes()).padStart(2, "0"),
|
|
67
|
+
String(d.getUTCSeconds()).padStart(2, "0"),
|
|
68
|
+
].join("");
|
|
69
|
+
return `${prefix}-${ts}-${randomBytes(4).toString("hex")}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function identifier(value, label = "id") {
|
|
73
|
+
if (typeof value !== "string" || !ID_RE.test(value) || value.length > 80) {
|
|
74
|
+
throw new OpsError(`${label}: expected lowercase kebab id (1..80 characters)`);
|
|
75
|
+
}
|
|
76
|
+
if (/^(?:con|prn|aux|nul|com[0-9]|lpt[0-9])$/.test(value)) {
|
|
77
|
+
throw new OpsError(`${label}: Windows reserved name`);
|
|
78
|
+
}
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function pyList(items) {
|
|
83
|
+
return `[${items.map((x) => `'${x}'`).join(", ")}]`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function exact(value, allowed, required, label) {
|
|
87
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
88
|
+
throw new OpsError(`${label}: expected object`);
|
|
89
|
+
}
|
|
90
|
+
const unknown = Object.keys(value).filter((k) => !allowed.has(k)).sort();
|
|
91
|
+
if (unknown.length) throw new OpsError(`${label}: unknown fields: ${pyList(unknown)}`);
|
|
92
|
+
const missing = [...required].filter((k) => !(k in value)).sort();
|
|
93
|
+
if (missing.length) throw new OpsError(`${label}: missing fields: ${pyList(missing)}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function pyRepr(value) {
|
|
97
|
+
if (typeof value !== "string") return String(value);
|
|
98
|
+
return `'${value.replaceAll("\\", "\\\\").replaceAll("'", "\\'")}'`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function relative(value) {
|
|
102
|
+
if (typeof value !== "string" || !value || value.includes("\\") || value.includes("\x00") || value.includes(":")) {
|
|
103
|
+
throw new OpsError(`unsafe relative path: ${pyRepr(value)}`);
|
|
104
|
+
}
|
|
105
|
+
if (value.startsWith("/") || value.split("/").some((x) => x === "" || x === "." || x === "..")) {
|
|
106
|
+
throw new OpsError(`unsafe relative path: ${pyRepr(value)}`);
|
|
107
|
+
}
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function adapter(platform) {
|
|
112
|
+
return platform === "windows" ? win32 : posix;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function rootPath(value, platform) {
|
|
116
|
+
const p = adapter(platform);
|
|
117
|
+
if (typeof value !== "string" || value.includes("\x00") || !p.isAbsolute(value) || value.split(/[\\/]/).includes("..")) {
|
|
118
|
+
throw new OpsError("host root must be an absolute, non-traversing path");
|
|
119
|
+
}
|
|
120
|
+
const n = p.normalize(value);
|
|
121
|
+
if (platform === "windows") {
|
|
122
|
+
if (n.startsWith("\\\\") || p.parse(n).root === n || n.slice(2).includes(":")) {
|
|
123
|
+
throw new OpsError("UNC, drive root and ADS paths are not host roots");
|
|
124
|
+
}
|
|
125
|
+
const parts = n.split(/[\\/]/).filter(Boolean);
|
|
126
|
+
if (parts.length < 2) throw new OpsError("UNC, drive root and ADS paths are not host roots");
|
|
127
|
+
const lower = n.toLowerCase().replace(/\\+$/, "");
|
|
128
|
+
if (["c:\\windows", "c:\\program files", "c:\\users", "c:\\programdata"].includes(lower)) {
|
|
129
|
+
throw new OpsError("a system directory cannot be host_root");
|
|
130
|
+
}
|
|
131
|
+
return n;
|
|
132
|
+
}
|
|
133
|
+
if (["/", "/etc", "/usr", "/var", "/home", "/root", "/tmp", "/srv", "/opt", "/mnt"].includes(n)) {
|
|
134
|
+
throw new OpsError("host_root must be an OPS-specific child directory");
|
|
135
|
+
}
|
|
136
|
+
return n;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function targetJoin(host, ...parts) {
|
|
140
|
+
const p = adapter(host.platform);
|
|
141
|
+
let cur = host.root;
|
|
142
|
+
for (const part of parts) cur = p.join(cur, ...relative(part).split("/"));
|
|
143
|
+
return cur;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function within(path, root, platform) {
|
|
147
|
+
const p = adapter(platform);
|
|
148
|
+
if (!p.isAbsolute(path) || path.split(/[\\/]/).includes("..")) return false;
|
|
149
|
+
const rel = p.relative(root, path);
|
|
150
|
+
if (!rel || rel.startsWith("..") || p.isAbsolute(rel)) return false;
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function noSymlinks(path, { allowMissing = true } = {}) {
|
|
155
|
+
const chain = [];
|
|
156
|
+
let cur = path;
|
|
157
|
+
while (true) {
|
|
158
|
+
chain.unshift(cur);
|
|
159
|
+
const parent = dirname(cur);
|
|
160
|
+
if (parent === cur) break;
|
|
161
|
+
cur = parent;
|
|
162
|
+
}
|
|
163
|
+
for (const q of chain) {
|
|
164
|
+
let st;
|
|
165
|
+
try {
|
|
166
|
+
st = lstatSync(q);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
if (error.code === "ENOENT") {
|
|
169
|
+
if (allowMissing) continue;
|
|
170
|
+
throw new OpsError(`missing path: ${q}`);
|
|
171
|
+
}
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
if (st.isSymbolicLink()) throw new OpsError(`symlink/reparse point rejected: ${q}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function secure(path, directory = false) {
|
|
179
|
+
if (process.platform !== "win32") {
|
|
180
|
+
chmodSync(path, directory ? 0o700 : 0o600);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const who = spawnSync("whoami", ["/user", "/fo", "csv", "/nh"], { encoding: "utf8" });
|
|
184
|
+
if (who.status !== 0) throw new Error(who.stderr);
|
|
185
|
+
const sid = who.stdout.trim().split(",").at(-1).replaceAll('"', "");
|
|
186
|
+
const flags = directory ? "(OI)(CI)F" : "F";
|
|
187
|
+
const ic = spawnSync("icacls", [path, "/inheritance:r", "/grant:r", `*${sid}:${flags}`, "*S-1-5-18:F"], { encoding: "utf8" });
|
|
188
|
+
if (ic.status !== 0) throw new Error(ic.stderr);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function privateDir(path) {
|
|
192
|
+
noSymlinks(path);
|
|
193
|
+
const missing = [];
|
|
194
|
+
let p = path;
|
|
195
|
+
while (!existsSync(p)) {
|
|
196
|
+
missing.push(p);
|
|
197
|
+
const next = dirname(p);
|
|
198
|
+
if (next === p) break;
|
|
199
|
+
p = next;
|
|
200
|
+
}
|
|
201
|
+
for (const d of missing.reverse()) {
|
|
202
|
+
mkdirSync(d, { mode: 0o700 });
|
|
203
|
+
secure(d, true);
|
|
204
|
+
}
|
|
205
|
+
if (existsSync(path)) secure(path, true);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function atomicWrite(path, data, mode = 0o600, { exclusive = false } = {}) {
|
|
209
|
+
const buf = Buffer.isBuffer(data) ? data : Buffer.from(String(data), "utf8");
|
|
210
|
+
noSymlinks(path);
|
|
211
|
+
privateDir(dirname(path));
|
|
212
|
+
if (exclusive && existsSync(path)) throw new OpsError(`immutable artifact already exists: ${path}`);
|
|
213
|
+
const tmp = join(dirname(path), `.ops-write-${randomBytes(6).toString("hex")}`);
|
|
214
|
+
const fd = openSync(tmp, "w");
|
|
215
|
+
try {
|
|
216
|
+
writeSync(fd, buf);
|
|
217
|
+
fsyncSync(fd);
|
|
218
|
+
} finally {
|
|
219
|
+
closeSync(fd);
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
if (process.platform === "win32") secure(tmp);
|
|
223
|
+
else chmodSync(tmp, mode);
|
|
224
|
+
if (exclusive) {
|
|
225
|
+
try {
|
|
226
|
+
linkSync(tmp, path);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
if (error.code === "EEXIST") throw new OpsError(`immutable artifact already exists: ${path}`);
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
unlinkSync(tmp);
|
|
232
|
+
} else {
|
|
233
|
+
try {
|
|
234
|
+
renameSync(tmp, path);
|
|
235
|
+
} catch (error) {
|
|
236
|
+
if (process.platform === "win32" && (error.code === "EEXIST" || error.code === "EPERM" || error.code === "EACCES")) {
|
|
237
|
+
unlinkSync(path);
|
|
238
|
+
renameSync(tmp, path);
|
|
239
|
+
} else {
|
|
240
|
+
throw error;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (process.platform !== "win32") chmodSync(path, mode);
|
|
245
|
+
} catch (error) {
|
|
246
|
+
try { unlinkSync(tmp); } catch {}
|
|
247
|
+
throw error;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function writeJson(path, value, { exclusive = false } = {}) {
|
|
252
|
+
assertJsonSafe(value);
|
|
253
|
+
atomicWrite(path, `${JSON.stringify(value, null, 2)}\n`, 0o600, { exclusive });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function assertNoDuplicateKeys(text) {
|
|
257
|
+
let i = 0;
|
|
258
|
+
const n = text.length;
|
|
259
|
+
const skip = () => { while (i < n && /\s/.test(text[i])) i++; };
|
|
260
|
+
function parseString() {
|
|
261
|
+
i++;
|
|
262
|
+
while (i < n) {
|
|
263
|
+
if (text[i] === "\\") { i += 2; continue; }
|
|
264
|
+
if (text[i] === '"') { i++; return; }
|
|
265
|
+
i++;
|
|
266
|
+
}
|
|
267
|
+
throw new Error("Unterminated string");
|
|
268
|
+
}
|
|
269
|
+
function parseValue() {
|
|
270
|
+
skip();
|
|
271
|
+
const c = text[i];
|
|
272
|
+
if (c === "{") return parseObject();
|
|
273
|
+
if (c === "[") {
|
|
274
|
+
i++; skip();
|
|
275
|
+
if (text[i] === "]") { i++; return; }
|
|
276
|
+
while (true) {
|
|
277
|
+
parseValue(); skip();
|
|
278
|
+
if (text[i] === ",") { i++; continue; }
|
|
279
|
+
if (text[i] === "]") { i++; return; }
|
|
280
|
+
throw new Error("Invalid array");
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (c === '"') return parseString();
|
|
284
|
+
if (c === "t" || c === "f" || c === "n") { while (i < n && /[a-z]/.test(text[i])) i++; return; }
|
|
285
|
+
if (c === "-" || (c >= "0" && c <= "9")) { while (i < n && /[0-9eE+.\-]/.test(text[i])) i++; return; }
|
|
286
|
+
throw new Error("Invalid JSON");
|
|
287
|
+
}
|
|
288
|
+
function parseObject() {
|
|
289
|
+
i++; skip();
|
|
290
|
+
const seen = new Set();
|
|
291
|
+
if (text[i] === "}") { i++; return; }
|
|
292
|
+
while (true) {
|
|
293
|
+
skip();
|
|
294
|
+
if (text[i] !== '"') throw new Error("Expected key");
|
|
295
|
+
const start = i;
|
|
296
|
+
parseString();
|
|
297
|
+
const key = JSON.parse(text.slice(start, i));
|
|
298
|
+
if (seen.has(key)) throw new Error(`duplicate JSON key: ${key}`);
|
|
299
|
+
seen.add(key);
|
|
300
|
+
skip();
|
|
301
|
+
if (text[i] !== ":") throw new Error("Expected colon");
|
|
302
|
+
i++;
|
|
303
|
+
parseValue();
|
|
304
|
+
skip();
|
|
305
|
+
if (text[i] === ",") { i++; continue; }
|
|
306
|
+
if (text[i] === "}") { i++; return; }
|
|
307
|
+
throw new Error("Expected comma");
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
parseValue();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function readJson(path) {
|
|
314
|
+
noSymlinks(path, { allowMissing: false });
|
|
315
|
+
const text = readFileSync(path, "utf8");
|
|
316
|
+
try {
|
|
317
|
+
if (/\bNaN\b|\bInfinity\b/.test(text.replace(/"(?:\\.|[^"\\])*"/g, '""'))) {
|
|
318
|
+
throw new Error("nonfinite");
|
|
319
|
+
}
|
|
320
|
+
assertNoDuplicateKeys(text);
|
|
321
|
+
return JSON.parse(text);
|
|
322
|
+
} catch (error) {
|
|
323
|
+
throw new OpsError(`invalid JSON: ${path}: ${error.message}`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function withLock(path, owner, fn) {
|
|
328
|
+
privateDir(dirname(path));
|
|
329
|
+
try {
|
|
330
|
+
mkdirSync(path, { mode: 0o700 });
|
|
331
|
+
} catch (error) {
|
|
332
|
+
if (error.code === "EEXIST") throw new OpsError(`lock-held: ${path}; inspect owner, never auto-break`);
|
|
333
|
+
throw error;
|
|
334
|
+
}
|
|
335
|
+
writeJson(join(path, "owner.json"), { ...owner, pid: process.pid, machine: hostname(), at: now() });
|
|
336
|
+
try {
|
|
337
|
+
return fn();
|
|
338
|
+
} finally {
|
|
339
|
+
try { unlinkSync(join(path, "owner.json")); } catch {}
|
|
340
|
+
try { rmSync(path, { recursive: true, force: true }); } catch {}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function redact(text, secrets) {
|
|
345
|
+
for (const value of [...new Set(secrets)].sort((a, b) => b.length - a.length)) {
|
|
346
|
+
if (value) text = text.split(value).join("[REDACTED]");
|
|
347
|
+
}
|
|
348
|
+
return text.replace(/(password|passwd|token|secret|access_key)(\s*[=:]\s*)[^\s,;]+/gi, "$1$2[REDACTED]");
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export function credentialsIn(value) {
|
|
352
|
+
const found = new Set();
|
|
353
|
+
const re = new RegExp(SECRET_RE.source, "g");
|
|
354
|
+
let m;
|
|
355
|
+
const blob = JSON.stringify(value);
|
|
356
|
+
while ((m = re.exec(blob))) found.add(`${m[1]}@${m[2]}`);
|
|
357
|
+
return found;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function resolveSecrets(value, ledger) {
|
|
361
|
+
if (typeof value === "string") {
|
|
362
|
+
return value.replace(new RegExp(SECRET_RE.source, "g"), (_w, cid, version, field) => {
|
|
363
|
+
const result = ledger?.entries?.[cid]?.[version]?.values?.[field];
|
|
364
|
+
if (result === undefined) throw new OpsError(`missing credential: ${cid}@${version}:${field}`);
|
|
365
|
+
if (typeof result !== "string") throw new OpsError("credential values must be strings");
|
|
366
|
+
return result;
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
if (Array.isArray(value)) return value.map((v) => resolveSecrets(v, ledger));
|
|
370
|
+
if (value && typeof value === "object") {
|
|
371
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, resolveSecrets(v, ledger)]));
|
|
372
|
+
}
|
|
373
|
+
return value;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export function emptyStatus() {
|
|
377
|
+
return {
|
|
378
|
+
schema_version: 3,
|
|
379
|
+
workflow: "ops",
|
|
380
|
+
revision: 0,
|
|
381
|
+
controller: null,
|
|
382
|
+
hosts: {},
|
|
383
|
+
projects: {},
|
|
384
|
+
deployments: {},
|
|
385
|
+
allocations: {},
|
|
386
|
+
bindings: {},
|
|
387
|
+
releases: {},
|
|
388
|
+
policies: { server_readme_credentials: false, server_operations: true, strict_docker_root: true },
|
|
389
|
+
updated_at: null,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export { withLock as lock };
|