@matthesketh/fleet 1.8.0 → 1.11.0

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.
Files changed (233) hide show
  1. package/README.md +186 -16
  2. package/dist/bin/fleet-agent.d.ts +2 -0
  3. package/dist/bin/fleet-agent.js +7 -0
  4. package/dist/cli.d.ts +5 -0
  5. package/dist/cli.js +73 -31
  6. package/dist/commands/add.d.ts +2 -1
  7. package/dist/commands/add.js +66 -59
  8. package/dist/commands/audit.d.ts +1 -0
  9. package/dist/commands/audit.js +144 -0
  10. package/dist/commands/backup.d.ts +1 -0
  11. package/dist/commands/backup.js +510 -0
  12. package/dist/commands/boot-start.d.ts +3 -1
  13. package/dist/commands/boot-start.js +39 -47
  14. package/dist/commands/completions.d.ts +6 -0
  15. package/dist/commands/completions.js +83 -0
  16. package/dist/commands/config.d.ts +16 -0
  17. package/dist/commands/config.js +96 -0
  18. package/dist/commands/deploy.js +3 -2
  19. package/dist/commands/deps.js +5 -1
  20. package/dist/commands/doctor.d.ts +32 -0
  21. package/dist/commands/doctor.js +186 -0
  22. package/dist/commands/egress.d.ts +1 -1
  23. package/dist/commands/egress.js +13 -10
  24. package/dist/commands/freeze.d.ts +8 -4
  25. package/dist/commands/freeze.js +77 -59
  26. package/dist/commands/git.js +2 -2
  27. package/dist/commands/health.d.ts +2 -1
  28. package/dist/commands/health.js +38 -56
  29. package/dist/commands/init.d.ts +2 -1
  30. package/dist/commands/init.js +83 -73
  31. package/dist/commands/install-mcp.d.ts +3 -1
  32. package/dist/commands/install-mcp.js +53 -34
  33. package/dist/commands/list.d.ts +2 -1
  34. package/dist/commands/list.js +22 -19
  35. package/dist/commands/logs.js +1 -1
  36. package/dist/commands/notify.d.ts +1 -0
  37. package/dist/commands/notify.js +51 -0
  38. package/dist/commands/patch-systemd.d.ts +7 -1
  39. package/dist/commands/patch-systemd.js +71 -31
  40. package/dist/commands/remove.d.ts +3 -1
  41. package/dist/commands/remove.js +37 -26
  42. package/dist/commands/restart.d.ts +4 -1
  43. package/dist/commands/restart.js +17 -20
  44. package/dist/commands/rollback.d.ts +4 -1
  45. package/dist/commands/rollback.js +33 -42
  46. package/dist/commands/secrets.js +157 -9
  47. package/dist/commands/start.d.ts +4 -1
  48. package/dist/commands/start.js +17 -20
  49. package/dist/commands/status.d.ts +1 -1
  50. package/dist/commands/status.js +21 -26
  51. package/dist/commands/stop.d.ts +4 -1
  52. package/dist/commands/stop.js +17 -20
  53. package/dist/commands/testflight.d.ts +1 -0
  54. package/dist/commands/testflight.js +193 -0
  55. package/dist/commands/update.d.ts +16 -0
  56. package/dist/commands/update.js +95 -0
  57. package/dist/core/audit/cache.d.ts +4 -0
  58. package/dist/core/audit/cache.js +37 -0
  59. package/dist/core/audit/config.d.ts +5 -0
  60. package/dist/core/audit/config.js +35 -0
  61. package/dist/core/audit/greenlight.d.ts +11 -0
  62. package/dist/core/audit/greenlight.js +81 -0
  63. package/dist/core/audit/reporters/cli.d.ts +3 -0
  64. package/dist/core/audit/reporters/cli.js +68 -0
  65. package/dist/core/audit/suppress.d.ts +6 -0
  66. package/dist/core/audit/suppress.js +37 -0
  67. package/dist/core/audit/target.d.ts +5 -0
  68. package/dist/core/audit/target.js +26 -0
  69. package/dist/core/audit/types.d.ts +54 -0
  70. package/dist/core/audit/types.js +5 -0
  71. package/dist/core/backup/browser-api.d.ts +66 -0
  72. package/dist/core/backup/browser-api.js +197 -0
  73. package/dist/core/backup/browser-server.d.ts +11 -0
  74. package/dist/core/backup/browser-server.js +241 -0
  75. package/dist/core/backup/browser-ui.d.ts +5 -0
  76. package/dist/core/backup/browser-ui.js +268 -0
  77. package/dist/core/backup/cloudflare.d.ts +7 -0
  78. package/dist/core/backup/cloudflare.js +82 -0
  79. package/dist/core/backup/config.d.ts +9 -0
  80. package/dist/core/backup/config.js +80 -0
  81. package/dist/core/backup/detect.d.ts +11 -0
  82. package/dist/core/backup/detect.js +71 -0
  83. package/dist/core/backup/dump.d.ts +11 -0
  84. package/dist/core/backup/dump.js +82 -0
  85. package/dist/core/backup/index.d.ts +9 -0
  86. package/dist/core/backup/index.js +9 -0
  87. package/dist/core/backup/repo.d.ts +71 -0
  88. package/dist/core/backup/repo.js +256 -0
  89. package/dist/core/backup/schedule.d.ts +17 -0
  90. package/dist/core/backup/schedule.js +90 -0
  91. package/dist/core/backup/sensitive.d.ts +5 -0
  92. package/dist/core/backup/sensitive.js +37 -0
  93. package/dist/core/backup/status.d.ts +3 -0
  94. package/dist/core/backup/status.js +29 -0
  95. package/dist/core/backup/statuspage.d.ts +23 -0
  96. package/dist/core/backup/statuspage.js +145 -0
  97. package/dist/core/backup/system.d.ts +24 -0
  98. package/dist/core/backup/system.js +209 -0
  99. package/dist/core/backup/totp.d.ts +16 -0
  100. package/dist/core/backup/totp.js +116 -0
  101. package/dist/core/backup/types.d.ts +70 -0
  102. package/dist/core/backup/types.js +7 -0
  103. package/dist/core/backup/unlock.d.ts +19 -0
  104. package/dist/core/backup/unlock.js +69 -0
  105. package/dist/core/boot-refresh.d.ts +1 -1
  106. package/dist/core/boot-refresh.js +10 -9
  107. package/dist/core/deps/actors/pr-creator.d.ts +5 -3
  108. package/dist/core/deps/actors/pr-creator.js +71 -18
  109. package/dist/core/deps/collectors/fetch-with-timeout.d.ts +7 -0
  110. package/dist/core/deps/collectors/fetch-with-timeout.js +16 -0
  111. package/dist/core/deps/collectors/npm.js +3 -1
  112. package/dist/core/deps/collectors/vulnerability.d.ts +8 -0
  113. package/dist/core/deps/collectors/vulnerability.js +31 -2
  114. package/dist/core/deps/config.js +6 -0
  115. package/dist/core/deps/scanner.js +1 -1
  116. package/dist/core/deps/types.d.ts +8 -0
  117. package/dist/core/env.d.ts +3 -0
  118. package/dist/core/env.js +11 -0
  119. package/dist/core/exec.d.ts +1 -0
  120. package/dist/core/exec.js +4 -0
  121. package/dist/core/file-lock.d.ts +18 -0
  122. package/dist/core/file-lock.js +44 -0
  123. package/dist/core/git-onboard.js +10 -13
  124. package/dist/core/github.d.ts +3 -1
  125. package/dist/core/github.js +10 -7
  126. package/dist/core/logs-policy.d.ts +5 -0
  127. package/dist/core/logs-policy.js +20 -1
  128. package/dist/core/operator.d.ts +21 -0
  129. package/dist/core/operator.js +54 -0
  130. package/dist/core/registry.d.ts +18 -0
  131. package/dist/core/registry.js +26 -0
  132. package/dist/core/routines/schema.d.ts +11 -11
  133. package/dist/core/routines/schema.js +14 -3
  134. package/dist/core/routines/store.d.ts +8 -8
  135. package/dist/core/secrets-ops.d.ts +31 -6
  136. package/dist/core/secrets-ops.js +208 -102
  137. package/dist/core/secrets-providers.js +2 -2
  138. package/dist/core/secrets-rotation.d.ts +1 -1
  139. package/dist/core/secrets-rotation.js +58 -52
  140. package/dist/core/secrets-v2-cleanup.d.ts +19 -0
  141. package/dist/core/secrets-v2-cleanup.js +94 -0
  142. package/dist/core/secrets-v2-creds.d.ts +9 -0
  143. package/dist/core/secrets-v2-creds.js +44 -0
  144. package/dist/core/secrets-v2-install.d.ts +13 -0
  145. package/dist/core/secrets-v2-install.js +76 -0
  146. package/dist/core/secrets-v2-keypair.d.ts +10 -0
  147. package/dist/core/secrets-v2-keypair.js +31 -0
  148. package/dist/core/secrets-v2-migrate.d.ts +29 -0
  149. package/dist/core/secrets-v2-migrate.js +395 -0
  150. package/dist/core/secrets-v2-ops.d.ts +36 -0
  151. package/dist/core/secrets-v2-ops.js +184 -0
  152. package/dist/core/secrets-v2-protocol.d.ts +19 -0
  153. package/dist/core/secrets-v2-protocol.js +60 -0
  154. package/dist/core/secrets-v2-snapshot.d.ts +36 -0
  155. package/dist/core/secrets-v2-snapshot.js +115 -0
  156. package/dist/core/secrets-v2.d.ts +21 -0
  157. package/dist/core/secrets-v2.js +249 -0
  158. package/dist/core/secrets.d.ts +39 -4
  159. package/dist/core/secrets.js +91 -11
  160. package/dist/core/self-update.d.ts +32 -11
  161. package/dist/core/self-update.js +52 -14
  162. package/dist/core/testflight/asc.d.ts +12 -0
  163. package/dist/core/testflight/asc.js +101 -0
  164. package/dist/core/testflight/credentials.d.ts +3 -0
  165. package/dist/core/testflight/credentials.js +35 -0
  166. package/dist/core/testflight/eas.d.ts +4 -0
  167. package/dist/core/testflight/eas.js +38 -0
  168. package/dist/core/testflight/resolve.d.ts +6 -0
  169. package/dist/core/testflight/resolve.js +44 -0
  170. package/dist/core/testflight/types.d.ts +13 -0
  171. package/dist/core/testflight/types.js +3 -0
  172. package/dist/core/testflight/workflow.d.ts +17 -0
  173. package/dist/core/testflight/workflow.js +65 -0
  174. package/dist/core/validate.d.ts +1 -0
  175. package/dist/core/validate.js +8 -0
  176. package/dist/mcp/audit-tools.d.ts +2 -0
  177. package/dist/mcp/audit-tools.js +94 -0
  178. package/dist/mcp/git-tools.js +1 -1
  179. package/dist/mcp/registry-bridge.d.ts +10 -0
  180. package/dist/mcp/registry-bridge.js +65 -0
  181. package/dist/mcp/secrets-tools.js +2 -2
  182. package/dist/mcp/server.js +16 -82
  183. package/dist/mcp/testflight-tools.d.ts +2 -0
  184. package/dist/mcp/testflight-tools.js +52 -0
  185. package/dist/registry/context.d.ts +7 -0
  186. package/dist/registry/context.js +37 -0
  187. package/dist/registry/index.d.ts +5 -0
  188. package/dist/registry/index.js +44 -0
  189. package/dist/registry/parse-args.d.ts +13 -0
  190. package/dist/registry/parse-args.js +74 -0
  191. package/dist/registry/registry.d.ts +24 -0
  192. package/dist/registry/registry.js +26 -0
  193. package/dist/registry/render.d.ts +3 -0
  194. package/dist/registry/render.js +29 -0
  195. package/dist/registry/types.d.ts +50 -0
  196. package/dist/registry/types.js +1 -0
  197. package/dist/templates/agent-unit.d.ts +5 -0
  198. package/dist/templates/agent-unit.js +40 -0
  199. package/dist/templates/app-unit-edit.d.ts +2 -0
  200. package/dist/templates/app-unit-edit.js +46 -0
  201. package/dist/templates/compose-edit.d.ts +2 -0
  202. package/dist/templates/compose-edit.js +156 -0
  203. package/dist/templates/nginx.js +11 -0
  204. package/dist/templates/systemd.js +6 -0
  205. package/dist/tui/components/ArgForm.d.ts +7 -0
  206. package/dist/tui/components/ArgForm.js +64 -0
  207. package/dist/tui/components/ArgForm.test.d.ts +1 -0
  208. package/dist/tui/components/ArgForm.test.js +19 -0
  209. package/dist/tui/components/KeyHint.js +5 -0
  210. package/dist/tui/hooks/use-secrets.d.ts +8 -8
  211. package/dist/tui/hooks/use-secrets.js +7 -7
  212. package/dist/tui/router.d.ts +1 -0
  213. package/dist/tui/router.js +26 -9
  214. package/dist/tui/router.test.d.ts +1 -0
  215. package/dist/tui/router.test.js +13 -0
  216. package/dist/tui/routines/components/SignalsGrid.test.js +2 -2
  217. package/dist/tui/routines/tabs/ScaffoldTab.js +1 -1
  218. package/dist/tui/tests/redaction-rerender.test.d.ts +1 -0
  219. package/dist/tui/tests/redaction-rerender.test.js +53 -0
  220. package/dist/tui/tests/scroll-flicker-proof.test.d.ts +1 -0
  221. package/dist/tui/tests/scroll-flicker-proof.test.js +145 -0
  222. package/dist/tui/types.d.ts +1 -1
  223. package/dist/tui/views/CommandPalette.d.ts +5 -0
  224. package/dist/tui/views/CommandPalette.js +90 -0
  225. package/dist/tui/views/CommandPalette.test.d.ts +1 -0
  226. package/dist/tui/views/CommandPalette.test.js +117 -0
  227. package/dist/tui/views/Dashboard.js +10 -7
  228. package/dist/tui/views/HealthView.js +14 -5
  229. package/dist/tui/views/SecretEdit.js +15 -16
  230. package/dist/tui/views/SecretEdit.test.d.ts +1 -0
  231. package/dist/tui/views/SecretEdit.test.js +82 -0
  232. package/dist/tui/views/SecretsView.js +26 -16
  233. package/package.json +9 -6
@@ -3,7 +3,7 @@
3
3
  * compose override file (.fleet/logging.override.yml) per app, plus journald
4
4
  * vacuum policy. Conservative defaults applied when unset.
5
5
  */
6
- import { existsSync, mkdirSync, writeFileSync, statSync } from 'node:fs';
6
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, statSync } from 'node:fs';
7
7
  import { join, dirname } from 'node:path';
8
8
  import { execSafe } from './exec.js';
9
9
  export const DEFAULT_POLICY = {
@@ -50,12 +50,31 @@ export function buildComposeOverride(app, policy) {
50
50
  export function overridePath(app) {
51
51
  return join(app.composePath, '.fleet', 'logging.override.yml');
52
52
  }
53
+ /** ensure the app repo's .gitignore covers .fleet/ so the auto-generated
54
+ * override file doesn't get committed accidentally. no-op when there's no
55
+ * .gitignore (operator may be using a different vcs or none at all) and
56
+ * idempotent — never appends a duplicate entry. */
57
+ export function ensureFleetGitignored(composePath) {
58
+ const giPath = join(composePath, '.gitignore');
59
+ if (!existsSync(giPath))
60
+ return;
61
+ const current = readFileSync(giPath, 'utf-8');
62
+ // accept any of: .fleet, .fleet/, /.fleet, /.fleet/ — operators write all
63
+ // four forms.
64
+ if (/^\s*\/?\.fleet\/?\s*$/m.test(current))
65
+ return;
66
+ const append = current.endsWith('\n') ? '' : '\n';
67
+ writeFileSync(giPath, `${current}${append}\n# auto-added by fleet logs setup\n.fleet/\n`);
68
+ }
53
69
  export function writeComposeOverride(app, policy) {
54
70
  const path = overridePath(app);
55
71
  const dir = dirname(path);
56
72
  if (!existsSync(dir))
57
73
  mkdirSync(dir, { recursive: true });
58
74
  writeFileSync(path, buildComposeOverride(app, policy));
75
+ // the override lands inside the operator's app repo. add .fleet/ to the
76
+ // app's .gitignore (if there is one) so it doesn't get committed.
77
+ ensureFleetGitignored(app.composePath);
59
78
  return path;
60
79
  }
61
80
  /** Best-effort docker-side log status. Uses `docker inspect` for driver + size. */
@@ -0,0 +1,21 @@
1
+ export interface OperatorConfig {
2
+ username: string;
3
+ homeDir: string;
4
+ domain: string;
5
+ githubOrg: string;
6
+ }
7
+ export type OperatorField = keyof OperatorConfig;
8
+ export declare const OPERATOR_FIELDS: readonly OperatorField[];
9
+ /** test-only: clears the memoised config. */
10
+ export declare function _resetOperatorCache(): void;
11
+ /** path the operator config is read from / written to. exported so the
12
+ * fleet config command can print where it lives. */
13
+ export declare function operatorPath(): string;
14
+ /** loads operator identity from data/operator.json (gitignored, instance-local).
15
+ * throws if the file is missing or incomplete — there is no safe default,
16
+ * and guessing another operator's identity is never correct. */
17
+ export declare function loadOperator(): OperatorConfig;
18
+ /** persist the operator config to disk and clear the memoised copy so the
19
+ * next loadOperator() picks up the new values. atomic write via .tmp +
20
+ * rename so a crash mid-write never leaves a partial file behind. */
21
+ export declare function saveOperator(cfg: OperatorConfig): void;
@@ -0,0 +1,54 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { FleetError } from './errors.js';
5
+ const here = dirname(fileURLToPath(import.meta.url));
6
+ export const OPERATOR_FIELDS = ['username', 'homeDir', 'domain', 'githubOrg'];
7
+ const FIELDS = OPERATOR_FIELDS;
8
+ let cache = null;
9
+ /** test-only: clears the memoised config. */
10
+ export function _resetOperatorCache() { cache = null; }
11
+ /** path the operator config is read from / written to. exported so the
12
+ * fleet config command can print where it lives. */
13
+ export function operatorPath() {
14
+ return process.env.FLEET_OPERATOR_PATH ?? join(here, '..', '..', 'data', 'operator.json');
15
+ }
16
+ /** loads operator identity from data/operator.json (gitignored, instance-local).
17
+ * throws if the file is missing or incomplete — there is no safe default,
18
+ * and guessing another operator's identity is never correct. */
19
+ export function loadOperator() {
20
+ if (cache)
21
+ return cache;
22
+ const path = operatorPath();
23
+ if (!existsSync(path)) {
24
+ throw new FleetError(`operator config not found at ${path} — ` +
25
+ `copy data/operator.example.json to data/operator.json and fill it in`);
26
+ }
27
+ const raw = JSON.parse(readFileSync(path, 'utf-8'));
28
+ for (const field of FIELDS) {
29
+ if (!raw[field])
30
+ throw new FleetError(`operator config ${path} is missing field: ${field}`);
31
+ }
32
+ cache = raw;
33
+ return cache;
34
+ }
35
+ /** persist the operator config to disk and clear the memoised copy so the
36
+ * next loadOperator() picks up the new values. atomic write via .tmp +
37
+ * rename so a crash mid-write never leaves a partial file behind. */
38
+ export function saveOperator(cfg) {
39
+ for (const field of FIELDS) {
40
+ if (typeof cfg[field] !== 'string' || cfg[field].length === 0) {
41
+ throw new FleetError(`operator config: ${field} must be a non-empty string`);
42
+ }
43
+ }
44
+ const path = operatorPath();
45
+ const dir = dirname(path);
46
+ if (!existsSync(dir))
47
+ mkdirSync(dir, { recursive: true });
48
+ const tmp = path + '.tmp';
49
+ writeFileSync(tmp, JSON.stringify(cfg, null, 2) + '\n', { mode: 0o600 });
50
+ // rename is atomic on the same filesystem; covers the crash-mid-write race
51
+ // a plain writeFileSync exposes.
52
+ renameSync(tmp, path);
53
+ cache = null;
54
+ }
@@ -61,3 +61,21 @@ export declare function findApp(reg: Registry, name: string): AppEntry | undefin
61
61
  export declare function addApp(reg: Registry, app: AppEntry): Registry;
62
62
  export declare function removeApp(reg: Registry, name: string): Registry;
63
63
  export declare function registryPath(): string;
64
+ /**
65
+ * Run a read-modify-write transaction against the registry under an
66
+ * inter-process lock. The lock is held for the full load → mutate → save
67
+ * cycle, so concurrent CLI / cron / systemd / bot invocations don't lose
68
+ * each other's updates.
69
+ *
70
+ * The mutator may return a different Registry object (e.g. one returned by
71
+ * `addApp` / `removeApp`, which mutate in place but also return the registry
72
+ * for chaining) or simply mutate the input and return it. The returned value
73
+ * is what gets persisted.
74
+ *
75
+ * Returns void: callers needing the post-save state should re-load. Keeping
76
+ * this side-effecting matches how `load()` + `save()` are used today.
77
+ *
78
+ * Important: do not call this from inside another `withRegistry` block on the
79
+ * same process — proper-lockfile is not reentrant and will deadlock.
80
+ */
81
+ export declare function withRegistry(fn: (reg: Registry) => Registry | Promise<Registry>): Promise<void>;
@@ -1,6 +1,7 @@
1
1
  import { readFileSync, existsSync, mkdirSync, copyFileSync, renameSync, openSync, writeSync, fsyncSync, closeSync } from 'node:fs';
2
2
  import { dirname, join } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
+ import { withFileLock } from './file-lock.js';
4
5
  const __dirname = dirname(fileURLToPath(import.meta.url));
5
6
  function resolveRegistryPath() {
6
7
  return process.env.FLEET_REGISTRY_PATH
@@ -92,3 +93,28 @@ export function removeApp(reg, name) {
92
93
  export function registryPath() {
93
94
  return resolveRegistryPath();
94
95
  }
96
+ /**
97
+ * Run a read-modify-write transaction against the registry under an
98
+ * inter-process lock. The lock is held for the full load → mutate → save
99
+ * cycle, so concurrent CLI / cron / systemd / bot invocations don't lose
100
+ * each other's updates.
101
+ *
102
+ * The mutator may return a different Registry object (e.g. one returned by
103
+ * `addApp` / `removeApp`, which mutate in place but also return the registry
104
+ * for chaining) or simply mutate the input and return it. The returned value
105
+ * is what gets persisted.
106
+ *
107
+ * Returns void: callers needing the post-save state should re-load. Keeping
108
+ * this side-effecting matches how `load()` + `save()` are used today.
109
+ *
110
+ * Important: do not call this from inside another `withRegistry` block on the
111
+ * same process — proper-lockfile is not reentrant and will deadlock.
112
+ */
113
+ export async function withRegistry(fn) {
114
+ const path = resolveRegistryPath();
115
+ await withFileLock(path, async () => {
116
+ const reg = load();
117
+ const next = await fn(reg);
118
+ save(next);
119
+ });
120
+ }
@@ -204,15 +204,14 @@ export declare const RoutineSchema: z.ZodObject<{
204
204
  createdAt: z.ZodOptional<z.ZodString>;
205
205
  updatedAt: z.ZodOptional<z.ZodString>;
206
206
  }, "strip", z.ZodTypeAny, {
207
- enabled: boolean;
208
207
  name: string;
208
+ enabled: boolean;
209
209
  id: string;
210
210
  notify: {
211
211
  config: Record<string, unknown>;
212
212
  kind: "email" | "stdout" | "webhook" | "slack";
213
213
  on: "always" | "failure" | "success";
214
214
  }[];
215
- description: string;
216
215
  schedule: {
217
216
  kind: "manual";
218
217
  } | {
@@ -221,6 +220,8 @@ export declare const RoutineSchema: z.ZodObject<{
221
220
  randomizedDelaySec: number;
222
221
  persistent: boolean;
223
222
  };
223
+ tags: string[];
224
+ description: string;
224
225
  targets: string[];
225
226
  perTarget: boolean;
226
227
  task: {
@@ -244,7 +245,6 @@ export declare const RoutineSchema: z.ZodObject<{
244
245
  tool: string;
245
246
  args: Record<string, unknown>;
246
247
  };
247
- tags: string[];
248
248
  updatedAt?: string | undefined;
249
249
  createdAt?: string | undefined;
250
250
  }, {
@@ -286,10 +286,10 @@ export declare const RoutineSchema: z.ZodObject<{
286
286
  config?: Record<string, unknown> | undefined;
287
287
  on?: "always" | "failure" | "success" | undefined;
288
288
  }[] | undefined;
289
+ tags?: string[] | undefined;
289
290
  description?: string | undefined;
290
291
  targets?: string[] | undefined;
291
292
  perTarget?: boolean | undefined;
292
- tags?: string[] | undefined;
293
293
  createdAt?: string | undefined;
294
294
  }>;
295
295
  export type Routine = z.infer<typeof RoutineSchema>;
@@ -303,13 +303,13 @@ export declare const RunEventSchema: z.ZodDiscriminatedUnion<"kind", [z.ZodObjec
303
303
  }, "strip", z.ZodTypeAny, {
304
304
  at: string;
305
305
  kind: "start";
306
- routineId: string;
307
306
  target: string | null;
307
+ routineId: string;
308
308
  }, {
309
309
  at: string;
310
310
  kind: "start";
311
- routineId: string;
312
311
  target: string | null;
312
+ routineId: string;
313
313
  }>, z.ZodObject<{
314
314
  kind: z.ZodLiteral<"stdout">;
315
315
  chunk: z.ZodString;
@@ -370,14 +370,14 @@ export declare const RunEventSchema: z.ZodDiscriminatedUnion<"kind", [z.ZodObjec
370
370
  error: z.ZodOptional<z.ZodString>;
371
371
  }, "strip", z.ZodTypeAny, {
372
372
  at: string;
373
- status: "timeout" | "ok" | "failed" | "aborted" | "queued" | "running";
373
+ status: "aborted" | "timeout" | "ok" | "queued" | "running" | "failed";
374
374
  kind: "end";
375
375
  exitCode: number;
376
376
  durationMs: number;
377
377
  error?: string | undefined;
378
378
  }, {
379
379
  at: string;
380
- status: "timeout" | "ok" | "failed" | "aborted" | "queued" | "running";
380
+ status: "aborted" | "timeout" | "ok" | "queued" | "running" | "failed";
381
381
  kind: "end";
382
382
  exitCode: number;
383
383
  durationMs: number;
@@ -397,16 +397,16 @@ export declare const SignalSchema: z.ZodObject<{
397
397
  collectedAt: z.ZodString;
398
398
  ttlMs: z.ZodNumber;
399
399
  }, "strip", z.ZodTypeAny, {
400
- detail: string;
401
- kind: "git-clean" | "git-ahead" | "git-behind" | "open-prs" | "pr-age-max" | "deps-outdated" | "deps-vulns" | "build-ok" | "tests-ok" | "env-schema-ok" | "container-up" | "ci-status" | "cache-age";
402
400
  value: string | number | boolean | null;
401
+ kind: "git-clean" | "git-ahead" | "git-behind" | "open-prs" | "pr-age-max" | "deps-outdated" | "deps-vulns" | "build-ok" | "tests-ok" | "env-schema-ok" | "container-up" | "ci-status" | "cache-age";
402
+ detail: string;
403
403
  repo: string;
404
404
  state: "warn" | "error" | "unknown" | "ok";
405
405
  collectedAt: string;
406
406
  ttlMs: number;
407
407
  }, {
408
- kind: "git-clean" | "git-ahead" | "git-behind" | "open-prs" | "pr-age-max" | "deps-outdated" | "deps-vulns" | "build-ok" | "tests-ok" | "env-schema-ok" | "container-up" | "ci-status" | "cache-age";
409
408
  value: string | number | boolean | null;
409
+ kind: "git-clean" | "git-ahead" | "git-behind" | "open-prs" | "pr-age-max" | "deps-outdated" | "deps-vulns" | "build-ok" | "tests-ok" | "env-schema-ok" | "container-up" | "ci-status" | "cache-age";
410
410
  repo: string;
411
411
  state: "warn" | "error" | "unknown" | "ok";
412
412
  collectedAt: string;
@@ -1,6 +1,17 @@
1
1
  import { z } from 'zod';
2
2
  const ROUTINE_ID_REGEX = /^[a-z][a-z0-9-]{0,62}$/;
3
3
  const NO_SHELL_META = /^[^`$;&|><\n\r\\"]*$/;
4
+ // Printable ASCII only — no newlines, no control chars. Routine names and
5
+ // descriptions are interpolated into systemd unit files; a newline would let
6
+ // a hostile name inject directives like [Service]/User=root/ExecStart=…
7
+ // that the systemd loader then runs as root after daemon-reload. Keep this
8
+ // strict — widen explicitly if a future feature truly needs more.
9
+ const ROUTINE_TEXT_REGEX = /^[\x20-\x7E]+$/;
10
+ // systemd's documented OnCalendar grammar: weekdays, dates, times — all
11
+ // printable ASCII without quotes, semicolons, control chars or newlines.
12
+ // Same injection concern as above. Widen if "~" (last weekday) etc. is
13
+ // actually needed.
14
+ const ON_CALENDAR_REGEX = /^[A-Za-z0-9*\-/:., \t]+$/;
4
15
  const DEFAULT_WALLCLOCK_MS = 15 * 60 * 1000;
5
16
  const DEFAULT_TOKEN_CAP = 100_000;
6
17
  const DEFAULT_MAX_USD = 5;
@@ -38,15 +49,15 @@ export const RoutineScheduleSchema = z.union([
38
49
  z.object({ kind: z.literal('manual') }),
39
50
  z.object({
40
51
  kind: z.literal('calendar'),
41
- onCalendar: z.string().min(1).max(200),
52
+ onCalendar: z.string().min(1).max(200).regex(ON_CALENDAR_REGEX, 'systemd OnCalendar tokens only (letters, digits, *-/:., space, tab)'),
42
53
  randomizedDelaySec: z.number().int().nonnegative().max(3600).default(0),
43
54
  persistent: z.boolean().default(true),
44
55
  }),
45
56
  ]);
46
57
  export const RoutineSchema = z.object({
47
58
  id: z.string().regex(ROUTINE_ID_REGEX, 'lowercase alphanumeric and dashes only'),
48
- name: z.string().min(1).max(100),
49
- description: z.string().max(2000).default(''),
59
+ name: z.string().min(1).max(100).regex(ROUTINE_TEXT_REGEX, 'printable ASCII only, no newlines or control chars'),
60
+ description: z.string().max(2000).regex(/^[\x20-\x7E]*$/, 'printable ASCII only, no newlines or control chars').default(''),
50
61
  schedule: RoutineScheduleSchema,
51
62
  enabled: z.boolean().default(true),
52
63
  targets: z.array(z.string().min(1)).default([]),
@@ -109,15 +109,14 @@ declare const FileSchema: z.ZodObject<{
109
109
  createdAt: z.ZodOptional<z.ZodString>;
110
110
  updatedAt: z.ZodOptional<z.ZodString>;
111
111
  }, "strip", z.ZodTypeAny, {
112
- enabled: boolean;
113
112
  name: string;
113
+ enabled: boolean;
114
114
  id: string;
115
115
  notify: {
116
116
  config: Record<string, unknown>;
117
117
  kind: "email" | "stdout" | "webhook" | "slack";
118
118
  on: "always" | "failure" | "success";
119
119
  }[];
120
- description: string;
121
120
  schedule: {
122
121
  kind: "manual";
123
122
  } | {
@@ -126,6 +125,8 @@ declare const FileSchema: z.ZodObject<{
126
125
  randomizedDelaySec: number;
127
126
  persistent: boolean;
128
127
  };
128
+ tags: string[];
129
+ description: string;
129
130
  targets: string[];
130
131
  perTarget: boolean;
131
132
  task: {
@@ -149,7 +150,6 @@ declare const FileSchema: z.ZodObject<{
149
150
  tool: string;
150
151
  args: Record<string, unknown>;
151
152
  };
152
- tags: string[];
153
153
  updatedAt?: string | undefined;
154
154
  createdAt?: string | undefined;
155
155
  }, {
@@ -191,25 +191,24 @@ declare const FileSchema: z.ZodObject<{
191
191
  config?: Record<string, unknown> | undefined;
192
192
  on?: "always" | "failure" | "success" | undefined;
193
193
  }[] | undefined;
194
+ tags?: string[] | undefined;
194
195
  description?: string | undefined;
195
196
  targets?: string[] | undefined;
196
197
  perTarget?: boolean | undefined;
197
- tags?: string[] | undefined;
198
198
  createdAt?: string | undefined;
199
199
  }>, "many">;
200
200
  defaultsSeededAt: z.ZodOptional<z.ZodString>;
201
201
  }, "strip", z.ZodTypeAny, {
202
202
  version: 1;
203
203
  routines: {
204
- enabled: boolean;
205
204
  name: string;
205
+ enabled: boolean;
206
206
  id: string;
207
207
  notify: {
208
208
  config: Record<string, unknown>;
209
209
  kind: "email" | "stdout" | "webhook" | "slack";
210
210
  on: "always" | "failure" | "success";
211
211
  }[];
212
- description: string;
213
212
  schedule: {
214
213
  kind: "manual";
215
214
  } | {
@@ -218,6 +217,8 @@ declare const FileSchema: z.ZodObject<{
218
217
  randomizedDelaySec: number;
219
218
  persistent: boolean;
220
219
  };
220
+ tags: string[];
221
+ description: string;
221
222
  targets: string[];
222
223
  perTarget: boolean;
223
224
  task: {
@@ -241,7 +242,6 @@ declare const FileSchema: z.ZodObject<{
241
242
  tool: string;
242
243
  args: Record<string, unknown>;
243
244
  };
244
- tags: string[];
245
245
  updatedAt?: string | undefined;
246
246
  createdAt?: string | undefined;
247
247
  }[];
@@ -287,10 +287,10 @@ declare const FileSchema: z.ZodObject<{
287
287
  config?: Record<string, unknown> | undefined;
288
288
  on?: "always" | "failure" | "success" | undefined;
289
289
  }[] | undefined;
290
+ tags?: string[] | undefined;
290
291
  description?: string | undefined;
291
292
  targets?: string[] | undefined;
292
293
  perTarget?: boolean | undefined;
293
- tags?: string[] | undefined;
294
294
  createdAt?: string | undefined;
295
295
  }[];
296
296
  defaultsSeededAt?: string | undefined;
@@ -8,10 +8,15 @@ export declare function safeSealApp(app: string, content: string, sourceFile: st
8
8
  export declare function safeSealDbSecrets(app: string, secretsMap: Record<string, string>, sourceDir: string): SealValidation;
9
9
  export declare function setSecret(app: string, key: string, value: string, opts?: {
10
10
  allowWeak?: boolean;
11
- }): void;
11
+ }): Promise<void>;
12
+ /** read a single secret value. deliberately does NOT hold the manifest
13
+ * lock — a concurrent setSecret/seal does an atomic file rename, so the
14
+ * worst-case race here is reading the immediate-pre-rotation vault blob.
15
+ * audit logging happens at the command layer so programmatic reads
16
+ * (background sync, validation) don't pollute the audit trail. */
12
17
  export declare function getSecret(app: string, key: string): string | null;
13
- export declare function importEnvFile(app: string, path: string): number;
14
- export declare function importDbSecrets(app: string, dir: string): number;
18
+ export declare function importEnvFile(app: string, path: string): Promise<number>;
19
+ export declare function importDbSecrets(app: string, dir: string): Promise<number>;
15
20
  export declare function exportApp(app: string): string;
16
21
  export interface DriftResult {
17
22
  app: string;
@@ -22,12 +27,32 @@ export interface DriftResult {
22
27
  }
23
28
  export declare function detectDrift(app?: string): DriftResult[];
24
29
  export declare function unsealAll(): void;
25
- export declare function sealFromRuntime(app?: string): string[];
26
- export declare function rotateKey(): {
30
+ export declare function sealFromRuntime(app?: string): Promise<string[]>;
31
+ /**
32
+ * Rotate the age private key and re-encrypt every app's vault file with it.
33
+ *
34
+ * RECOVERY PROCEDURE (manual, only if rollback itself fails):
35
+ * 1. The previous private key is preserved at `<KEY_PATH>.old` while a
36
+ * rotation is in flight. If you see that file lying around, a rotation
37
+ * crashed mid-way.
38
+ * 2. Each app's pre-rotate encrypted file is preserved as
39
+ * `vault/<app>.{env,secrets}.age.bak-rotate-<ts>` for the duration of
40
+ * the rotation. They are removed automatically on success OR after a
41
+ * successful rollback.
42
+ * 3. To restore by hand: copy `<KEY_PATH>.old` back to `<KEY_PATH>`
43
+ * (chmod 0600), then for each `*.bak-rotate-<ts>` copy it over the
44
+ * matching encrypted file. This puts the vault back into the
45
+ * pre-rotation state.
46
+ *
47
+ * On a partial-failure path inside this function, that recovery is performed
48
+ * automatically before re-throwing. The whole rotation runs under the
49
+ * manifest's inter-process lock.
50
+ */
51
+ export declare function rotateKey(): Promise<{
27
52
  oldPubkey: string;
28
53
  newPubkey: string;
29
54
  appsRotated: string[];
30
- };
55
+ }>;
31
56
  export declare function getStatus(): {
32
57
  initialized: boolean;
33
58
  sealed: boolean;