@takodotid/circuit 0.43.1 → 1.0.0-rc.2

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 (256) hide show
  1. package/README.md +115 -37
  2. package/package.json +21 -58
  3. package/src/checks/across.ts +402 -0
  4. package/src/checks/built-in.ts +256 -0
  5. package/src/checks/device.ts +436 -0
  6. package/src/checks/index.ts +53 -0
  7. package/src/cli/advisories.ts +108 -0
  8. package/src/cli/app.ts +71 -0
  9. package/src/cli/editor-types.generated.ts +30 -0
  10. package/src/cli/editor-types.ts +69 -0
  11. package/src/cli/format.ts +124 -0
  12. package/src/cli/git-hooks.ts +79 -0
  13. package/src/cli/index.ts +350 -1170
  14. package/src/cli/migrate.ts +554 -0
  15. package/src/cli/project-commands.ts +656 -0
  16. package/src/cli/releases.ts +374 -0
  17. package/src/cli/remember.ts +339 -0
  18. package/src/cli/rename.ts +27 -33
  19. package/src/cli/secrets-password.ts +60 -0
  20. package/src/cli/secrets.ts +258 -0
  21. package/src/cli/setup.ts +300 -0
  22. package/src/cli/signing.ts +23 -0
  23. package/src/cli/text-editor.ts +268 -0
  24. package/src/cli/ui.ts +298 -0
  25. package/src/cli/version.ts +2 -0
  26. package/src/cli/versions.ts +158 -0
  27. package/src/config/address.ts +102 -0
  28. package/src/config/circuit.d.ts +30 -0
  29. package/src/config/common.ts +65 -0
  30. package/src/config/communities.ts +479 -0
  31. package/src/config/define.ts +684 -0
  32. package/src/config/device.ts +164 -0
  33. package/src/config/firewall.ts +150 -0
  34. package/src/config/index.ts +22 -0
  35. package/src/{schema → config}/interfaces.ts +121 -85
  36. package/src/config/keys.ts +434 -0
  37. package/src/config/management.ts +61 -0
  38. package/src/config/merge.ts +335 -0
  39. package/src/config/models.ts +70 -0
  40. package/src/config/monitoring.ts +64 -0
  41. package/src/config/network.ts +180 -0
  42. package/src/config/presets.ts +149 -0
  43. package/src/config/routing.ts +401 -0
  44. package/src/config/services.ts +51 -0
  45. package/src/config/switching.ts +84 -0
  46. package/src/config/system.ts +103 -0
  47. package/src/{schema → config}/time-zones.ts +1 -1
  48. package/src/core/address.ts +1 -1
  49. package/src/core/cabling.ts +8 -43
  50. package/src/core/graph.ts +40 -0
  51. package/src/{integrations → core}/registries.ts +5 -3
  52. package/src/core/secrets-file.ts +539 -0
  53. package/src/core/secrets.ts +83 -64
  54. package/src/core/topology.ts +103 -146
  55. package/src/core/utils.ts +12 -1
  56. package/src/model.ts +83 -0
  57. package/src/platforms/derive.ts +64 -37
  58. package/src/platforms/finding.ts +9 -0
  59. package/src/platforms/fortios/firewall.ts +76 -64
  60. package/src/platforms/fortios/index.ts +189 -125
  61. package/src/platforms/fortios/names.ts +51 -18
  62. package/src/platforms/fortios/render.ts +69 -62
  63. package/src/platforms/fortios/routing.ts +89 -58
  64. package/src/platforms/fortios/vdoms.ts +47 -16
  65. package/src/platforms/junos/TRAPS.md +2 -2
  66. package/src/platforms/junos/bgp.ts +63 -42
  67. package/src/platforms/junos/index.ts +122 -89
  68. package/src/platforms/junos/render.ts +139 -104
  69. package/src/platforms/platform.ts +10 -8
  70. package/src/platforms/raisecom-ros/TRAPS.md +1 -1
  71. package/src/platforms/raisecom-ros/index.ts +89 -70
  72. package/src/platforms/raisecom-ros/render.ts +49 -45
  73. package/src/platforms/routeros/TRAPS.md +3 -3
  74. package/src/platforms/routeros/index.ts +121 -60
  75. package/src/platforms/routeros/render.ts +214 -195
  76. package/src/platforms/vrp/TRAPS.md +1 -1
  77. package/src/platforms/vrp/index.ts +74 -50
  78. package/src/platforms/vrp/render.ts +67 -56
  79. package/src/project/project.ts +725 -0
  80. package/src/studio/app/env.d.ts +9 -0
  81. package/src/studio/app/index.html +18 -0
  82. package/src/studio/app/src/App.vue +95 -0
  83. package/src/studio/app/src/components/AddressesView.vue +38 -0
  84. package/src/studio/app/src/components/CablesView.vue +29 -0
  85. package/src/studio/app/src/components/CompareView.vue +160 -0
  86. package/src/studio/app/src/components/DataTable.vue +49 -0
  87. package/src/studio/app/src/components/DeviceView.vue +237 -0
  88. package/src/studio/app/src/components/DevicesView.vue +32 -0
  89. package/src/studio/app/src/components/Faceplate.vue +75 -0
  90. package/src/studio/app/src/components/GraphView.vue +282 -0
  91. package/src/studio/app/src/components/LineDiff.vue +24 -0
  92. package/src/studio/app/src/components/ProblemsView.vue +42 -0
  93. package/src/studio/app/src/components/SearchView.vue +72 -0
  94. package/src/studio/app/src/components/SecretsView.vue +112 -0
  95. package/src/studio/app/src/components/SessionsView.vue +35 -0
  96. package/src/studio/app/src/components/Settings.vue +95 -0
  97. package/src/studio/app/src/components/ToolsView.vue +118 -0
  98. package/src/studio/app/src/components/VlansView.vue +50 -0
  99. package/src/studio/app/src/lib/csv.ts +19 -0
  100. package/src/studio/{public/diff.js → app/src/lib/diff.ts} +40 -35
  101. package/src/studio/app/src/lib/format.ts +38 -0
  102. package/src/studio/app/src/lib/net.ts +68 -0
  103. package/src/studio/app/src/lib/secrets-crypto.ts +35 -0
  104. package/src/studio/app/src/lib/store.ts +71 -0
  105. package/src/studio/app/src/lib/tools.ts +217 -0
  106. package/src/studio/app/src/main.ts +9 -0
  107. package/src/studio/{public → app/src}/style.css +14 -4
  108. package/src/studio/app/tsconfig.json +17 -0
  109. package/src/studio/app/vite.config.ts +16 -0
  110. package/src/studio/assets.generated.ts +3 -0
  111. package/src/studio/data.ts +96 -188
  112. package/src/studio/git.ts +27 -43
  113. package/src/studio/server.ts +196 -107
  114. package/src/studio/types.ts +141 -0
  115. package/src/transport/console.ts +3 -3
  116. package/src/transport/ssh.ts +1 -1
  117. package/dist/chunks/index-3b343xva.js +0 -177
  118. package/dist/chunks/index-6wb41q67.js +0 -62
  119. package/dist/chunks/index-n7w3tg5y.js +0 -149
  120. package/dist/chunks/index-yzw86rc8.js +0 -921
  121. package/dist/chunks/new-w3zys5y4.js +0 -172
  122. package/dist/chunks/server-cxr9ynxc.js +0 -259
  123. package/dist/cli/index.js +0 -12922
  124. package/dist/helpers/index.js +0 -881
  125. package/dist/index.js +0 -28
  126. package/dist/types/core/address-space.d.ts +0 -62
  127. package/dist/types/core/address.d.ts +0 -44
  128. package/dist/types/core/cabling.d.ts +0 -41
  129. package/dist/types/core/cross-checks.d.ts +0 -28
  130. package/dist/types/core/define.d.ts +0 -79
  131. package/dist/types/core/project-files.d.ts +0 -12
  132. package/dist/types/core/schema-docs.d.ts +0 -25
  133. package/dist/types/core/secrets.d.ts +0 -62
  134. package/dist/types/core/topology.d.ts +0 -96
  135. package/dist/types/core/utils.d.ts +0 -10
  136. package/dist/types/core/validator.d.ts +0 -23
  137. package/dist/types/helpers/bgp.d.ts +0 -195
  138. package/dist/types/helpers/cables.d.ts +0 -69
  139. package/dist/types/helpers/checks.d.ts +0 -36
  140. package/dist/types/helpers/exchange.d.ts +0 -55
  141. package/dist/types/helpers/filters.d.ts +0 -51
  142. package/dist/types/helpers/index.d.ts +0 -14
  143. package/dist/types/helpers/route-server.d.ts +0 -52
  144. package/dist/types/helpers/upstreams.d.ts +0 -192
  145. package/dist/types/index.d.ts +0 -10
  146. package/dist/types/integrations/librenms.d.ts +0 -60
  147. package/dist/types/integrations/netbox.d.ts +0 -89
  148. package/dist/types/integrations/peeringdb.d.ts +0 -24
  149. package/dist/types/integrations/prometheus.d.ts +0 -24
  150. package/dist/types/integrations/registries.d.ts +0 -8
  151. package/dist/types/platforms/bgp-wait.d.ts +0 -24
  152. package/dist/types/platforms/catalog.d.ts +0 -109
  153. package/dist/types/platforms/derive.d.ts +0 -17
  154. package/dist/types/platforms/fortios/firewall.d.ts +0 -47
  155. package/dist/types/platforms/fortios/index.d.ts +0 -140
  156. package/dist/types/platforms/fortios/models.d.ts +0 -14
  157. package/dist/types/platforms/fortios/names.d.ts +0 -52
  158. package/dist/types/platforms/fortios/plan.d.ts +0 -114
  159. package/dist/types/platforms/fortios/render.d.ts +0 -77
  160. package/dist/types/platforms/fortios/routing.d.ts +0 -56
  161. package/dist/types/platforms/fortios/tree.d.ts +0 -37
  162. package/dist/types/platforms/fortios/vdoms.d.ts +0 -50
  163. package/dist/types/platforms/index.d.ts +0 -13
  164. package/dist/types/platforms/junos/bgp.d.ts +0 -44
  165. package/dist/types/platforms/junos/index.d.ts +0 -114
  166. package/dist/types/platforms/junos/models.d.ts +0 -14
  167. package/dist/types/platforms/junos/render.d.ts +0 -154
  168. package/dist/types/platforms/line-diff.d.ts +0 -67
  169. package/dist/types/platforms/platform.d.ts +0 -135
  170. package/dist/types/platforms/raisecom-ros/index.d.ts +0 -71
  171. package/dist/types/platforms/raisecom-ros/models.d.ts +0 -14
  172. package/dist/types/platforms/raisecom-ros/render.d.ts +0 -41
  173. package/dist/types/platforms/routeros/export.d.ts +0 -59
  174. package/dist/types/platforms/routeros/index.d.ts +0 -116
  175. package/dist/types/platforms/routeros/models.d.ts +0 -18
  176. package/dist/types/platforms/routeros/plan.d.ts +0 -58
  177. package/dist/types/platforms/routeros/render.d.ts +0 -250
  178. package/dist/types/platforms/vrp/index.d.ts +0 -83
  179. package/dist/types/platforms/vrp/models.d.ts +0 -12
  180. package/dist/types/platforms/vrp/render.d.ts +0 -96
  181. package/dist/types/schema/common.d.ts +0 -30
  182. package/dist/types/schema/device.d.ts +0 -126
  183. package/dist/types/schema/firewall.d.ts +0 -131
  184. package/dist/types/schema/index.d.ts +0 -8
  185. package/dist/types/schema/interfaces.d.ts +0 -205
  186. package/dist/types/schema/policy.d.ts +0 -107
  187. package/dist/types/schema/routing.d.ts +0 -161
  188. package/dist/types/schema/system.d.ts +0 -198
  189. package/dist/types/schema/time-zones.d.ts +0 -2
  190. package/dist/types/transport/console.d.ts +0 -115
  191. package/dist/types/transport/path-check.d.ts +0 -32
  192. package/dist/types/transport/ssh.d.ts +0 -27
  193. package/src/cli/graph-format.ts +0 -91
  194. package/src/cli/new.ts +0 -234
  195. package/src/core/address-space.ts +0 -320
  196. package/src/core/cross-checks.ts +0 -369
  197. package/src/core/define.ts +0 -124
  198. package/src/core/project-files.ts +0 -35
  199. package/src/core/schema-docs.ts +0 -174
  200. package/src/core/validator.ts +0 -508
  201. package/src/helpers/bgp.ts +0 -473
  202. package/src/helpers/cables.ts +0 -74
  203. package/src/helpers/checks.ts +0 -201
  204. package/src/helpers/exchange.ts +0 -123
  205. package/src/helpers/filters.ts +0 -107
  206. package/src/helpers/index.ts +0 -25
  207. package/src/helpers/route-server.ts +0 -212
  208. package/src/helpers/upstreams.ts +0 -369
  209. package/src/index.ts +0 -13
  210. package/src/integrations/librenms.ts +0 -291
  211. package/src/integrations/netbox.ts +0 -620
  212. package/src/integrations/peeringdb.ts +0 -173
  213. package/src/integrations/prometheus.ts +0 -90
  214. package/src/schema/common.ts +0 -41
  215. package/src/schema/device.ts +0 -145
  216. package/src/schema/firewall.ts +0 -140
  217. package/src/schema/index.ts +0 -8
  218. package/src/schema/policy.ts +0 -115
  219. package/src/schema/routing.ts +0 -173
  220. package/src/schema/system.ts +0 -203
  221. package/src/studio/public/app.js +0 -186
  222. package/src/studio/public/compare.js +0 -250
  223. package/src/studio/public/dom.js +0 -138
  224. package/src/studio/public/evaluate.js +0 -687
  225. package/src/studio/public/export.js +0 -130
  226. package/src/studio/public/graph.js +0 -1541
  227. package/src/studio/public/index.html +0 -83
  228. package/src/studio/public/net.js +0 -134
  229. package/src/studio/public/tables.js +0 -1289
  230. package/src/studio/public/tools.js +0 -610
  231. package/templates/colocation/checks.ts +0 -53
  232. package/templates/colocation/circuit.config.ts +0 -9
  233. package/templates/colocation/edge-01.ts +0 -133
  234. package/templates/colocation/routing.ts +0 -13
  235. package/templates/colocation/tenancy.ts +0 -76
  236. package/templates/colocation/tenants/acme.ts +0 -19
  237. package/templates/colocation/tenants/index.ts +0 -6
  238. package/templates/colocation/tor-01.ts +0 -33
  239. package/templates/edge-router/checks.ts +0 -20
  240. package/templates/edge-router/circuit.config.ts +0 -8
  241. package/templates/edge-router/edge-01.ts +0 -110
  242. package/templates/edge-router/routing.ts +0 -13
  243. package/templates/project/AGENTS.md +0 -51
  244. package/templates/project/CLAUDE.md +0 -1
  245. package/templates/project/README.md +0 -16
  246. package/templates/project/gitignore +0 -5
  247. package/templates/project/tsconfig.json +0 -13
  248. package/templates/single-site/checks.ts +0 -26
  249. package/templates/single-site/circuit.config.ts +0 -8
  250. package/templates/single-site/router.ts +0 -86
  251. package/templates/single-site/site.ts +0 -18
  252. package/templates/single-site/switch.ts +0 -34
  253. /package/src/studio/{public → app/public}/favicon.svg +0 -0
  254. /package/src/studio/{public → app/public}/special-gothic-latin-ext.woff2 +0 -0
  255. /package/src/studio/{public → app/public}/special-gothic-latin.woff2 +0 -0
  256. /package/src/studio/{public → app/public}/special-gothic-license.txt +0 -0
@@ -1,11 +1,12 @@
1
- // Secrets are references in the config and values only at the moment a command is sent. A reference names a variable, set in the environment or in `.env` beside the config, or a file kept out of the repository. A variable's value may itself be a 1Password reference, `op://vault/item/field`, read with the 1Password CLI.
1
+ // Secrets are references in the config and values only at the moment a command is sent. A reference names a variable, set in the environment or in the network's secrets file (see `SecretsFile`), or a file kept out of the repository. A value in the secrets file is encrypted, or a 1Password reference, `op://vault/item/field`, read with the 1Password CLI.
2
2
 
3
- import { spawnSync } from "node:child_process";
4
3
  import { existsSync, readFileSync } from "node:fs";
5
4
  import { join } from "node:path";
6
- import { parseEnv } from "node:util";
7
- import type { Network } from "./define";
8
- import type { Console, Device, IP, Secret } from "../schema";
5
+ import type { Console, Device, IP, Secret } from "../model";
6
+ import { OnePassword, SecretsFile } from "./secrets-file";
7
+
8
+ /** Anything that holds secret references, such as a network's devices. */
9
+ type Network = object;
9
10
 
10
11
  /** How a device is logged in to: its management address and SSH port, its connection user and that user's password, and its console. */
11
12
  export type DeviceLogin = {
@@ -19,68 +20,59 @@ export type DeviceLogin = {
19
20
  };
20
21
 
21
22
  export class Secrets {
22
- /** The directory of the config file. `.env` and secret files are found from here. */
23
+ /** The directory of the config file. The secrets file and secret files are found from here. */
23
24
  private static root = process.cwd();
25
+ /** The secrets file of the network being worked on, relative to `root`. */
26
+ private static fileName = SecretsFile.DEFAULT;
24
27
 
25
- private static envFile: Record<string, string> | undefined;
26
- private static readonly fromOnePassword = new Map<string, string>();
28
+ private static values: Record<string, string> | undefined;
27
29
  /** Every value read in this run, so `scrub` can hide it. */
28
30
  private static readonly revealed = new Set<string>();
29
31
 
30
32
  /** What `scrub` puts where a secret was. */
31
33
  static readonly REDACTED = "<redacted>";
32
34
 
33
- /** A value in `.env` shorter than this is not hidden: `.env` can hold settings that are not secrets, and hiding a short one would garble the text. */
34
- private static readonly SHORTEST_DOTENV_VALUE = 6;
35
+ /** A value in plain text shorter than this is not hidden: hiding a short one would garble the text. */
36
+ private static readonly SHORTEST_PLAIN_VALUE = 6;
35
37
 
36
38
  /** A marker anywhere in a command. Its group is the reference. */
37
39
  static readonly MARKER = /<secret:([A-Za-z0-9_./:-]+)>/g;
38
40
 
39
- /** Find `.env` and secret files from this directory, the config file's, instead of the working directory. */
41
+ /** Find the secrets file and secret files from this directory, the config file's, instead of the working directory. */
40
42
  static useRoot(directory: string): void {
41
43
  Secrets.root = directory;
42
- Secrets.envFile = undefined;
44
+ Secrets.useFile(SecretsFile.DEFAULT);
43
45
  }
44
46
 
45
- /** `.env` beside the config, read once. Node and Bun read it the same way they load `.env` themselves: a `#` starts a comment unless the value is quoted. */
46
- private static dotenv(): Record<string, string> {
47
- if (Secrets.envFile) return Secrets.envFile;
48
-
49
- const path = join(Secrets.root, ".env");
50
- const text = existsSync(path) ? readFileSync(path, "utf8") : "";
51
-
52
- Secrets.refuseCutValues(text);
53
- Secrets.envFile = parseEnv(text) as Record<string, string>;
54
- return Secrets.envFile;
47
+ /** Read secrets from this file, relative to the project: the one a network's `secrets.file` names, or `.circuit/secrets`. */
48
+ static useFile(name: string = SecretsFile.DEFAULT): void {
49
+ Secrets.fileName = name;
50
+ Secrets.values = undefined;
55
51
  }
56
52
 
57
- /** An unquoted value is cut at a `#`. A `#` after a space is a comment, but one inside the value, as in `abc#def`, would cut a password short and send that to a device, so it stops the run and asks for quotes. */
58
- private static refuseCutValues(text: string): void {
59
- for (const line of text.split(/\r?\n/)) {
60
- const [, name, value] = /^\s*(?:export\s+)?([\w.-]+)\s*=\s*(.*)$/.exec(line) ?? [];
61
- if (!name || !value || /^['"`]/.test(value) || !/\S#/.test(value)) continue;
62
-
63
- throw new Error(`.env: the value of ${name} has a # in it, which starts a comment there; put the value in quotes`);
64
- }
53
+ /** The secrets file in use. */
54
+ static file(): SecretsFile {
55
+ return SecretsFile.at(Secrets.root, Secrets.fileName);
65
56
  }
66
57
 
67
- /** Read a 1Password reference with the 1Password CLI, `op`, once per run. */
68
- private static readOnePassword(name: string, reference: string): string {
69
- const cached = Secrets.fromOnePassword.get(reference);
70
- if (cached !== undefined) return cached;
58
+ /** The secrets file's values as written, read once. A `#` starts a comment unless the value is quoted. */
59
+ private static stored(): Record<string, string> {
60
+ if (Secrets.values) return Secrets.values;
71
61
 
72
- const result = spawnSync("op", ["read", "--no-newline", reference], { encoding: "utf8" });
62
+ const file = Secrets.file();
63
+ Secrets.refuseCutValues(file.exists() ? readFileSync(file.path, "utf8") : "", file.name);
64
+ Secrets.values = file.values();
65
+ return Secrets.values;
66
+ }
73
67
 
74
- if (result.error) throw new Error(`secret ${name} is a 1Password reference, and the 1Password CLI, op, is not installed`);
68
+ /** An unquoted value is cut at a `#`. A `#` after a space is a comment, but one inside the value, as in `abc#def`, would cut a value short and send that to a device, so it stops the run and asks for quotes. */
69
+ private static refuseCutValues(text: string, name: string): void {
70
+ for (const line of text.split(/\r?\n/)) {
71
+ const [, key, value] = /^\s*(?:export\s+)?([\w.-]+)\s*=\s*(.*)$/.exec(line) ?? [];
72
+ if (!key || !value || /^['"`]/.test(value) || !/\S#/.test(value)) continue;
75
73
 
76
- if (result.status !== 0) {
77
- const reason = result.stderr.trim() || `op exited with ${result.status}`;
78
- throw new Error(`secret ${name}: 1Password could not read ${reference}: ${reason}`);
74
+ throw new Error(`${name}: the value of ${key} has a # in it, which starts a comment there; put the value in quotes`);
79
75
  }
80
-
81
- const value = result.stdout;
82
- Secrets.fromOnePassword.set(reference, value);
83
- return value;
84
76
  }
85
77
 
86
78
  static isSecret(value: unknown): value is Secret {
@@ -92,18 +84,16 @@ export class Secrets {
92
84
  return "secret" in secret ? secret.secret : `file:${secret.secret_file}`;
93
85
  }
94
86
 
95
- /** Where a reference's value comes from, such as `.env` or `1Password, through .env`, without reading it. Undefined when it is set nowhere. */
87
+ /** Where a reference's value comes from, such as `.circuit/secrets, encrypted` or `1Password, through the environment`, without reading it. Undefined when it is set nowhere. */
96
88
  static sourceOf(reference: string): string | undefined {
97
89
  if (reference.startsWith("file:")) return existsSync(join(Secrets.root, reference.slice("file:".length))) ? "a file" : undefined;
98
90
 
99
- const fromFile = Secrets.dotenv()[reference];
100
- const value = process.env[reference] || fromFile;
91
+ const where = process.env[reference] ? "the environment" : Secrets.file().name;
92
+ const value = process.env[reference] || Secrets.stored()[reference];
101
93
  if (!value) return undefined;
102
94
 
103
- // Bun loads `.env` into the environment when it starts in the same directory, so the same value in both came from `.env`.
104
- const where = process.env[reference] && process.env[reference] !== fromFile ? "the environment" : ".env";
105
-
106
- return value.startsWith("op://") ? `1Password, through ${where}` : where;
95
+ if (value.startsWith("op://")) return `1Password, through ${where}`;
96
+ return SecretsFile.isEncrypted(value) ? `${where}, encrypted` : where;
107
97
  }
108
98
 
109
99
  /** The value behind a reference, as `referenceOf` writes it: one marker's reference, for a platform that takes a secret interactively. */
@@ -120,17 +110,26 @@ export class Secrets {
120
110
  return value;
121
111
  }
122
112
 
123
- // `.env` is read first even when the environment has the value, so a value cut at a `#` is caught under Bun too, which loads `.env` itself.
124
- const fromFile = Secrets.dotenv()[reference];
125
- const value = process.env[reference] || fromFile;
126
- if (!value) throw new Error(`secret ${reference} is not set in the environment or in .env`);
113
+ // The file is read first even when the environment has the value, so a value cut at a `#` is caught anyway.
114
+ const fromFile = Secrets.stored()[reference];
115
+ const fromEnvironment = process.env[reference];
116
+ if (!fromEnvironment && fromFile === undefined)
117
+ throw new Error(
118
+ `secret ${reference} is not set in ${Secrets.file().name}: set it with ${SecretsFile.command(Secrets.file().name, `set ${reference}`)}`
119
+ );
120
+
121
+ const revealed = fromEnvironment
122
+ ? fromEnvironment.startsWith("op://")
123
+ ? OnePassword.read(fromEnvironment, `secret ${reference}`)
124
+ : fromEnvironment
125
+ : Secrets.file().decrypt(reference, fromFile!);
126
+ if (!revealed) throw new Error(`secret ${reference} is empty in ${Secrets.file().name}`);
127
127
 
128
- const revealed = value.startsWith("op://") ? Secrets.readOnePassword(reference, value) : value;
129
128
  Secrets.revealed.add(revealed);
130
129
  return revealed;
131
130
  }
132
131
 
133
- /** The value of a secret, or of a variable named directly: from the environment, `.env`, 1Password or a file. */
132
+ /** The value of a secret, or of a variable named directly: from the environment, the secrets file, 1Password or a file. */
134
133
  static reveal(secret: Secret | string): string {
135
134
  return Secrets.valueOf(typeof secret === "string" ? secret : Secrets.referenceOf(secret));
136
135
  }
@@ -144,16 +143,35 @@ export class Secrets {
144
143
  return found;
145
144
  }
146
145
 
146
+ /**
147
+ * The secrets file when a secret of `value`, such as a device, is encrypted in it and no password is at hand: the person has to type
148
+ * it before the work starts. Undefined when every secret can be read. A password in the environment or 1Password opens the file here.
149
+ */
150
+ static lockedFor(value: unknown): SecretsFile | undefined {
151
+ const file = Secrets.file();
152
+ const needed = [...Secrets.referencesIn(value)].some(
153
+ (reference) =>
154
+ !reference.startsWith("file:") && !process.env[reference] && SecretsFile.isEncrypted(Secrets.stored()[reference] ?? "")
155
+ );
156
+ return needed && !file.tryUnlock() ? file : undefined;
157
+ }
158
+
147
159
  /**
148
160
  * Text with every secret value Circuit knows replaced by `<redacted>`, before it is printed. Devices echo what they are sent, passwords too.
149
161
  *
150
- * Hidden are: each value in `.env` of at least six characters, each value read in this run (1Password reads and secret files among them), each secret of `network` that the environment, `.env` or a file holds, and `extra`. A 1Password reference not read in this run is not read for this.
162
+ * Hidden are: each value in plain text in the secrets file of at least six characters, each value read in this run (1Password reads and secret files among them), each secret of `network` that the environment, the secrets file or a file holds, and `extra`. A 1Password reference not read in this run is not read for this.
151
163
  */
152
164
  static scrub(text: string, options: { network?: Network; extra?: readonly string[] } = {}): string {
153
165
  const values = new Set<string>(options.extra ?? []);
154
166
 
155
- for (const value of Object.values(Secrets.dotenv())) {
156
- if (value.length >= Secrets.SHORTEST_DOTENV_VALUE && !value.startsWith("op://")) values.add(value);
167
+ // An encrypted value or a reference is not the secret itself.
168
+ for (const [name, value] of Object.entries(Secrets.stored())) {
169
+ if (
170
+ name !== SecretsFile.PASSWORD &&
171
+ value.length >= Secrets.SHORTEST_PLAIN_VALUE &&
172
+ SecretsFile.kindOf(value) === "not encrypted"
173
+ )
174
+ values.add(value);
157
175
  }
158
176
 
159
177
  for (const value of Secrets.revealed) values.add(value);
@@ -168,22 +186,23 @@ export class Secrets {
168
186
  return ordered.reduce((out, value) => out.split(value).join(Secrets.REDACTED), text);
169
187
  }
170
188
 
171
- /** The value behind a reference when it can be had without asking anyone: a 1Password reference only once read in this run. Undefined otherwise. */
189
+ /** The value behind a reference when it can be had without asking anyone: a 1Password reference only once read in this run, an encrypted value only with the file open. Undefined otherwise. */
172
190
  private static storedValueOf(reference: string): string | undefined {
173
191
  if (reference.startsWith("file:")) {
174
192
  const path = join(Secrets.root, reference.slice("file:".length));
175
193
  return existsSync(path) ? readFileSync(path, "utf8").trimEnd() : undefined;
176
194
  }
177
195
 
178
- const value = process.env[reference] || Secrets.dotenv()[reference];
179
- if (!value?.startsWith("op://")) return value;
196
+ const fromEnvironment = process.env[reference];
197
+ if (fromEnvironment) return fromEnvironment.startsWith("op://") ? OnePassword.cached(fromEnvironment) : fromEnvironment;
180
198
 
181
- return Secrets.fromOnePassword.get(value);
199
+ const value = Secrets.stored()[reference];
200
+ return value ? Secrets.file().revealIfOpen(reference, value) : undefined;
182
201
  }
183
202
 
184
203
  /** How Circuit logs in to a device: its address, port, connection user, that user's password revealed, and its console. */
185
204
  static loginOf(device: Device): DeviceLogin {
186
- const user = device.users?.[device.connection.user];
205
+ const user = device.system?.users?.[device.connection.user];
187
206
  if (!user) throw new Error(`${device.name}: connection user ${device.connection.user} is not one of its users`);
188
207
 
189
208
  return {