@takodotid/circuit 0.43.0 → 1.0.0-rc.1

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 (257) 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 +67 -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 +348 -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/plan.ts +132 -44
  63. package/src/platforms/fortios/render.ts +69 -62
  64. package/src/platforms/fortios/routing.ts +89 -58
  65. package/src/platforms/fortios/vdoms.ts +47 -16
  66. package/src/platforms/junos/TRAPS.md +2 -2
  67. package/src/platforms/junos/bgp.ts +63 -42
  68. package/src/platforms/junos/index.ts +122 -89
  69. package/src/platforms/junos/render.ts +139 -104
  70. package/src/platforms/platform.ts +10 -8
  71. package/src/platforms/raisecom-ros/TRAPS.md +1 -1
  72. package/src/platforms/raisecom-ros/index.ts +89 -70
  73. package/src/platforms/raisecom-ros/render.ts +49 -45
  74. package/src/platforms/routeros/TRAPS.md +3 -3
  75. package/src/platforms/routeros/index.ts +121 -60
  76. package/src/platforms/routeros/render.ts +214 -195
  77. package/src/platforms/vrp/TRAPS.md +1 -1
  78. package/src/platforms/vrp/index.ts +74 -50
  79. package/src/platforms/vrp/render.ts +67 -56
  80. package/src/project/project.ts +725 -0
  81. package/src/studio/app/env.d.ts +9 -0
  82. package/src/studio/app/index.html +18 -0
  83. package/src/studio/app/src/App.vue +95 -0
  84. package/src/studio/app/src/components/AddressesView.vue +38 -0
  85. package/src/studio/app/src/components/CablesView.vue +29 -0
  86. package/src/studio/app/src/components/CompareView.vue +160 -0
  87. package/src/studio/app/src/components/DataTable.vue +49 -0
  88. package/src/studio/app/src/components/DeviceView.vue +237 -0
  89. package/src/studio/app/src/components/DevicesView.vue +32 -0
  90. package/src/studio/app/src/components/Faceplate.vue +75 -0
  91. package/src/studio/app/src/components/GraphView.vue +282 -0
  92. package/src/studio/app/src/components/LineDiff.vue +24 -0
  93. package/src/studio/app/src/components/ProblemsView.vue +42 -0
  94. package/src/studio/app/src/components/SearchView.vue +72 -0
  95. package/src/studio/app/src/components/SecretsView.vue +112 -0
  96. package/src/studio/app/src/components/SessionsView.vue +35 -0
  97. package/src/studio/app/src/components/Settings.vue +95 -0
  98. package/src/studio/app/src/components/ToolsView.vue +118 -0
  99. package/src/studio/app/src/components/VlansView.vue +50 -0
  100. package/src/studio/app/src/lib/csv.ts +19 -0
  101. package/src/studio/{public/diff.js → app/src/lib/diff.ts} +40 -35
  102. package/src/studio/app/src/lib/format.ts +38 -0
  103. package/src/studio/app/src/lib/net.ts +68 -0
  104. package/src/studio/app/src/lib/secrets-crypto.ts +35 -0
  105. package/src/studio/app/src/lib/store.ts +71 -0
  106. package/src/studio/app/src/lib/tools.ts +217 -0
  107. package/src/studio/app/src/main.ts +9 -0
  108. package/src/studio/{public → app/src}/style.css +14 -4
  109. package/src/studio/app/tsconfig.json +17 -0
  110. package/src/studio/app/vite.config.ts +16 -0
  111. package/src/studio/assets.generated.ts +3 -0
  112. package/src/studio/data.ts +96 -188
  113. package/src/studio/git.ts +27 -43
  114. package/src/studio/server.ts +196 -107
  115. package/src/studio/types.ts +141 -0
  116. package/src/transport/console.ts +3 -3
  117. package/src/transport/ssh.ts +1 -1
  118. package/dist/chunks/index-3b343xva.js +0 -177
  119. package/dist/chunks/index-6wb41q67.js +0 -62
  120. package/dist/chunks/index-n7w3tg5y.js +0 -149
  121. package/dist/chunks/index-yzw86rc8.js +0 -921
  122. package/dist/chunks/new-w3zys5y4.js +0 -172
  123. package/dist/chunks/server-cxr9ynxc.js +0 -259
  124. package/dist/cli/index.js +0 -12872
  125. package/dist/helpers/index.js +0 -881
  126. package/dist/index.js +0 -28
  127. package/dist/types/core/address-space.d.ts +0 -62
  128. package/dist/types/core/address.d.ts +0 -44
  129. package/dist/types/core/cabling.d.ts +0 -41
  130. package/dist/types/core/cross-checks.d.ts +0 -28
  131. package/dist/types/core/define.d.ts +0 -79
  132. package/dist/types/core/project-files.d.ts +0 -12
  133. package/dist/types/core/schema-docs.d.ts +0 -25
  134. package/dist/types/core/secrets.d.ts +0 -62
  135. package/dist/types/core/topology.d.ts +0 -96
  136. package/dist/types/core/utils.d.ts +0 -10
  137. package/dist/types/core/validator.d.ts +0 -23
  138. package/dist/types/helpers/bgp.d.ts +0 -195
  139. package/dist/types/helpers/cables.d.ts +0 -69
  140. package/dist/types/helpers/checks.d.ts +0 -36
  141. package/dist/types/helpers/exchange.d.ts +0 -55
  142. package/dist/types/helpers/filters.d.ts +0 -51
  143. package/dist/types/helpers/index.d.ts +0 -14
  144. package/dist/types/helpers/route-server.d.ts +0 -52
  145. package/dist/types/helpers/upstreams.d.ts +0 -192
  146. package/dist/types/index.d.ts +0 -10
  147. package/dist/types/integrations/librenms.d.ts +0 -60
  148. package/dist/types/integrations/netbox.d.ts +0 -89
  149. package/dist/types/integrations/peeringdb.d.ts +0 -24
  150. package/dist/types/integrations/prometheus.d.ts +0 -24
  151. package/dist/types/integrations/registries.d.ts +0 -8
  152. package/dist/types/platforms/bgp-wait.d.ts +0 -24
  153. package/dist/types/platforms/catalog.d.ts +0 -109
  154. package/dist/types/platforms/derive.d.ts +0 -17
  155. package/dist/types/platforms/fortios/firewall.d.ts +0 -47
  156. package/dist/types/platforms/fortios/index.d.ts +0 -140
  157. package/dist/types/platforms/fortios/models.d.ts +0 -14
  158. package/dist/types/platforms/fortios/names.d.ts +0 -52
  159. package/dist/types/platforms/fortios/plan.d.ts +0 -104
  160. package/dist/types/platforms/fortios/render.d.ts +0 -77
  161. package/dist/types/platforms/fortios/routing.d.ts +0 -56
  162. package/dist/types/platforms/fortios/tree.d.ts +0 -37
  163. package/dist/types/platforms/fortios/vdoms.d.ts +0 -50
  164. package/dist/types/platforms/index.d.ts +0 -13
  165. package/dist/types/platforms/junos/bgp.d.ts +0 -44
  166. package/dist/types/platforms/junos/index.d.ts +0 -114
  167. package/dist/types/platforms/junos/models.d.ts +0 -14
  168. package/dist/types/platforms/junos/render.d.ts +0 -154
  169. package/dist/types/platforms/line-diff.d.ts +0 -67
  170. package/dist/types/platforms/platform.d.ts +0 -135
  171. package/dist/types/platforms/raisecom-ros/index.d.ts +0 -71
  172. package/dist/types/platforms/raisecom-ros/models.d.ts +0 -14
  173. package/dist/types/platforms/raisecom-ros/render.d.ts +0 -41
  174. package/dist/types/platforms/routeros/export.d.ts +0 -59
  175. package/dist/types/platforms/routeros/index.d.ts +0 -116
  176. package/dist/types/platforms/routeros/models.d.ts +0 -18
  177. package/dist/types/platforms/routeros/plan.d.ts +0 -58
  178. package/dist/types/platforms/routeros/render.d.ts +0 -250
  179. package/dist/types/platforms/vrp/index.d.ts +0 -83
  180. package/dist/types/platforms/vrp/models.d.ts +0 -12
  181. package/dist/types/platforms/vrp/render.d.ts +0 -96
  182. package/dist/types/schema/common.d.ts +0 -30
  183. package/dist/types/schema/device.d.ts +0 -126
  184. package/dist/types/schema/firewall.d.ts +0 -131
  185. package/dist/types/schema/index.d.ts +0 -8
  186. package/dist/types/schema/interfaces.d.ts +0 -205
  187. package/dist/types/schema/policy.d.ts +0 -107
  188. package/dist/types/schema/routing.d.ts +0 -161
  189. package/dist/types/schema/system.d.ts +0 -198
  190. package/dist/types/schema/time-zones.d.ts +0 -2
  191. package/dist/types/transport/console.d.ts +0 -115
  192. package/dist/types/transport/path-check.d.ts +0 -32
  193. package/dist/types/transport/ssh.d.ts +0 -27
  194. package/src/cli/graph-format.ts +0 -91
  195. package/src/cli/new.ts +0 -234
  196. package/src/core/address-space.ts +0 -320
  197. package/src/core/cross-checks.ts +0 -369
  198. package/src/core/define.ts +0 -124
  199. package/src/core/project-files.ts +0 -35
  200. package/src/core/schema-docs.ts +0 -174
  201. package/src/core/validator.ts +0 -508
  202. package/src/helpers/bgp.ts +0 -473
  203. package/src/helpers/cables.ts +0 -74
  204. package/src/helpers/checks.ts +0 -201
  205. package/src/helpers/exchange.ts +0 -123
  206. package/src/helpers/filters.ts +0 -107
  207. package/src/helpers/index.ts +0 -25
  208. package/src/helpers/route-server.ts +0 -212
  209. package/src/helpers/upstreams.ts +0 -369
  210. package/src/index.ts +0 -13
  211. package/src/integrations/librenms.ts +0 -291
  212. package/src/integrations/netbox.ts +0 -620
  213. package/src/integrations/peeringdb.ts +0 -173
  214. package/src/integrations/prometheus.ts +0 -90
  215. package/src/schema/common.ts +0 -41
  216. package/src/schema/device.ts +0 -145
  217. package/src/schema/firewall.ts +0 -140
  218. package/src/schema/index.ts +0 -8
  219. package/src/schema/policy.ts +0 -115
  220. package/src/schema/routing.ts +0 -173
  221. package/src/schema/system.ts +0 -203
  222. package/src/studio/public/app.js +0 -186
  223. package/src/studio/public/compare.js +0 -250
  224. package/src/studio/public/dom.js +0 -138
  225. package/src/studio/public/evaluate.js +0 -687
  226. package/src/studio/public/export.js +0 -130
  227. package/src/studio/public/graph.js +0 -1541
  228. package/src/studio/public/index.html +0 -83
  229. package/src/studio/public/net.js +0 -134
  230. package/src/studio/public/tables.js +0 -1289
  231. package/src/studio/public/tools.js +0 -610
  232. package/templates/colocation/checks.ts +0 -53
  233. package/templates/colocation/circuit.config.ts +0 -9
  234. package/templates/colocation/edge-01.ts +0 -133
  235. package/templates/colocation/routing.ts +0 -13
  236. package/templates/colocation/tenancy.ts +0 -76
  237. package/templates/colocation/tenants/acme.ts +0 -19
  238. package/templates/colocation/tenants/index.ts +0 -6
  239. package/templates/colocation/tor-01.ts +0 -33
  240. package/templates/edge-router/checks.ts +0 -20
  241. package/templates/edge-router/circuit.config.ts +0 -8
  242. package/templates/edge-router/edge-01.ts +0 -110
  243. package/templates/edge-router/routing.ts +0 -13
  244. package/templates/project/AGENTS.md +0 -51
  245. package/templates/project/CLAUDE.md +0 -1
  246. package/templates/project/README.md +0 -16
  247. package/templates/project/gitignore +0 -5
  248. package/templates/project/tsconfig.json +0 -13
  249. package/templates/single-site/checks.ts +0 -26
  250. package/templates/single-site/circuit.config.ts +0 -8
  251. package/templates/single-site/router.ts +0 -86
  252. package/templates/single-site/site.ts +0 -18
  253. package/templates/single-site/switch.ts +0 -34
  254. /package/src/studio/{public → app/public}/favicon.svg +0 -0
  255. /package/src/studio/{public → app/public}/special-gothic-latin-ext.woff2 +0 -0
  256. /package/src/studio/{public → app/public}/special-gothic-latin.woff2 +0 -0
  257. /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 {