@devdogsuga/devtools 0.1.25 → 0.1.27

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 (271) hide show
  1. package/README.md +72 -11
  2. package/bin/devtools.mjs +4 -4
  3. package/dist/cli-CZKkNHK5.js +6619 -0
  4. package/dist/failure-log-BBPGejQv.js +256 -0
  5. package/dist/launch.js +267 -281
  6. package/dist/local-env-D8HC2JK_.js +351 -0
  7. package/dist/local-env-DrBCXqD8.js +2 -0
  8. package/dist/menu-CQBsQRgH.js +1684 -0
  9. package/dist/peer-redirect-hooks.js +15 -0
  10. package/dist/peers-RBplwovA.js +229 -0
  11. package/dist/root-C2OmZDwD.js +94 -0
  12. package/dist/stack-Bx-55oi_.js +2 -0
  13. package/dist/stack-BynCgRVS.js +181 -0
  14. package/dist/supabase-project-0knM0mqd.js +426 -0
  15. package/dist/telemetry-C4foD8MD.js +249 -0
  16. package/dist/telemetry.js +2 -148
  17. package/package.json +10 -29
  18. package/bin/devtools-ci-bare.mjs +0 -48
  19. package/bin/devtools-ci.mjs +0 -31
  20. package/dist/args.d.ts +0 -26
  21. package/dist/args.js +0 -78
  22. package/dist/bws/bw.d.ts +0 -18
  23. package/dist/bws/bw.js +0 -70
  24. package/dist/bws/client.d.ts +0 -43
  25. package/dist/bws/client.js +0 -363
  26. package/dist/bws/environments.d.ts +0 -96
  27. package/dist/bws/environments.js +0 -111
  28. package/dist/bws/pace.d.ts +0 -25
  29. package/dist/bws/pace.js +0 -43
  30. package/dist/bws/retry.d.ts +0 -12
  31. package/dist/bws/retry.js +0 -39
  32. package/dist/bws/store.d.ts +0 -1
  33. package/dist/bws/store.js +0 -32
  34. package/dist/bws/token.d.ts +0 -31
  35. package/dist/bws/token.js +0 -88
  36. package/dist/bws/vault.d.ts +0 -29
  37. package/dist/bws/vault.js +0 -200
  38. package/dist/catalog.d.ts +0 -57
  39. package/dist/catalog.js +0 -57
  40. package/dist/cf/build.d.ts +0 -13
  41. package/dist/cf/build.js +0 -65
  42. package/dist/cf/commands.d.ts +0 -1
  43. package/dist/cf/commands.js +0 -172
  44. package/dist/cf/local-env.d.ts +0 -58
  45. package/dist/cf/local-env.js +0 -177
  46. package/dist/ci.d.ts +0 -1
  47. package/dist/ci.js +0 -328
  48. package/dist/cli.d.ts +0 -11
  49. package/dist/cli.js +0 -1094
  50. package/dist/commands.d.ts +0 -261
  51. package/dist/commands.js +0 -1318
  52. package/dist/completions.d.ts +0 -4
  53. package/dist/completions.js +0 -110
  54. package/dist/cron/commands.d.ts +0 -47
  55. package/dist/cron/commands.js +0 -372
  56. package/dist/cron/discovery.d.ts +0 -55
  57. package/dist/cron/discovery.js +0 -85
  58. package/dist/cron/schema.d.ts +0 -45
  59. package/dist/cron/schema.js +0 -35
  60. package/dist/db/config-push.d.ts +0 -8
  61. package/dist/db/config-push.js +0 -11
  62. package/dist/db/connection.d.ts +0 -79
  63. package/dist/db/connection.js +0 -114
  64. package/dist/db/exec.d.ts +0 -1
  65. package/dist/db/exec.js +0 -11
  66. package/dist/db/generate-types.d.ts +0 -1
  67. package/dist/db/generate-types.js +0 -4
  68. package/dist/db/generated-env.d.ts +0 -69
  69. package/dist/db/generated-env.js +0 -113
  70. package/dist/db/introspect.d.ts +0 -1
  71. package/dist/db/introspect.js +0 -122
  72. package/dist/db/local-env.d.ts +0 -65
  73. package/dist/db/local-env.js +0 -23
  74. package/dist/db/new-migration.d.ts +0 -8
  75. package/dist/db/new-migration.js +0 -69
  76. package/dist/db/pooler.d.ts +0 -23
  77. package/dist/db/pooler.js +0 -55
  78. package/dist/db/run.d.ts +0 -68
  79. package/dist/db/run.js +0 -156
  80. package/dist/db/seed-buckets.d.ts +0 -2
  81. package/dist/db/seed-buckets.js +0 -4
  82. package/dist/db/seed-production.d.ts +0 -8
  83. package/dist/db/seed-production.js +0 -79
  84. package/dist/db/seed-roles.d.ts +0 -7
  85. package/dist/db/seed-roles.js +0 -33
  86. package/dist/db/session-refresh.d.ts +0 -43
  87. package/dist/db/session-refresh.js +0 -40
  88. package/dist/deploy/migrations.d.ts +0 -20
  89. package/dist/deploy/migrations.js +0 -68
  90. package/dist/deploy/orphans.d.ts +0 -20
  91. package/dist/deploy/orphans.js +0 -177
  92. package/dist/deploy/preflight.d.ts +0 -53
  93. package/dist/deploy/preflight.js +0 -189
  94. package/dist/deploy/report.d.ts +0 -26
  95. package/dist/deploy/report.js +0 -70
  96. package/dist/deploy/require-planner.d.ts +0 -2
  97. package/dist/deploy/require-planner.js +0 -60
  98. package/dist/deploy/require-token.d.ts +0 -15
  99. package/dist/deploy/require-token.js +0 -78
  100. package/dist/deploy/secrets-file.d.ts +0 -17
  101. package/dist/deploy/secrets-file.js +0 -166
  102. package/dist/deploy/write-env.d.ts +0 -40
  103. package/dist/deploy/write-env.js +0 -395
  104. package/dist/docs/index-pages.d.ts +0 -38
  105. package/dist/docs/index-pages.js +0 -186
  106. package/dist/emails/commands.d.ts +0 -15
  107. package/dist/emails/commands.js +0 -130
  108. package/dist/emails/email-types.d.ts +0 -13
  109. package/dist/emails/email-types.js +0 -1
  110. package/dist/emails/fixtures.d.ts +0 -20
  111. package/dist/emails/fixtures.js +0 -20
  112. package/dist/env/audit.d.ts +0 -206
  113. package/dist/env/audit.js +0 -436
  114. package/dist/env/cloudflare.d.ts +0 -21
  115. package/dist/env/cloudflare.js +0 -61
  116. package/dist/env/commands.d.ts +0 -99
  117. package/dist/env/commands.js +0 -613
  118. package/dist/env/discovery.d.ts +0 -15
  119. package/dist/env/discovery.js +0 -239
  120. package/dist/env/document.d.ts +0 -119
  121. package/dist/env/document.js +0 -323
  122. package/dist/env/example.d.ts +0 -68
  123. package/dist/env/example.js +0 -605
  124. package/dist/env/selection.d.ts +0 -105
  125. package/dist/env/selection.js +0 -228
  126. package/dist/env-entry.d.ts +0 -49
  127. package/dist/env-entry.js +0 -204
  128. package/dist/environment-doctor.d.ts +0 -45
  129. package/dist/environment-doctor.js +0 -429
  130. package/dist/environment.d.ts +0 -92
  131. package/dist/environment.js +0 -185
  132. package/dist/fingerprint.d.ts +0 -10
  133. package/dist/fingerprint.js +0 -18
  134. package/dist/gen/campus-map.d.ts +0 -1
  135. package/dist/gen/campus-map.js +0 -473
  136. package/dist/gen/commands.d.ts +0 -1
  137. package/dist/gen/commands.js +0 -25
  138. package/dist/gen/hypno.d.ts +0 -1
  139. package/dist/gen/hypno.js +0 -134
  140. package/dist/gh/client.d.ts +0 -80
  141. package/dist/gh/client.js +0 -259
  142. package/dist/gh/environments.d.ts +0 -83
  143. package/dist/gh/environments.js +0 -178
  144. package/dist/gh/rulesets/actors.d.ts +0 -14
  145. package/dist/gh/rulesets/actors.js +0 -69
  146. package/dist/gh/rulesets/api.d.ts +0 -15
  147. package/dist/gh/rulesets/api.js +0 -104
  148. package/dist/gh/rulesets/commands.d.ts +0 -3
  149. package/dist/gh/rulesets/commands.js +0 -203
  150. package/dist/gh/rulesets/desired.d.ts +0 -112
  151. package/dist/gh/rulesets/desired.js +0 -271
  152. package/dist/gh/rulesets/diff.d.ts +0 -65
  153. package/dist/gh/rulesets/diff.js +0 -170
  154. package/dist/gh/rulesets/types.d.ts +0 -76
  155. package/dist/gh/rulesets/types.js +0 -13
  156. package/dist/gh/settings/api.d.ts +0 -40
  157. package/dist/gh/settings/api.js +0 -180
  158. package/dist/gh/settings/commands.d.ts +0 -1
  159. package/dist/gh/settings/commands.js +0 -254
  160. package/dist/gh/settings/desired.d.ts +0 -53
  161. package/dist/gh/settings/desired.js +0 -43
  162. package/dist/gh/settings/diff.d.ts +0 -57
  163. package/dist/gh/settings/diff.js +0 -161
  164. package/dist/gh/settings/types.d.ts +0 -92
  165. package/dist/gh/settings/types.js +0 -11
  166. package/dist/gh/settings/workflows.d.ts +0 -41
  167. package/dist/gh/settings/workflows.js +0 -128
  168. package/dist/grantRoot.d.ts +0 -42
  169. package/dist/grantRoot.js +0 -89
  170. package/dist/help.d.ts +0 -18
  171. package/dist/help.js +0 -169
  172. package/dist/images/commands.d.ts +0 -32
  173. package/dist/images/commands.js +0 -231
  174. package/dist/images/events.d.ts +0 -47
  175. package/dist/images/events.js +0 -116
  176. package/dist/images/graphics.d.ts +0 -70
  177. package/dist/images/graphics.js +0 -234
  178. package/dist/images/open-graph-types.d.ts +0 -75
  179. package/dist/images/open-graph-types.js +0 -1
  180. package/dist/images/prompts.d.ts +0 -34
  181. package/dist/images/prompts.js +0 -100
  182. package/dist/images/render.d.ts +0 -21
  183. package/dist/images/render.js +0 -61
  184. package/dist/images/select.d.ts +0 -64
  185. package/dist/images/select.js +0 -96
  186. package/dist/instance.d.ts +0 -69
  187. package/dist/instance.js +0 -102
  188. package/dist/invocation.d.ts +0 -49
  189. package/dist/invocation.js +0 -78
  190. package/dist/launch-ci.d.ts +0 -1
  191. package/dist/launch-ci.js +0 -90
  192. package/dist/launch.d.ts +0 -18
  193. package/dist/menu.d.ts +0 -43
  194. package/dist/menu.js +0 -300
  195. package/dist/moderation.d.ts +0 -70
  196. package/dist/moderation.js +0 -128
  197. package/dist/oauth/browser.d.ts +0 -1
  198. package/dist/oauth/browser.js +0 -13
  199. package/dist/oauth/config.d.ts +0 -24
  200. package/dist/oauth/config.js +0 -24
  201. package/dist/oauth/connect-url.d.ts +0 -17
  202. package/dist/oauth/connect-url.js +0 -23
  203. package/dist/oauth/db.d.ts +0 -37
  204. package/dist/oauth/db.js +0 -108
  205. package/dist/oauth/device.d.ts +0 -93
  206. package/dist/oauth/device.js +0 -198
  207. package/dist/oauth/discovery.d.ts +0 -28
  208. package/dist/oauth/discovery.js +0 -54
  209. package/dist/oauth/env-file.d.ts +0 -6
  210. package/dist/oauth/env-file.js +0 -35
  211. package/dist/oauth/exchange.d.ts +0 -43
  212. package/dist/oauth/exchange.js +0 -93
  213. package/dist/oauth/label.d.ts +0 -1
  214. package/dist/oauth/label.js +0 -25
  215. package/dist/oauth/loopback.d.ts +0 -52
  216. package/dist/oauth/loopback.js +0 -168
  217. package/dist/oauth/pkce.d.ts +0 -6
  218. package/dist/oauth/pkce.js +0 -34
  219. package/dist/oauth/target.d.ts +0 -54
  220. package/dist/oauth/target.js +0 -102
  221. package/dist/oauth/transport.d.ts +0 -41
  222. package/dist/oauth/transport.js +0 -32
  223. package/dist/oauth/wizard.d.ts +0 -40
  224. package/dist/oauth/wizard.js +0 -578
  225. package/dist/persona.d.ts +0 -18
  226. package/dist/persona.js +0 -221
  227. package/dist/pick.d.ts +0 -6
  228. package/dist/pick.js +0 -64
  229. package/dist/pipes.d.ts +0 -1
  230. package/dist/pipes.js +0 -25
  231. package/dist/planner/checks.d.ts +0 -26
  232. package/dist/planner/checks.js +0 -104
  233. package/dist/planner/commands.d.ts +0 -10
  234. package/dist/planner/commands.js +0 -367
  235. package/dist/planner/db.d.ts +0 -15
  236. package/dist/planner/db.js +0 -33
  237. package/dist/planner/role.d.ts +0 -33
  238. package/dist/planner/role.js +0 -68
  239. package/dist/repo/peer-redirect-hooks.d.ts +0 -16
  240. package/dist/repo/peer-redirect-hooks.js +0 -10
  241. package/dist/repo/peer-redirect.d.ts +0 -4
  242. package/dist/repo/peer-redirect.js +0 -26
  243. package/dist/repo/peers.d.ts +0 -39
  244. package/dist/repo/peers.js +0 -139
  245. package/dist/repo/resolve.d.ts +0 -41
  246. package/dist/repo/resolve.js +0 -152
  247. package/dist/repo/root.d.ts +0 -23
  248. package/dist/repo/root.js +0 -91
  249. package/dist/repo/source.d.ts +0 -14
  250. package/dist/repo/source.js +0 -54
  251. package/dist/repo/supabase-project.d.ts +0 -60
  252. package/dist/repo/supabase-project.js +0 -124
  253. package/dist/repo/tsx-loader.d.ts +0 -6
  254. package/dist/repo/tsx-loader.js +0 -41
  255. package/dist/run/pick.d.ts +0 -90
  256. package/dist/run/pick.js +0 -664
  257. package/dist/setup.d.ts +0 -1
  258. package/dist/setup.js +0 -280
  259. package/dist/stack.d.ts +0 -48
  260. package/dist/stack.js +0 -403
  261. package/dist/telemetry.d.ts +0 -59
  262. package/dist/tier.d.ts +0 -40
  263. package/dist/tier.js +0 -96
  264. package/dist/ui.d.ts +0 -40
  265. package/dist/ui.js +0 -71
  266. package/dist/version.d.ts +0 -7
  267. package/dist/version.js +0 -17
  268. package/dist/workers.d.ts +0 -5
  269. package/dist/workers.js +0 -33
  270. package/dist/workflows/commands.d.ts +0 -54
  271. package/dist/workflows/commands.js +0 -763
package/dist/env/audit.js DELETED
@@ -1,436 +0,0 @@
1
- /**
2
- * Four-way drift detection: local `.env`, Bitwarden, GitHub, Cloudflare.
3
- *
4
- * Each store answers a different question, and only one of them answers it
5
- * completely:
6
- *
7
- * | Store | Exposes | So it can be checked for |
8
- * |--------------------|-------------------|--------------------------|
9
- * | local .env | names AND values | value drift |
10
- * | Bitwarden | names AND values | value drift (the truth) |
11
- * | GitHub *secrets* | names + updatedAt | presence, staleness |
12
- * | GitHub *variables* | names AND values | value drift |
13
- * | Cloudflare | names only | presence, orphans |
14
- *
15
- * That asymmetry is the whole design. A GitHub *secret* is write-only, so a
16
- * clean audit of one does not mean "the values match". It means "nothing
17
- * detectable is wrong", and the report says which is which rather than implying
18
- * the stronger claim.
19
- *
20
- * Timestamps are what rescue that row from being presence-only. Bitwarden
21
- * reports a `revisionDate` per secret and `gh secret list` reports `updatedAt`,
22
- * so "was GitHub updated after Bitwarden last changed?" IS answerable. That is
23
- * the failure this design actually has: a credential rotated in Bitwarden and
24
- * never propagated, which a name-only check calls healthy right up until the old
25
- * one is revoked.
26
- *
27
- * ⚠️ GitHub *variables* are the one downstream store that needs none of that
28
- * reasoning: `gh variable list` returns the value, so the public per-environment
29
- * keys are compared exactly the way the local file is. Staleness is deliberately
30
- * NOT applied to them: a timestamp is a proxy for a comparison that could not
31
- * be made, and using the proxy where the real answer is available would report a
32
- * variable re-pushed with an identical value as drift. The security plan's §3.6
33
- * limitation ("names only… a changed value is undetectable") now holds for the
34
- * secret row alone.
35
- *
36
- * ⚠️ One scope sits outside that table altogether: the repository's OWN
37
- * variables. Push writes environment-scoped values only, and an environment
38
- * variable SHADOWS a repository one of the same name, so a repository-level
39
- * copy of a managed key is read by nothing, drifts forever, and becomes the
40
- * live value on the day somebody deletes the environment copy. It is checked
41
- * here for the same reason §3.6's orphans are: nothing manages it, so nothing
42
- * else in this system would ever mention it.
43
- */
44
- export function audit(input) {
45
- const ignore = input.ignore ?? new Set();
46
- const neverStore = input.neverStore ?? new Set();
47
- const minted = input.minted ?? new Set();
48
- const commented = input.localCommented ?? new Set();
49
- const variables = input.variables ?? new Set();
50
- const githubVariables = input.githubVariables ?? [];
51
- const findings = [];
52
- const relevant = (key) => !ignore.has(key) && !neverStore.has(key) && !minted.has(key);
53
- // ── must not be stored anywhere remote ─────────────────────────────────────
54
- // Checked first and reported as errors. `BWS_ACCESS_TOKEN` in a Bitwarden
55
- // project is a key locked inside the box it opens; in GitHub it would give CI
56
- // the ability to read every secret we have.
57
- for (const key of neverStore) {
58
- if (input.bws.has(key)) {
59
- findings.push({
60
- key,
61
- severity: "error",
62
- store: "local",
63
- summary: "must NEVER be stored in Bitwarden — delete it from the project; " +
64
- "it belongs in the Password Manager vault, exported per shell",
65
- });
66
- }
67
- for (const copy of input.github.filter((g) => g.name === key)) {
68
- findings.push({
69
- key,
70
- severity: "error",
71
- store: "github",
72
- summary: `must NEVER be a GitHub secret — delete it from \`${copy.environment}\`; ` +
73
- "nothing machine-shaped may authenticate to Secrets Manager",
74
- });
75
- }
76
- for (const [worker, names] of input.cloudflare ?? []) {
77
- if (!names.has(key))
78
- continue;
79
- findings.push({
80
- key,
81
- severity: "error",
82
- store: "cloudflare",
83
- summary: `must NEVER be a Worker secret — delete it from ${worker}`,
84
- });
85
- }
86
- }
87
- // ── minted at deploy time ──────────────────────────────────────────────────
88
- // Cloudflare is deliberately NOT checked: that is where these belong, and
89
- // saying so is the entire point of the category. What IS checked is the two
90
- // stores a minted credential must never reach, because a copy in either is a
91
- // long-lived token nobody rotates sitting beside one that rotates every
92
- // deploy, and the stale copy is the one an operator will reach for when
93
- // something breaks.
94
- for (const key of minted) {
95
- if (input.bws.has(key)) {
96
- findings.push({
97
- key,
98
- severity: "error",
99
- store: "local",
100
- summary: "is minted at deploy time and must not be stored in Bitwarden — " +
101
- "delete it from the project; the deploy signs a fresh one and " +
102
- "writes it straight to the Worker",
103
- });
104
- }
105
- for (const copy of input.github.filter((g) => g.name === key)) {
106
- findings.push({
107
- key,
108
- severity: "error",
109
- store: "github",
110
- summary: `is minted at deploy time and must not be a GitHub secret — delete ` +
111
- `it from \`${copy.environment}\`; what CI needs is the signing key, ` +
112
- "not a token somebody minted once",
113
- });
114
- }
115
- }
116
- // ── in the wrong GitHub store ──────────────────────────────────────────────
117
- // Errors, and reported before the drift passes, because both directions are
118
- // silent misconfigurations that a presence check calls healthy: the NAME is
119
- // in GitHub either way.
120
- for (const copy of input.github) {
121
- if (!relevant(copy.name) || !variables.has(copy.name))
122
- continue;
123
- findings.push({
124
- key: copy.name,
125
- severity: "error",
126
- store: "github",
127
- summary: `is public and is a SECRET on \`${copy.environment}\` — delete it and ` +
128
- "let push set it as a variable; GitHub masks a secret's value in logs " +
129
- "by substring, which redacts the very links and hostnames it appears in",
130
- });
131
- }
132
- for (const copy of githubVariables) {
133
- if (!relevant(copy.name) || variables.has(copy.name))
134
- continue;
135
- // Only for a key the registry calls a secret. An unrecognised name in the
136
- // variable store is an orphan from a rename, reported as one below.
137
- // Calling it a leaked secret would be a guess, and a loud wrong one. With
138
- // no declared set there is no way to tell the two apart, so neither fires.
139
- if (!input.declared?.has(copy.name))
140
- continue;
141
- findings.push({
142
- key: copy.name,
143
- severity: "error",
144
- store: "github",
145
- summary: `is a secret and is a VARIABLE on \`${copy.environment}\` — its value ` +
146
- "is readable by anyone who can see the repository's Actions config, " +
147
- "and it is not masked in logs. Delete it there and rotate it",
148
- });
149
- }
150
- // ── repository-level variables ─────────────────────────────────────────────
151
- // The scope push never writes to, and the one a presence check cannot see
152
- // from inside an environment.
153
- //
154
- // ⚠️ An ENVIRONMENT variable SHADOWS a repository variable of the same name.
155
- // So a repository-level `AIRTABLE_BASE_ID`, which people were told to set by
156
- // hand before push started routing that key, is not merely redundant: it is
157
- // unreadable from every job, holds whatever it held the day it was set, and
158
- // becomes live the moment somebody deletes the environment copy. Nothing
159
- // manages it and nothing else here would ever mention it, which makes it the
160
- // same failure as the §3.6 orphan audit rather than a new one.
161
- const repository = input.repositoryVariables;
162
- if (repository !== undefined && !repository.readable) {
163
- // A finding, in the same list as the rest, rather than a line printed
164
- // underneath it: the whole hazard of this check is that its absence looks
165
- // like health, and a clean report with a quiet caveat below it reads as
166
- // clean. The key is parenthesised so it cannot be mistaken for one.
167
- findings.push({
168
- key: "(repository variables)",
169
- severity: "warning",
170
- store: "github",
171
- summary: "could not check — listing the repository's own variables failed " +
172
- `(${repository.reason}). This run rules out NOTHING at that scope; a ` +
173
- "repository variable shadowed by an environment copy would look " +
174
- "exactly like this report. Run `gh variable list` by hand",
175
- });
176
- }
177
- const repositoryNames = repository?.readable ? repository.names : [];
178
- for (const name of repositoryNames) {
179
- // Membership in `variables` IS the declaration test for this branch, since
180
- // the set comes from the registry, which is why it does not also consult
181
- // `declared`. A caller that passed one and forgot the other would
182
- // otherwise get silence from the check it asked for.
183
- if (variables.has(name)) {
184
- if (!relevant(name))
185
- continue;
186
- findings.push({
187
- key: name,
188
- severity: "warning",
189
- store: "github",
190
- summary: "is also a REPOSITORY-level GitHub variable, which push does not " +
191
- "manage — the environment copy shadows it, so every job reads the " +
192
- "environment value and this one is invisible, stale, and unnoticed " +
193
- "until somebody deletes that copy, whereupon it silently becomes " +
194
- `the live value. Delete it: \`gh variable delete ${name}\``,
195
- });
196
- continue;
197
- }
198
- // A name the registry calls a SECRET, sitting in the readable store one
199
- // scope up. The environment-level version of this is an error above, and
200
- // this is the same finding with the shadowing sentence removed, because
201
- // there is none: secrets and variables are separate namespaces, so nothing
202
- // hides this one.
203
- //
204
- // ⚠️ Filtered on `ignore` alone, deliberately NOT on the whole of
205
- // `relevant()`. An ignored key legitimately lives outside these stores.
206
- // `GITHUB_ORG` is a committed constant, and a repository variable holding
207
- // it is somebody making a reasonable choice. A never-store or minted
208
- // credential in the READABLE store is the worst case this file knows
209
- // about, not an exempt one, so those two fall through to the error.
210
- //
211
- // `declared` is required here for the reason it is required above: without
212
- // it an unrecognised name is indistinguishable from another team's
213
- // variable, and calling that a leaked secret would be a loud guess.
214
- if (ignore.has(name))
215
- continue;
216
- if (!input.declared?.has(name))
217
- continue;
218
- findings.push({
219
- key: name,
220
- severity: "error",
221
- store: "github",
222
- summary: "is a secret and is a REPOSITORY-level GitHub variable — its value " +
223
- "is readable by anyone who can see the repository's Actions config, " +
224
- "and no environment copy hides it: secrets and variables are separate " +
225
- `namespaces. Delete it (\`gh variable delete ${name}\`) and rotate it`,
226
- });
227
- }
228
- // ── undeclared keys ────────────────────────────────────────────────────────
229
- // Reported once, here, and then excluded from the drift comparisons below:
230
- // an undeclared key is invisible to push routing, so "in your .env, not in
231
- // Bitwarden" would be true and useless, since pushing cannot fix it.
232
- const undeclared = (key) => input.declared !== undefined && !input.declared.has(key);
233
- for (const key of input.local.keys()) {
234
- if (!relevant(key) || !undeclared(key))
235
- continue;
236
- findings.push({
237
- key,
238
- severity: "warning",
239
- store: "local",
240
- summary: "in your .env but declared in no env manifest — push skips it. " +
241
- "Declare it with define() in the owning package's env.ts, or remove " +
242
- "the line",
243
- });
244
- }
245
- // ── local vs Bitwarden ─────────────────────────────────────────────────────
246
- // The only VALUE comparison available anywhere in this system.
247
- for (const [key, value] of input.local) {
248
- if (!relevant(key) || undeclared(key))
249
- continue;
250
- const truth = input.bws.get(key);
251
- if (truth === undefined) {
252
- findings.push({
253
- key,
254
- severity: "warning",
255
- store: "local",
256
- summary: "in your .env, not in Bitwarden — either a local-only value or one " +
257
- "somebody forgot to push",
258
- });
259
- }
260
- else if (truth.value !== value) {
261
- findings.push({
262
- key,
263
- severity: "error",
264
- store: "local",
265
- summary: "your .env disagrees with Bitwarden — pull to take theirs, push to " +
266
- "take yours",
267
- });
268
- }
269
- }
270
- for (const key of input.bws.keys()) {
271
- if (!relevant(key))
272
- continue;
273
- if (!input.local.has(key)) {
274
- findings.push({
275
- key,
276
- severity: commented.has(key) ? "info" : "warning",
277
- store: "local",
278
- summary: commented.has(key)
279
- ? "commented out in your .env; Bitwarden has a value"
280
- : "in Bitwarden, missing from your .env",
281
- });
282
- }
283
- }
284
- // ── Bitwarden vs GitHub ────────────────────────────────────────────────────
285
- // One pass over both stores. Which one a key is compared against is decided
286
- // by the registry, not by where the key happened to turn up. Otherwise a
287
- // misplaced copy would define its own correctness and never be reported.
288
- for (const [key, entry] of input.bws) {
289
- if (!relevant(key))
290
- continue;
291
- const expected = input.route(key);
292
- if (expected === null)
293
- continue;
294
- const accepted = input.accepted ??
295
- ((_key, environment) => environment === expected);
296
- const isVariable = variables.has(key);
297
- const noun = isVariable ? "variable" : "secret";
298
- const pool = isVariable
299
- ? githubVariables
300
- : input.github;
301
- const copies = pool.filter((g) => g.name === key);
302
- const here = copies.find((g) => g.environment === expected);
303
- // A copy somewhere it does not belong. Listed FIRST because for the
304
- // apply-only credentials this is the reviewer gate failing open: the token
305
- // is sitting in an environment that deploys with nobody in front of it.
306
- //
307
- // `accepted`, not `!== expected`: `production-apply` legitimately holds a
308
- // second copy of most production keys. An apply-tier key in `production`
309
- // still lands here, which is the case this finding exists for.
310
- for (const stray of copies.filter((g) => !accepted(key, g.environment))) {
311
- findings.push({
312
- key,
313
- severity: "error",
314
- store: "github",
315
- summary: `also set as a ${noun} on \`${stray.environment}\`, which is not ` +
316
- `where it belongs (\`${expected}\`) — delete it there`,
317
- });
318
- }
319
- if (!here) {
320
- findings.push({
321
- key,
322
- severity: "error",
323
- store: "github",
324
- summary: `in Bitwarden, NOT a ${noun} on the \`${expected}\` GitHub ` +
325
- "environment — the deploy cannot see it",
326
- });
327
- continue;
328
- }
329
- // The comparison a secret cannot have. Kept distinct from the missing case
330
- // above on purpose: "absent" is fixed by a push, "drifted" means somebody
331
- // edited the value in the GitHub UI and the two stores now disagree about
332
- // which is real, and the fix has to start by deciding that.
333
- if (isVariable) {
334
- const mine = githubVariables.find((g) => g.name === key && g.environment === expected);
335
- if (mine !== undefined && mine.value !== entry.value) {
336
- findings.push({
337
- key,
338
- severity: "error",
339
- store: "github",
340
- summary: `the \`${expected}\` GitHub variable's VALUE disagrees with ` +
341
- "Bitwarden — push to overwrite GitHub, or fix Bitwarden if the " +
342
- "edit there was the deliberate one",
343
- });
344
- }
345
- continue;
346
- }
347
- if (isStale(entry.revisionDate, here.updatedAt)) {
348
- findings.push({
349
- key,
350
- severity: "error",
351
- store: "github",
352
- summary: "rotated in Bitwarden after GitHub was last updated — the deploy is " +
353
- "still using the previous value",
354
- });
355
- }
356
- }
357
- for (const copy of input.github) {
358
- if (!relevant(copy.name))
359
- continue;
360
- if (!input.bws.has(copy.name)) {
361
- findings.push({
362
- key: copy.name,
363
- severity: "warning",
364
- store: "github",
365
- summary: `in the \`${copy.environment}\` GitHub environment, not in ` +
366
- "Bitwarden — an orphan from a rename or a removal",
367
- });
368
- }
369
- }
370
- for (const copy of githubVariables) {
371
- if (!relevant(copy.name))
372
- continue;
373
- if (!input.bws.has(copy.name)) {
374
- findings.push({
375
- key: copy.name,
376
- severity: "warning",
377
- store: "github",
378
- summary: `a variable on the \`${copy.environment}\` GitHub environment, not ` +
379
- "in Bitwarden — an orphan from a rename or a removal",
380
- });
381
- }
382
- }
383
- // ── Bitwarden vs Cloudflare ────────────────────────────────────────────────
384
- // Presence only, and asymmetric on purpose: a Worker holding a secret nobody
385
- // stores is an orphan worth reporting, but a secret NOT on a given Worker is
386
- // usually correct -- DISCORD_TOKEN belongs to platform and nothing else.
387
- // `--secrets-file` preserves what it omits, so orphans persist indefinitely.
388
- for (const [worker, names] of input.cloudflare ?? []) {
389
- for (const name of names) {
390
- if (!relevant(name))
391
- continue;
392
- if (!input.bws.has(name)) {
393
- findings.push({
394
- key: name,
395
- severity: "warning",
396
- store: "cloudflare",
397
- summary: `on ${worker}, not in Bitwarden — a renamed or dropped variable leaves its secret behind`,
398
- });
399
- }
400
- }
401
- }
402
- return findings.sort((a, b) => rank(a.severity) - rank(b.severity) || a.key.localeCompare(b.key));
403
- }
404
- /**
405
- * True when GitHub's copy predates the Bitwarden revision.
406
- *
407
- * A missing or unparseable date on either side returns false. A comparison
408
- * against `NaN` is false anyway, so this only makes that explicit. The reason
409
- * to be explicit is that the alternative reading, "unknown means stale",
410
- * turns one malformed timestamp into a report that says everything is behind,
411
- * after which nobody reads it, including on the run where something really is.
412
- */
413
- function isStale(revisionDate, updatedAt) {
414
- if (!revisionDate || !updatedAt)
415
- return false;
416
- const revised = Date.parse(revisionDate);
417
- const updated = Date.parse(updatedAt);
418
- if (Number.isNaN(revised) || Number.isNaN(updated))
419
- return false;
420
- return updated < revised;
421
- }
422
- function rank(s) {
423
- return s === "error" ? 0 : s === "warning" ? 1 : 2;
424
- }
425
- export function hasErrors(findings) {
426
- return findings.some((f) => f.severity === "error");
427
- }
428
- export function renderFindings(findings) {
429
- if (findings.length === 0) {
430
- return "No detectable drift.";
431
- }
432
- const mark = { error: "✗", warning: "!", info: "·" };
433
- return findings
434
- .map((f) => `${mark[f.severity]} ${f.key} ${f.summary}`)
435
- .join("\n");
436
- }
@@ -1,21 +0,0 @@
1
- import { workerApps } from "../workers.js";
2
- /** Re-exported so existing importers of this module (e.g. `deploy/orphans.ts`,
3
- * this file's own tests) do not need to know the list moved to `workers.ts`.
4
- * A function, not a top-level constant — see `workers.ts`'s header on why
5
- * eagerly reading `workers.json` at import time broke `--help`/`setup`
6
- * outside a repo. */
7
- export { workerApps };
8
- export declare class CloudflareError extends Error {
9
- }
10
- /**
11
- * Secret names per Worker for one target.
12
- *
13
- * A Worker that fails to read comes back as an empty set rather than a throw,
14
- * and the caller is told which ones could not be read. A Worker that has never
15
- * been deployed is the common case during setup, and it must not stop the audit
16
- * from reporting everything it CAN see.
17
- */
18
- export declare function listWorkerSecrets(target: string): Promise<{
19
- secrets: Map<string, Set<string>>;
20
- unreadable: string[];
21
- }>;
@@ -1,61 +0,0 @@
1
- /**
2
- * Reading the secret NAMES set on each Worker.
3
- *
4
- * Cloudflare never returns a secret's value, so this contributes presence only.
5
- * That is still worth having: `wrangler deploy --secrets-file` **preserves what
6
- * it omits**, so a renamed or dropped variable leaves its secret on the Worker
7
- * indefinitely, and nothing else in the system can see it.
8
- */
9
- import { execFile } from "node:child_process";
10
- import { promisify } from "node:util";
11
- import { join } from "node:path";
12
- import { findRepoRoot } from "../repo/root.js";
13
- import { workerApps } from "../workers.js";
14
- const run = promisify(execFile);
15
- /** Re-exported so existing importers of this module (e.g. `deploy/orphans.ts`,
16
- * this file's own tests) do not need to know the list moved to `workers.ts`.
17
- * A function, not a top-level constant — see `workers.ts`'s header on why
18
- * eagerly reading `workers.json` at import time broke `--help`/`setup`
19
- * outside a repo. */
20
- export { workerApps };
21
- export class CloudflareError extends Error {
22
- }
23
- /**
24
- * Secret names per Worker for one target.
25
- *
26
- * A Worker that fails to read comes back as an empty set rather than a throw,
27
- * and the caller is told which ones could not be read. A Worker that has never
28
- * been deployed is the common case during setup, and it must not stop the audit
29
- * from reporting everything it CAN see.
30
- */
31
- export async function listWorkerSecrets(target) {
32
- const secrets = new Map();
33
- const unreadable = [];
34
- for (const app of workerApps()) {
35
- const worker = `${target}-${app}`;
36
- try {
37
- const { stdout } = await run("pnpm", [
38
- "exec",
39
- "wrangler",
40
- "secret",
41
- "list",
42
- // ⚠️ WRANGLER's `--env`, not ours. Our flag is `--target`; this one
43
- // names a wrangler.jsonc environment block and must keep its spelling.
44
- "--env",
45
- target,
46
- "--format",
47
- "json",
48
- ], { cwd: join(findRepoRoot(), "apps", app), shell: false });
49
- // wrangler prints its banner on stdout too, so take the JSON array only.
50
- const start = stdout.indexOf("[");
51
- const parsed = start === -1
52
- ? []
53
- : JSON.parse(stdout.slice(start));
54
- secrets.set(worker, new Set(parsed.map((s) => s.name)));
55
- }
56
- catch {
57
- unreadable.push(worker);
58
- }
59
- }
60
- return { secrets, unreadable };
61
- }
@@ -1,99 +0,0 @@
1
- import type { EnvTarget } from "@devdogsuga/env";
2
- import { type VaultTarget } from "../bws/environments.js";
3
- import { EnvDocument } from "./document.js";
4
- export interface EnvOptions {
5
- /**
6
- * Typed as any `EnvTarget` rather than a `VaultTarget`, on purpose.
7
- * `development` has to REACH these commands so each can refuse it by name.
8
- * Narrowing here would move the refusal back into the argument parser, which
9
- * is where it was missing.
10
- */
11
- target: EnvTarget;
12
- /** Overrides the file the target implies. */
13
- file?: string;
14
- yes?: boolean;
15
- }
16
- /**
17
- * Writes the document, tidying first.
18
- *
19
- * `group()` runs on every write rather than as its own command: files drift a
20
- * line at a time, and a tidy pass nobody remembers to run is a tidy pass that
21
- * never happens.
22
- *
23
- * Env files contain credentials and must not be world-readable. The mode
24
- * parameter only applies on creation, so existing world-readable files are
25
- * tightened with chmod after each write.
26
- */
27
- export declare function save(path: string, doc: EnvDocument): Promise<boolean>;
28
- /**
29
- * Brings Bitwarden's values into the target's env file, in place.
30
- *
31
- * ⚠️ With `--file .env` (or `--target development`, which is refused) this is
32
- * the file `pnpm dev` reads. Pulling `production` into it points local
33
- * development at production, which is why that target warns and defaults its
34
- * confirmation to no.
35
- */
36
- export declare function runEnvPull(options: EnvOptions): Promise<void>;
37
- /**
38
- * Sends the target's env file to Bitwarden and then on to GitHub.
39
- *
40
- * Both, always. Bitwarden alone is the failure this design has: the source of
41
- * truth moves, the deploy does not, and everything looks healthy until the old
42
- * credential is revoked.
43
- *
44
- * ⚠️ The file comes from the TARGET. `--target staging` reads `.env.staging`,
45
- * not the root `.env`. That defaulting mistake meant this command uploaded a
46
- * developer's own values to a shared project and said "3 created, 12 updated"
47
- * about it.
48
- *
49
- * ⚠️ Two GitHub stores on the far side, and only one Bitwarden project. The
50
- * public per-environment values become GitHub *variables* rather than secrets
51
- * (see `gh/client.ts` for why the store matters), but they are stored in
52
- * Bitwarden alongside the secrets, because "Bitwarden is the source of truth"
53
- * has to be true of a WHOLE target for `pull` to rebuild a working env file.
54
- * Splitting them would make GitHub the only home for 27 keys and the second
55
- * source of truth for their values, which is the arrangement this tool exists
56
- * to remove.
57
- */
58
- export declare function runEnvPush(options: EnvOptions): Promise<void>;
59
- /**
60
- * The second half of every push.
61
- *
62
- * One Bitwarden project can feed more than one GitHub environment: `production`
63
- * feeds both `production` and `production-apply`, and which key goes where IS
64
- * the reviewer gate. So this loops over the routed targets rather than assuming
65
- * one, and confirms each separately. Agreeing to update production's ordinary
66
- * secrets is not agreeing to touch the two write-capable credentials sitting
67
- * behind the reviewers.
68
- *
69
- * The two maps stay two maps all the way down to the two `gh` calls. Merging
70
- * them and branching at the bottom would put "which store does this go to?" one
71
- * boolean away from being wrong, and getting it wrong in the variable direction
72
- * publishes a credential's plaintext to everyone who can read the repository's
73
- * Actions config.
74
- *
75
- * Exported ONLY so dispatch can be tested against a mocked `gh` client. The two
76
- * `setSecret`/`setVariable` lines below are the one place where a swap is
77
- * silent, irreversible, and invisible to `selection.ts`'s tests.
78
- */
79
- export declare function pushToGithub(target: VaultTarget, secrets: Map<string, string>, publicValues: Map<string, string>, yes?: boolean): Promise<void>;
80
- /** Read-only, and safe to run against anything. */
81
- export declare function runEnvAudit(options: EnvOptions): Promise<void>;
82
- /**
83
- * Empties every value in a local env file, losing none of them.
84
- *
85
- * The use is handing a filled-in file back to its blank state: after pulling
86
- * production onto a laptop, before passing a machine on, or when a set of keys
87
- * has to be re-entered from scratch. Deleting the values would do that too, and
88
- * would also delete the only copy of anything that was never pushed. So each
89
- * becomes a commented line holding what it was, plus an empty active line
90
- * beneath. The file still declares every key it needs, which makes it a
91
- * checklist rather than a blank page.
92
- *
93
- * Purely local. It touches no remote store, so it takes no `--target`: asking
94
- * which target to clear a file against would imply it reaches one. It works on
95
- * `.env` unless `--file` says otherwise, and that default is written out here
96
- * rather than inherited, so the shared `pathFor` has no "no target" case for a
97
- * remote command to fall into.
98
- */
99
- export declare function runEnvReset(options: Pick<EnvOptions, "file" | "yes">): Promise<void>;