@matthesketh/fleet 1.8.1 → 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 +9 -6
  228. package/dist/tui/views/HealthView.js +9 -4
  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 +8 -5
@@ -0,0 +1,74 @@
1
+ import { z } from 'zod';
2
+ /** single-dash short flags, mapped to the long-form (schema) field they set.
3
+ * short flags only ever set a boolean field true. */
4
+ const SHORT_FLAGS = { y: 'yes' };
5
+ /** true when a schema field (unwrapping optional/default) is a boolean. */
6
+ function isBooleanField(schema) {
7
+ let s = schema;
8
+ while (s instanceof z.ZodOptional || s instanceof z.ZodDefault) {
9
+ s = s._def.innerType;
10
+ }
11
+ return s instanceof z.ZodBoolean;
12
+ }
13
+ export function parseArgs(schema, argv) {
14
+ if (argv.includes('--help') || argv.includes('-h')) {
15
+ return { help: true };
16
+ }
17
+ const shape = schema.shape;
18
+ const fieldNames = Object.keys(shape);
19
+ const booleanFields = new Set(fieldNames.filter(n => isBooleanField(shape[n])));
20
+ const values = {};
21
+ const positionals = [];
22
+ for (let i = 0; i < argv.length; i++) {
23
+ const token = argv[i];
24
+ if (token.startsWith('--')) {
25
+ const body = token.slice(2);
26
+ const eq = body.indexOf('=');
27
+ if (eq !== -1) {
28
+ // --key=value form
29
+ values[body.slice(0, eq)] = body.slice(eq + 1);
30
+ continue;
31
+ }
32
+ if (booleanFields.has(body)) {
33
+ values[body] = true;
34
+ continue;
35
+ }
36
+ const next = argv[i + 1];
37
+ if (next === undefined || next.startsWith('--')) {
38
+ return { help: false, ok: false, error: `flag --${body} requires a value` };
39
+ }
40
+ values[body] = next;
41
+ i++;
42
+ }
43
+ else if (/^-[A-Za-z]$/.test(token)) {
44
+ // single-dash short flag (e.g. -y) — resolve via the alias table
45
+ const long = SHORT_FLAGS[token.slice(1)];
46
+ if (long === undefined || !(long in shape)) {
47
+ return { help: false, ok: false, error: `unknown flag: ${token}` };
48
+ }
49
+ values[long] = true;
50
+ }
51
+ else {
52
+ positionals.push(token);
53
+ }
54
+ }
55
+ // reject flags that aren't in the schema — a mistyped flag must not be dropped silently
56
+ const unknownFlags = Object.keys(values).filter(k => !(k in shape));
57
+ if (unknownFlags.length > 0) {
58
+ return { help: false, ok: false, error: `unknown flag(s): ${unknownFlags.join(', ')}` };
59
+ }
60
+ // assign leftover positionals to non-boolean fields in declaration order
61
+ const positionalFields = fieldNames.filter(n => !booleanFields.has(n) && !(n in values));
62
+ for (let i = 0; i < positionals.length && i < positionalFields.length; i++) {
63
+ values[positionalFields[i]] = positionals[i];
64
+ }
65
+ const parsed = schema.safeParse(values);
66
+ if (!parsed.success) {
67
+ return {
68
+ help: false,
69
+ ok: false,
70
+ error: parsed.error.issues.map(iss => `${iss.path.join('.')}: ${iss.message}`).join('; '),
71
+ };
72
+ }
73
+ return { help: false, ok: true, values: parsed.data };
74
+ }
@@ -0,0 +1,24 @@
1
+ import type { z } from 'zod';
2
+ import type { CommandContext, CommandDef, CommandResult } from './types.js';
3
+ export declare function register(def: CommandDef): void;
4
+ /**
5
+ * defines a command with full argument-type inference: the `args` schema and
6
+ * the `run` handler's first parameter are tied through the shape generic `S`.
7
+ * returns the erased `CommandDef` the registry stores.
8
+ */
9
+ export declare function defineCommand<S extends z.ZodRawShape, D>(def: {
10
+ name: string;
11
+ summary: string;
12
+ args: z.ZodObject<S>;
13
+ destructive?: boolean;
14
+ cliOnly?: boolean;
15
+ tui?: 'palette' | {
16
+ view: string;
17
+ };
18
+ run(args: z.infer<z.ZodObject<S>>, ctx: CommandContext): Promise<CommandResult<D>>;
19
+ }): CommandDef<D>;
20
+ export declare function getCommand(name: string): CommandDef | undefined;
21
+ export declare function allCommands(): CommandDef[];
22
+ /** test-only: clears the registry between tests. prefer _resetLoader() from
23
+ * registry/index.ts if you also need to reset the loadRegistry guard. */
24
+ export declare function _resetRegistry(): void;
@@ -0,0 +1,26 @@
1
+ const registry = new Map();
2
+ export function register(def) {
3
+ if (registry.has(def.name)) {
4
+ throw new Error(`duplicate command registration: ${def.name}`);
5
+ }
6
+ registry.set(def.name, def);
7
+ }
8
+ /**
9
+ * defines a command with full argument-type inference: the `args` schema and
10
+ * the `run` handler's first parameter are tied through the shape generic `S`.
11
+ * returns the erased `CommandDef` the registry stores.
12
+ */
13
+ export function defineCommand(def) {
14
+ return def;
15
+ }
16
+ export function getCommand(name) {
17
+ return registry.get(name);
18
+ }
19
+ export function allCommands() {
20
+ return [...registry.values()].sort((a, b) => a.name.localeCompare(b.name));
21
+ }
22
+ /** test-only: clears the registry between tests. prefer _resetLoader() from
23
+ * registry/index.ts if you also need to reset the loadRegistry guard. */
24
+ export function _resetRegistry() {
25
+ registry.clear();
26
+ }
@@ -0,0 +1,3 @@
1
+ import type { RenderModel } from './types.js';
2
+ /** turns a RenderModel into a plain-text block for cli output. */
3
+ export declare function renderToText(model: RenderModel): string;
@@ -0,0 +1,29 @@
1
+ /** turns a RenderModel into a plain-text block for cli output. */
2
+ export function renderToText(model) {
3
+ switch (model.kind) {
4
+ case 'lines':
5
+ return model.lines.join('\n');
6
+ case 'keyValue': {
7
+ const width = Math.max(0, ...model.pairs.map(([k]) => k.length));
8
+ return model.pairs.map(([k, v]) => `${k.padEnd(width + 2)}${v}`).join('\n');
9
+ }
10
+ case 'table': {
11
+ const all = [model.columns, ...model.rows];
12
+ const widths = model.columns.map((_, i) => Math.max(...all.map(row => (row[i] ?? '').length)));
13
+ const fmt = (row) => row
14
+ .slice(0, model.columns.length)
15
+ .map((cell, i) => (i === model.columns.length - 1 ? cell : cell.padEnd(widths[i] + 2)))
16
+ .join('');
17
+ return all.map(fmt).join('\n');
18
+ }
19
+ case 'tree':
20
+ return treeLines(model.root, 0).join('\n');
21
+ }
22
+ }
23
+ function treeLines(node, depth) {
24
+ const out = [' '.repeat(depth) + node.label];
25
+ for (const child of node.children ?? []) {
26
+ out.push(...treeLines(child, depth + 1));
27
+ }
28
+ return out;
29
+ }
@@ -0,0 +1,50 @@
1
+ import type { z } from 'zod';
2
+ /** a renderable model each surface turns into text (cli) or components (tui). */
3
+ export type RenderModel = {
4
+ kind: 'lines';
5
+ lines: string[];
6
+ } | {
7
+ kind: 'keyValue';
8
+ pairs: Array<[string, string]>;
9
+ } | {
10
+ kind: 'table';
11
+ columns: string[];
12
+ rows: string[][];
13
+ } | {
14
+ kind: 'tree';
15
+ root: TreeNode;
16
+ };
17
+ export interface TreeNode {
18
+ label: string;
19
+ children?: TreeNode[];
20
+ }
21
+ /** the surface-agnostic result every command handler returns. */
22
+ export interface CommandResult<D = unknown> {
23
+ ok: boolean;
24
+ summary: string;
25
+ data: D;
26
+ render?: RenderModel;
27
+ }
28
+ /** surface-neutral services passed to every handler. */
29
+ export interface CommandContext {
30
+ confirm(prompt: string): Promise<boolean>;
31
+ log(event: {
32
+ level: 'info' | 'warn' | 'error';
33
+ message: string;
34
+ }): void;
35
+ env: NodeJS.ProcessEnv;
36
+ }
37
+ /** a command defined once; all three surfaces are derived from this. the
38
+ * registry stores the erased form — `run` receives the already-parsed args
39
+ * as a plain record. use `defineCommand` for inference at definition sites. */
40
+ export interface CommandDef<D = unknown> {
41
+ name: string;
42
+ summary: string;
43
+ args: z.ZodObject<z.ZodRawShape>;
44
+ destructive?: boolean;
45
+ cliOnly?: boolean;
46
+ tui?: 'palette' | {
47
+ view: string;
48
+ };
49
+ run(args: Record<string, unknown>, ctx: CommandContext): Promise<CommandResult<D>>;
50
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ /** build the templated systemd unit for fleet-secrets-agent@%i.service.
2
+ * the vault path comes from the caller — production code passes whatever
3
+ * FLEET_VAULT_DIR or the repo-local default resolves to, so this template
4
+ * never carries an operator-specific assumption. */
5
+ export declare function generateAgentUnit(vaultPath: string): string;
@@ -0,0 +1,40 @@
1
+ /** build the templated systemd unit for fleet-secrets-agent@%i.service.
2
+ * the vault path comes from the caller — production code passes whatever
3
+ * FLEET_VAULT_DIR or the repo-local default resolves to, so this template
4
+ * never carries an operator-specific assumption. */
5
+ export function generateAgentUnit(vaultPath) {
6
+ return [
7
+ '[Unit]',
8
+ 'Description=Fleet Secrets Agent for %i',
9
+ 'After=network.target',
10
+ 'PartOf=docker-%i.service',
11
+ '',
12
+ '[Service]',
13
+ 'Type=notify',
14
+ 'DynamicUser=yes',
15
+ 'RuntimeDirectory=fleet-secrets',
16
+ 'RuntimeDirectoryPreserve=yes',
17
+ 'LoadCredentialEncrypted=age-key:/etc/fleet/credentials/%i.cred',
18
+ `ExecStart=/usr/local/bin/fleet-agent --app %i --vault ${vaultPath} --socket /run/fleet-secrets/%i.sock`,
19
+ 'Restart=on-failure',
20
+ 'RestartSec=2',
21
+ '',
22
+ '# hardening',
23
+ 'ProtectSystem=strict',
24
+ 'ProtectHome=read-only',
25
+ `ReadOnlyPaths=${vaultPath}`,
26
+ 'PrivateTmp=yes',
27
+ 'NoNewPrivileges=yes',
28
+ 'ProtectKernelTunables=yes',
29
+ 'ProtectKernelModules=yes',
30
+ 'ProtectControlGroups=yes',
31
+ 'RestrictAddressFamilies=AF_UNIX',
32
+ 'RestrictNamespaces=yes',
33
+ 'SystemCallFilter=@system-service',
34
+ 'SystemCallFilter=~@privileged @resources @mount',
35
+ '',
36
+ '[Install]',
37
+ 'WantedBy=multi-user.target',
38
+ '',
39
+ ].join('\n');
40
+ }
@@ -0,0 +1,2 @@
1
+ export declare function addAgentDependency(content: string, app: string): string;
2
+ export declare function removeAgentDependency(content: string, app: string): string;
@@ -0,0 +1,46 @@
1
+ function findUnitSection(content) {
2
+ const lines = content.split('\n');
3
+ let start = -1;
4
+ let end = -1;
5
+ for (let i = 0; i < lines.length; i++) {
6
+ if (lines[i].trim() === '[Unit]') {
7
+ start = i;
8
+ continue;
9
+ }
10
+ if (start >= 0 && lines[i].startsWith('[') && lines[i].endsWith(']')) {
11
+ end = i;
12
+ break;
13
+ }
14
+ }
15
+ if (start < 0)
16
+ throw new Error('no [Unit] section found');
17
+ if (end < 0)
18
+ end = lines.length;
19
+ return { start, end };
20
+ }
21
+ export function addAgentDependency(content, app) {
22
+ const lines = content.split('\n');
23
+ const requires = `Requires=fleet-secrets-agent@${app}.service`;
24
+ const after = `After=fleet-secrets-agent@${app}.service`;
25
+ if (lines.includes(requires) && lines.includes(after))
26
+ return content;
27
+ const section = findUnitSection(content);
28
+ let insertAt = section.end;
29
+ while (insertAt > section.start + 1 && lines[insertAt - 1].trim() === '')
30
+ insertAt--;
31
+ const toInsert = [];
32
+ if (!lines.includes(requires))
33
+ toInsert.push(requires);
34
+ if (!lines.includes(after))
35
+ toInsert.push(after);
36
+ lines.splice(insertAt, 0, ...toInsert);
37
+ return lines.join('\n');
38
+ }
39
+ export function removeAgentDependency(content, app) {
40
+ const requires = `Requires=fleet-secrets-agent@${app}.service`;
41
+ const after = `After=fleet-secrets-agent@${app}.service`;
42
+ return content
43
+ .split('\n')
44
+ .filter(l => l !== requires && l !== after)
45
+ .join('\n');
46
+ }
@@ -0,0 +1,2 @@
1
+ export declare function migrateComposeToV2(yamlContent: string, app: string, service: string): string;
2
+ export declare function revertComposeFromV2(yamlContent: string, app: string, service: string): string;
@@ -0,0 +1,156 @@
1
+ import { parseDocument, Scalar, isMap, isSeq, isScalar } from 'yaml';
2
+ const FLEET_ENV_KEY = 'FLEET_SECRETS_SOCKET';
3
+ const FLEET_ENV_VAL = '/run/fleet.sock';
4
+ function v1EnvFile(app) {
5
+ return `/run/fleet-secrets/${app}/.env`;
6
+ }
7
+ function v2SocketMount(app) {
8
+ return `/run/fleet-secrets/${app}.sock:/run/fleet.sock:ro`;
9
+ }
10
+ function getService(doc, service) {
11
+ const svc = doc.getIn(['services', service], true);
12
+ if (!svc || !isMap(svc)) {
13
+ throw new Error(`service '${service}' not found in compose file`);
14
+ }
15
+ return svc;
16
+ }
17
+ function removeEnvFileEntry(svc, app) {
18
+ const v1Path = v1EnvFile(app);
19
+ const raw = svc.get('env_file', true);
20
+ if (raw === undefined || raw === null)
21
+ return;
22
+ if (isScalar(raw)) {
23
+ if (raw.value === v1Path) {
24
+ svc.delete('env_file');
25
+ }
26
+ return;
27
+ }
28
+ if (isSeq(raw)) {
29
+ const seq = raw;
30
+ const idx = seq.items.findIndex(item => isScalar(item) && item.value === v1Path);
31
+ if (idx !== -1) {
32
+ seq.items.splice(idx, 1);
33
+ }
34
+ if (seq.items.length === 0) {
35
+ svc.delete('env_file');
36
+ }
37
+ return;
38
+ }
39
+ if (typeof raw === 'string' && raw === v1Path) {
40
+ svc.delete('env_file');
41
+ }
42
+ }
43
+ function ensureEnvVar(svc) {
44
+ const envRaw = svc.get('environment', true);
45
+ if (!envRaw) {
46
+ svc.set('environment', { [FLEET_ENV_KEY]: FLEET_ENV_VAL });
47
+ return;
48
+ }
49
+ if (isMap(envRaw)) {
50
+ const env = envRaw;
51
+ if (!env.has(FLEET_ENV_KEY)) {
52
+ env.set(FLEET_ENV_KEY, FLEET_ENV_VAL);
53
+ }
54
+ return;
55
+ }
56
+ if (isSeq(envRaw)) {
57
+ const seq = envRaw;
58
+ const kvPrefix = `${FLEET_ENV_KEY}=`;
59
+ const already = seq.items.some(item => isScalar(item) && typeof item.value === 'string' && item.value.startsWith(kvPrefix));
60
+ if (!already) {
61
+ seq.add(`${FLEET_ENV_KEY}=${FLEET_ENV_VAL}`);
62
+ }
63
+ return;
64
+ }
65
+ }
66
+ function removeEnvVar(svc) {
67
+ const envRaw = svc.get('environment', true);
68
+ if (!envRaw)
69
+ return;
70
+ if (isMap(envRaw)) {
71
+ const env = envRaw;
72
+ env.delete(FLEET_ENV_KEY);
73
+ if (env.items.length === 0) {
74
+ svc.delete('environment');
75
+ }
76
+ return;
77
+ }
78
+ if (isSeq(envRaw)) {
79
+ const seq = envRaw;
80
+ const kvPrefix = `${FLEET_ENV_KEY}=`;
81
+ const idx = seq.items.findIndex(item => isScalar(item) && typeof item.value === 'string' && item.value.startsWith(kvPrefix));
82
+ if (idx !== -1) {
83
+ seq.items.splice(idx, 1);
84
+ }
85
+ if (seq.items.length === 0) {
86
+ svc.delete('environment');
87
+ }
88
+ }
89
+ }
90
+ function ensureSocketMount(svc, app) {
91
+ const mount = v2SocketMount(app);
92
+ const volRaw = svc.get('volumes', true);
93
+ if (!volRaw) {
94
+ svc.set('volumes', [mount]);
95
+ return;
96
+ }
97
+ if (isSeq(volRaw)) {
98
+ const seq = volRaw;
99
+ const already = seq.items.some(item => isScalar(item) && item.value === mount);
100
+ if (!already) {
101
+ seq.add(mount);
102
+ }
103
+ return;
104
+ }
105
+ }
106
+ function removeSocketMount(svc, app) {
107
+ const mount = v2SocketMount(app);
108
+ const volRaw = svc.get('volumes', true);
109
+ if (!volRaw || !isSeq(volRaw))
110
+ return;
111
+ const seq = volRaw;
112
+ const idx = seq.items.findIndex(item => isScalar(item) && item.value === mount);
113
+ if (idx !== -1) {
114
+ seq.items.splice(idx, 1);
115
+ }
116
+ if (seq.items.length === 0) {
117
+ svc.delete('volumes');
118
+ }
119
+ }
120
+ function restoreEnvFile(svc, app) {
121
+ const v1Path = v1EnvFile(app);
122
+ const raw = svc.get('env_file', true);
123
+ if (!raw) {
124
+ svc.set('env_file', v1Path);
125
+ return;
126
+ }
127
+ if (isScalar(raw)) {
128
+ if (raw.value !== v1Path) {
129
+ svc.set('env_file', v1Path);
130
+ }
131
+ return;
132
+ }
133
+ if (isSeq(raw)) {
134
+ const seq = raw;
135
+ const already = seq.items.some(item => isScalar(item) && item.value === v1Path);
136
+ if (!already) {
137
+ seq.items.unshift(new Scalar(v1Path));
138
+ }
139
+ }
140
+ }
141
+ export function migrateComposeToV2(yamlContent, app, service) {
142
+ const doc = parseDocument(yamlContent);
143
+ const svc = getService(doc, service);
144
+ removeEnvFileEntry(svc, app);
145
+ ensureEnvVar(svc);
146
+ ensureSocketMount(svc, app);
147
+ return doc.toString();
148
+ }
149
+ export function revertComposeFromV2(yamlContent, app, service) {
150
+ const doc = parseDocument(yamlContent);
151
+ const svc = getService(doc, service);
152
+ removeSocketMount(svc, app);
153
+ removeEnvVar(svc);
154
+ restoreEnvFile(svc, app);
155
+ return doc.toString();
156
+ }
@@ -1,4 +1,15 @@
1
+ import { assertDomain, assertHealthPath } from '../core/validate.js';
1
2
  export function generateNginxConfig(opts) {
3
+ // defence-in-depth: callers are expected to validate already, but a
4
+ // missed call must not produce a config that injects directives via
5
+ // ${domain} (server_name interpolation) or ${port} (proxy_pass).
6
+ // mirrors the assertComposeFile pattern in src/templates/systemd.ts.
7
+ assertDomain(opts.domain);
8
+ if (!Number.isInteger(opts.port) || opts.port < 1 || opts.port > 65535) {
9
+ throw new Error(`invalid port: ${opts.port}`);
10
+ }
11
+ if (opts.apiPrefix !== undefined)
12
+ assertHealthPath(opts.apiPrefix);
2
13
  const { domain, port, type } = opts;
3
14
  const apiPrefix = opts.apiPrefix ?? '/api';
4
15
  const securityHeaders = ` add_header X-Content-Type-Options "nosniff" always;
@@ -1,4 +1,10 @@
1
+ import { assertComposeFile } from '../core/validate.js';
1
2
  export function generateServiceFile(opts) {
3
+ // Defence-in-depth: even if a caller skipped upstream validation, refuse to
4
+ // emit a unit file with a composeFile value that could break out of the
5
+ // quoted -f argument and inject extra docker-compose flags or shell.
6
+ if (opts.composeFile)
7
+ assertComposeFile(opts.composeFile);
2
8
  const fileFlag = opts.composeFile ? ` -f "${opts.composeFile}"` : '';
3
9
  const dbDep = opts.dependsOnDatabases ? ' docker-databases.service' : '';
4
10
  return `[Unit]
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ import { z } from 'zod';
3
+ export declare function ArgForm(props: {
4
+ schema: z.ZodObject<z.ZodRawShape>;
5
+ onSubmit: (values: Record<string, unknown>) => void;
6
+ onCancel: () => void;
7
+ }): React.JSX.Element;
@@ -0,0 +1,64 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Box, Text } from 'ink';
4
+ import { z } from 'zod';
5
+ import { useRegisterHandler } from '@matthesketh/ink-input-dispatcher';
6
+ import { colors } from '../theme.js';
7
+ function describeField(name, schema) {
8
+ let s = schema;
9
+ while (s instanceof z.ZodOptional || s instanceof z.ZodDefault) {
10
+ s = s._def.innerType;
11
+ }
12
+ if (s instanceof z.ZodBoolean)
13
+ return { name, kind: 'boolean' };
14
+ if (s instanceof z.ZodNumber)
15
+ return { name, kind: 'number' };
16
+ if (s instanceof z.ZodEnum)
17
+ return { name, kind: 'enum', options: s._def.values };
18
+ return { name, kind: 'string' };
19
+ }
20
+ export function ArgForm(props) {
21
+ const fields = Object.entries(props.schema.shape).map(([n, s]) => describeField(n, s));
22
+ const [cursor, setCursor] = useState(0);
23
+ const [values, setValues] = useState(() => Object.fromEntries(fields.map(f => [f.name, f.kind === 'boolean' ? false : ''])));
24
+ const handler = (input, key) => {
25
+ if (key.escape) {
26
+ props.onCancel();
27
+ return true;
28
+ }
29
+ if (key.return) {
30
+ props.onSubmit(values);
31
+ return true;
32
+ }
33
+ if (key.downArrow) {
34
+ setCursor(c => Math.min(c + 1, fields.length - 1));
35
+ return true;
36
+ }
37
+ if (key.upArrow) {
38
+ setCursor(c => Math.max(c - 1, 0));
39
+ return true;
40
+ }
41
+ const field = fields[cursor];
42
+ if (!field)
43
+ return false;
44
+ if (field.kind === 'boolean') {
45
+ if (input === ' ') {
46
+ setValues(v => ({ ...v, [field.name]: !v[field.name] }));
47
+ return true;
48
+ }
49
+ }
50
+ else if (key.backspace || key.delete) {
51
+ setValues(v => ({ ...v, [field.name]: String(v[field.name] ?? '').slice(0, -1) }));
52
+ return true;
53
+ }
54
+ else if (input && !key.ctrl && !key.meta) {
55
+ setValues(v => ({ ...v, [field.name]: String(v[field.name] ?? '') + input }));
56
+ return true;
57
+ }
58
+ return false;
59
+ };
60
+ useRegisterHandler(handler);
61
+ return (_jsxs(Box, { flexDirection: "column", children: [fields.map((f, i) => (_jsxs(Box, { children: [_jsxs(Text, { color: i === cursor ? colors.primary : colors.muted, children: [i === cursor ? '> ' : ' ', f.name, ":", ' '] }), _jsx(Text, { children: f.kind === 'boolean'
62
+ ? (values[f.name] ? '[x]' : '[ ]')
63
+ : String(values[f.name] ?? '') })] }, f.name))), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, children: "\u2191\u2193 field \u00B7 space toggle \u00B7 enter run \u00B7 esc cancel" }) })] }));
64
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { render } from 'ink-testing-library';
3
+ import { describe, it, expect } from 'vitest';
4
+ import { z } from 'zod';
5
+ import { ArgForm } from './ArgForm.js';
6
+ describe('ArgForm', () => {
7
+ it('renders one labelled field per schema property', () => {
8
+ const schema = z.object({ app: z.string(), force: z.boolean().default(false) });
9
+ const { lastFrame } = render(_jsx(ArgForm, { schema: schema, onSubmit: () => { }, onCancel: () => { } }));
10
+ const frame = lastFrame() ?? '';
11
+ expect(frame).toContain('app');
12
+ expect(frame).toContain('force');
13
+ });
14
+ it('shows a toggle hint for boolean fields', () => {
15
+ const schema = z.object({ force: z.boolean().default(false) });
16
+ const { lastFrame } = render(_jsx(ArgForm, { schema: schema, onSubmit: () => { }, onCancel: () => { } }));
17
+ expect(lastFrame() ?? '').toMatch(/false|off|\[ \]/i);
18
+ });
19
+ });
@@ -54,6 +54,11 @@ const viewHints = {
54
54
  { key: 'L', label: 'level' },
55
55
  { key: 'q', label: 'quit' },
56
56
  ],
57
+ 'command-palette': [
58
+ { key: '↑/↓', label: 'navigate' },
59
+ { key: 'Enter', label: 'run' },
60
+ { key: 'Esc', label: 'close' },
61
+ ],
57
62
  };
58
63
  export function KeyHint() {
59
64
  const { currentView, confirmAction } = useAppState();
@@ -20,28 +20,28 @@ interface SecretsState {
20
20
  interface SecretsActions {
21
21
  refresh: () => void;
22
22
  loadAppSecrets: (app: string) => void;
23
- saveSecret: (app: string, key: string, value: string) => {
23
+ saveSecret: (app: string, key: string, value: string) => Promise<{
24
24
  ok: boolean;
25
25
  error?: string;
26
- };
27
- deleteSecret: (app: string, key: string) => {
26
+ }>;
27
+ deleteSecret: (app: string, key: string) => Promise<{
28
28
  ok: boolean;
29
29
  error?: string;
30
- };
30
+ }>;
31
31
  revealSecret: (app: string, key: string) => void;
32
32
  hideSecret: (key: string) => void;
33
33
  unseal: () => {
34
34
  ok: boolean;
35
35
  error?: string;
36
36
  };
37
- seal: () => {
37
+ seal: () => Promise<{
38
38
  ok: boolean;
39
39
  error?: string;
40
- };
41
- importEnv: (app: string, path: string) => {
40
+ }>;
41
+ importEnv: (app: string, path: string) => Promise<{
42
42
  ok: boolean;
43
43
  error?: string;
44
- };
44
+ }>;
45
45
  }
46
46
  export declare function useSecrets(): SecretsState & SecretsActions;
47
47
  export {};