@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
@@ -1,363 +0,0 @@
1
- /**
2
- * Secrets Manager, through the official SDK. No `bws` binary to install.
3
- *
4
- * This wrapped the `bws` CLI until 2026-08-19. The CLI was the right call
5
- * against the raw REST API: Secrets Manager is end-to-end encrypted, the
6
- * server stores ciphertext, and the client derives the key that opens it from
7
- * the access token, so `fetch` returns blobs and reimplementing the crypto is
8
- * not a trade worth making. `@bitwarden/sdk-napi` is that same client-side
9
- * crypto (the same Rust core the CLI wraps), loaded in-process, which buys two
10
- * things the CLI could not:
11
- *
12
- * * **Nothing to install.** The SDK is a dependency of this package, so
13
- * `pnpm install` is the whole setup. No "install bws from the releases
14
- * page" step, and no version somebody's laptop drifted on.
15
- * * **No credential in argv.** `bws secret create` took the VALUE as a
16
- * positional argument, visible to `ps` for the length of the call, a
17
- * documented property of the tool this wrapper could only apologize for.
18
- * In-process values never touch a process table.
19
- *
20
- * ⚠️ Imported LAZILY, at the first real call. The SDK is a native module, and
21
- * loading it at import time would tax every `cli:no-env` path, the CI guards
22
- * included, with a `.node` binary none of them use.
23
- *
24
- * The one thing the SDK needs that the CLI did not: the ORGANIZATION ID. The
25
- * CLI derived it from the access token's login response; the SDK's every list
26
- * and create takes it as an argument, and nothing in its API discovers it. It
27
- * is a public identifier (a UUID that confers nothing), read from `BWS_ORG_ID`.
28
- * See the declaration in `packages/devtools/env.ts`.
29
- */
30
- import { mkdir } from "node:fs/promises";
31
- import { homedir } from "node:os";
32
- import { dirname, join } from "node:path";
33
- import { log, select, text } from "@clack/prompts";
34
- import { NoAccessTokenError, promptForToken, resolveToken } from "./token.js";
35
- import { withRateLimitRetry } from "./retry.js";
36
- import { makePacer } from "./pace.js";
37
- import { saveToDevEnv } from "./store.js";
38
- import { readTokenFromVault, saveTokenToVault, VAULT_ITEM_NAME, } from "./vault.js";
39
- import { unwrap } from "../ui.js";
40
- export class BwsError extends Error {
41
- }
42
- /**
43
- * One pacer for the process: every Secrets Manager request funnels through
44
- * it, spaced under the published per-IP limits so the 429 retry stays a
45
- * backstop instead of the plan. See `pace.ts` for the numbers and sources.
46
- */
47
- const pace = makePacer();
48
- let explicitToken;
49
- let resolved;
50
- /** Where a just-typed token goes, picked in `offerSave`, used in `save`. */
51
- let saveDestination = "no";
52
- /** Records `--access-token`, before any command runs. */
53
- export function setExplicitAccessToken(token) {
54
- explicitToken = token;
55
- resolved = undefined;
56
- }
57
- /**
58
- * The access token, found once per process.
59
- *
60
- * Memoized as a **promise**, not a value: resolution can prompt, and a single
61
- * command makes several Secrets Manager calls. Caching the value alone would
62
- * still let two concurrent calls open two prompts over each other.
63
- *
64
- * Whatever the source, the token reaches the SDK as a function argument in
65
- * this process, never argv, never a child's environment.
66
- */
67
- export async function accessToken() {
68
- resolved ??= resolveToken({
69
- explicit: explicitToken,
70
- env: process.env.BWS_ACCESS_TOKEN,
71
- fromVault: readTokenFromVault,
72
- prompt: promptForToken,
73
- // The destination is picked when the save is offered, and the pick is
74
- // carried into `save` by closure: the two callbacks are halves of one
75
- // exchange. `.env` first: it is this repository's own file, `with-env`
76
- // already loads it, and `env push` refuses the key by name, so the only
77
- // machine that can read the saved copy is this one.
78
- offerSave: async () => {
79
- saveDestination = unwrap(await select({
80
- message: "Save it, so this is the last time you paste it?",
81
- options: [
82
- {
83
- value: "env",
84
- label: "yes — into .env",
85
- hint: "gitignored, this machine only; push refuses it by name",
86
- },
87
- {
88
- value: "vault",
89
- label: "yes — into my Bitwarden vault",
90
- hint: "needs `pnpm devtools bw login`; follows you across machines",
91
- },
92
- { value: "no", label: "no — ask me again next time" },
93
- ],
94
- initialValue: "env",
95
- }));
96
- return saveDestination !== "no";
97
- },
98
- save: async (token) => {
99
- if (saveDestination === "env") {
100
- await saveToDevEnv("BWS_ACCESS_TOKEN", token);
101
- log.success("Stored in .env — with-env loads it on every run.");
102
- return;
103
- }
104
- const saved = await saveTokenToVault(token);
105
- if (saved) {
106
- log.success(`Stored as "${VAULT_ITEM_NAME}" in your Bitwarden vault.`);
107
- }
108
- else {
109
- log.warn("Could not save it. The command will continue with the token you " +
110
- "typed; nothing was written to your vault.");
111
- }
112
- },
113
- onSource: (source) => {
114
- if (source === "flag") {
115
- log.warn("--access-token puts a live credential in argv, where `ps` can read " +
116
- "it for the length of the call, and in your shell history. Prefer " +
117
- "BWS_ACCESS_TOKEN or the vault.");
118
- }
119
- },
120
- }).catch((err) => {
121
- resolved = undefined; // so a later call can ask again
122
- throw err instanceof NoAccessTokenError ? new BwsError(err.message) : err;
123
- });
124
- return resolved;
125
- }
126
- let resolvedOrgId;
127
- /**
128
- * The organization the machine account belongs to: the environment, else a
129
- * prompt with an offer to save.
130
- *
131
- * Prompted rather than only refused because it is the one Secrets Manager
132
- * input with nothing secret about it: a public UUID that identifies without
133
- * authorizing. Where nobody can answer (a pipe), the named refusal below is
134
- * the answer, because an SDK error about a malformed UUID would send somebody
135
- * debugging the token, which is the one thing that is fine.
136
- */
137
- function organizationId() {
138
- resolvedOrgId ??= (async () => {
139
- const fromEnv = process.env.BWS_ORG_ID;
140
- if (fromEnv)
141
- return fromEnv;
142
- if (!process.stdin.isTTY) {
143
- throw new BwsError("BWS_ORG_ID is not set. The Secrets Manager SDK addresses everything " +
144
- "by organization id — a public UUID, shown in the Secrets Manager " +
145
- "URL (bitwarden.com/#/sm/<org-id>/...) and on the machine-account " +
146
- "page. Put it in your .env; it identifies, it does not authorize.");
147
- }
148
- const typed = unwrap(await text({
149
- message: "What is the Bitwarden organization id? (the UUID in the Secrets " +
150
- "Manager URL: bitwarden.com/#/sm/<org-id>/...)",
151
- validate: (v) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test((v ?? "").trim())
152
- ? undefined
153
- : "That is not a UUID.",
154
- })).trim();
155
- // Saved without a destination question, unlike the token: this is a
156
- // public identifier with exactly one sensible home, and "ask me every
157
- // run" is not a state anyone wants for a value that never changes.
158
- try {
159
- await saveToDevEnv("BWS_ORG_ID", typed);
160
- log.success("Stored BWS_ORG_ID in .env.");
161
- }
162
- catch (err) {
163
- log.warn(`Could not save it to .env: ${err instanceof Error ? err.message : String(err)}. ` +
164
- "Continuing with the id you typed.");
165
- }
166
- return typed;
167
- })();
168
- resolvedOrgId.catch(() => {
169
- resolvedOrgId = undefined; // so a later call can ask again
170
- });
171
- return resolvedOrgId;
172
- }
173
- /** The lazily-loaded, logged-in SDK client, one per process. */
174
- let sdk;
175
- /**
176
- * Where the SDK caches its login, so a process is not a fresh authentication.
177
- *
178
- * ⚠️ This file is what stops the 429s. Bitwarden's identity endpoint
179
- * rate-limits repeated access-token logins hard, and "push preflight, audit,
180
- * push staging, audit, push production, audit" is six devtools processes in
181
- * two minutes: six logins without this, ONE with it. The `bws` binary kept a
182
- * state directory for exactly this reason, and the SDK migration dropped it.
183
- * Per machine account (the token's client-id UUID is in the filename, public
184
- * by format), under 0700 directories, and outside the repository, because it
185
- * caches an auth token and must live where nothing syncs or commits it.
186
- */
187
- function stateFileFor(accessToken) {
188
- const clientId = /^\d+\.([0-9a-f-]{36})\./i.exec(accessToken)?.[1] ?? "default";
189
- return join(homedir(), ".config", "devdogs-devtools", `bws-state-${clientId}.json`);
190
- }
191
- async function client() {
192
- sdk ??= (async () => {
193
- const token = await accessToken();
194
- // Dynamic, so the native module loads only when a command actually
195
- // talks to Secrets Manager. See the header.
196
- const { BitwardenClient } = await import("@bitwarden/sdk-napi");
197
- const instance = new BitwardenClient();
198
- try {
199
- const stateFile = stateFileFor(token);
200
- await mkdir(dirname(stateFile), { recursive: true, mode: 0o700 });
201
- await withRateLimitRetry(async () => {
202
- await pace("write");
203
- return instance.auth().loginAccessToken(token, stateFile);
204
- }, { doing: "the login" });
205
- }
206
- catch (err) {
207
- throw new BwsError(describeSdkFailure(err, "logging in"));
208
- }
209
- return instance;
210
- })();
211
- sdk.catch(() => {
212
- sdk = undefined; // so a later call can retry with a fresh token prompt
213
- });
214
- return sdk;
215
- }
216
- /**
217
- * Turns an SDK failure into something with a next step in it.
218
- *
219
- * The two that actually happen are a rejected token and a token that does not
220
- * cover the project. The latter surfaces as a bare "not found", because a
221
- * project you cannot see and a project that does not exist are deliberately
222
- * the same answer.
223
- */
224
- function describeSdkFailure(err, doing) {
225
- const message = err instanceof Error ? err.message : String(err);
226
- if (/\b429\b|too many requests/i.test(message)) {
227
- return (`${message}\n\n` +
228
- "Bitwarden rate-limited this even after the automatic retries. Wait a " +
229
- "minute and re-run — the login is cached in a state file now, so " +
230
- "re-running does not spend another authentication.");
231
- }
232
- if (/404|not.?found/i.test(message)) {
233
- return (`${message}\n\n` +
234
- "A 404 here usually means the access token is valid but its machine " +
235
- "account is not assigned to this project — Secrets Manager reports " +
236
- "'invisible' and 'absent' identically.");
237
- }
238
- if (/401|unauthoriz|invalid|expired/i.test(message)) {
239
- return `${message}\n\nThe access token was rejected while ${doing}. It may have been revoked or belong to another organization.`;
240
- }
241
- return message || `Secrets Manager failed with no message while ${doing}.`;
242
- }
243
- function iso(value) {
244
- if (value instanceof Date)
245
- return value.toISOString();
246
- if (typeof value === "string" && value !== "")
247
- return value;
248
- return undefined;
249
- }
250
- /** Resolves a project name to its id. Names are unique within an org. */
251
- export async function projectIdFor(name) {
252
- const c = await client();
253
- const org = await organizationId();
254
- let projects;
255
- try {
256
- projects = (await withRateLimitRetry(async () => {
257
- await pace("read");
258
- return c.projects().list(org);
259
- }, {
260
- doing: "listing projects",
261
- })).data;
262
- }
263
- catch (err) {
264
- throw new BwsError(describeSdkFailure(err, "listing projects"));
265
- }
266
- const match = projects.find((p) => p.name === name);
267
- if (!match) {
268
- const visible = projects.map((p) => p.name).sort();
269
- throw new BwsError(`No project named "${name}".\n` +
270
- (visible.length > 0
271
- ? `This token can see: ${visible.join(", ")}`
272
- : "This token can see no projects at all, which usually means the " +
273
- "machine account has no project assignments yet."));
274
- }
275
- return match.id;
276
- }
277
- export async function listSecrets(projectId) {
278
- const c = await client();
279
- const org = await organizationId();
280
- try {
281
- // The SDK lists IDENTIFIERS org-wide (only what the token may see), then
282
- // fetches the values in one batch. Filtered to the project HERE, so the
283
- // callers' contract, "the secrets of this project", survives the transport
284
- // change byte-for-byte.
285
- const identifiers = (await withRateLimitRetry(async () => {
286
- await pace("read");
287
- return c.secrets().list(org);
288
- }, {
289
- doing: "listing secrets",
290
- })).data;
291
- if (identifiers.length === 0)
292
- return [];
293
- const full = (await withRateLimitRetry(async () => {
294
- await pace("read");
295
- return c.secrets().getByIds(identifiers.map((i) => i.id));
296
- }, { doing: "reading secrets" })).data;
297
- return full
298
- .filter((s) => s.projectId === projectId)
299
- .map((s) => ({
300
- id: s.id,
301
- key: s.key,
302
- value: s.value,
303
- note: s.note,
304
- projectId,
305
- revisionDate: iso(s.revisionDate),
306
- }));
307
- }
308
- catch (err) {
309
- throw new BwsError(describeSdkFailure(err, "listing secrets"));
310
- }
311
- }
312
- export async function createSecret(projectId, key, value, note) {
313
- const c = await client();
314
- const org = await organizationId();
315
- try {
316
- await withRateLimitRetry(async () => {
317
- await pace("write");
318
- return c.secrets().create(org, key, value, note, [projectId]);
319
- }, { doing: `creating ${key}` });
320
- }
321
- catch (err) {
322
- throw new BwsError(describeSdkFailure(err, `creating ${key}`));
323
- }
324
- }
325
- /**
326
- * Takes the whole secret rather than an id: the SDK's update is a full
327
- * replace (organization, key, project list included), and every caller
328
- * already holds the listed secret it is updating.
329
- */
330
- export async function updateSecret(secret, value, note) {
331
- const c = await client();
332
- const org = await organizationId();
333
- try {
334
- await withRateLimitRetry(async () => {
335
- await pace("write");
336
- return c
337
- .secrets()
338
- .update(org, secret.id, secret.key, value, note, [secret.projectId]);
339
- }, { doing: `updating ${secret.key}` });
340
- }
341
- catch (err) {
342
- throw new BwsError(describeSdkFailure(err, `updating ${secret.key}`));
343
- }
344
- }
345
- export async function deleteSecrets(ids) {
346
- if (ids.length === 0)
347
- return;
348
- const c = await client();
349
- try {
350
- await withRateLimitRetry(async () => {
351
- await pace("write");
352
- return c.secrets().delete(ids);
353
- }, {
354
- doing: "deleting secrets",
355
- });
356
- }
357
- catch (err) {
358
- throw new BwsError(describeSdkFailure(err, "deleting secrets"));
359
- }
360
- }
361
- export function byKey(secrets) {
362
- return new Map(secrets.map((s) => [s.key, s]));
363
- }
@@ -1,96 +0,0 @@
1
- /**
2
- * The targets whose values live in Bitwarden, and what backs each.
3
- *
4
- * ⚠️ DERIVED, NOT DECLARED. The vault targets and each one's project name come
5
- * from the one target table in `@devdogsuga/env`
6
- * (Backstage's `packages/env/src/targets.ts`); only the prose summaries are added here.
7
- * This file used to declare its own `preflight | staging | production` enum
8
- * beside that package's `development | staging | production` one, both behind
9
- * a flag spelled `--env`, and the two agreeing on the words `staging` and
10
- * `production` is what let `push --env staging` read the development `.env`
11
- * while `init --env staging` wrote `.env.staging`. See the table's own comment
12
- * for the full account.
13
- *
14
- * **Bitwarden is the source of truth, and CI never reads it.** Values are
15
- * curated in a local env file and sent onward by `pnpm devtools env push`,
16
- * which writes Bitwarden AND the GitHub environment secrets in one go. Deploy
17
- * jobs then read `${{ secrets.* }}` like any other workflow.
18
- *
19
- * That decision sets the whole budget. Nothing machine-shaped authenticates to
20
- * Secrets Manager, so there are **no CI machine accounts**, just one `admin`
21
- * account, held by a person, read/write on all three projects:
22
- *
23
- * | Used | Free plan |
24
- * |------|-----------|
25
- * | 3 projects: preflight, staging, production | 3 |
26
- * | 1 machine account, `admin` | 3 |
27
- *
28
- * Two machine accounts spare, where the previous shape had none and could not
29
- * afford a project for `preflight`.
30
- *
31
- * There are three projects and FOUR targets: `development` has none. `.env` is
32
- * one contributor's own file, there is no shared development credential set to
33
- * sync it against, and `pull`/`push`/`audit` refuse that target by name rather
34
- * than falling through to a default project.
35
- *
36
- * The sync costs a second copy that cannot be read back: GitHub secrets are
37
- * write-only, so `env audit` compares names and `updatedAt` against each
38
- * secret's `revisionDate` here. That catches the realistic failure, a rotation
39
- * pushed to Bitwarden and never propagated, without ever comparing values.
40
- *
41
- * ⚠️ These projects hold the PUBLIC per-environment values too, not only the
42
- * secrets: `PROJECT_REF`, `BASE_URL`, `PUBLISHABLE_KEY` and the rest go on to
43
- * GitHub as *variables* rather than secrets. "Bitwarden is the source of truth"
44
- * has to be true of a whole target for `pull` to rebuild an env file an app can
45
- * boot from; while it was true of the secret half only, 27 values existed
46
- * nowhere but somebody's laptop. Variables ARE readable back, so for those keys
47
- * the audit compares values and the paragraph above does not apply.
48
- *
49
- * What it buys: no `bws` binary and no Bitwarden network call in the deploy
50
- * path, and `${{ secrets.* }}` is masked in workflow logs automatically, which
51
- * a value pulled at run time is not unless somebody remembers `::add-mask::`.
52
- */
53
- import type { EnvTarget, VaultTarget } from "@devdogsuga/env";
54
- export type { VaultTarget };
55
- export declare const isVaultTarget: (target: EnvTarget) => target is VaultTarget;
56
- export interface EnvironmentSpec {
57
- /** BWS project name. Resolved to a UUID at run time, never committed. */
58
- project: string;
59
- /** Extra confirmation before writing. */
60
- guarded: boolean;
61
- summary: string;
62
- }
63
- /**
64
- * Was a module-level constant, computed eagerly at import time. Now lazy:
65
- * `VAULT_TARGETS`/`projectFor`/`isGuarded` come from the repo-resolved
66
- * `@devdogsuga/env` (see `repo/peers.ts`), which is not available until
67
- * SOMETHING has awaited `loadEnv()` — every real caller already does, via
68
- * `env/discovery.ts`'s `loadRegistry()` or `pick.ts`'s own `loadEnv()` call.
69
- * Caching keeps repeat reads (env commands call this per target) cheap.
70
- */
71
- export declare function environmentSpecs(): Record<VaultTarget, EnvironmentSpec>;
72
- /**
73
- * `--target development` reached `pull`, `push` or `audit`.
74
- *
75
- * Its own error rather than a lookup that returns `undefined`, because the old
76
- * shape of this mistake was silence: `development` was not in the vault enum,
77
- * so nothing named it and nothing said why. It is a real target with a real
78
- * file; what it lacks is a shared credential set to sync against.
79
- */
80
- export declare class NoVaultProjectError extends Error {
81
- readonly target: EnvTarget;
82
- constructor(target: EnvTarget);
83
- }
84
- /** Said wherever the refusal is reported, so the advice cannot drift. */
85
- export declare function noVaultProjectHints(): string[];
86
- /**
87
- * Refuses the one target that has no project, and narrows the rest.
88
- *
89
- * Called at the top of every command that talks to a vault, so the refusal is a
90
- * property of the commands rather than of the argument parser alone: a new
91
- * subcommand that forgets to validate its flag still cannot fall through to a
92
- * default project. It narrows as well as throws, so everything after the call
93
- * is typed as a target that HAS a project rather than merely believed to be
94
- * one.
95
- */
96
- export declare function assertVaultTarget(target: EnvTarget): asserts target is VaultTarget;
@@ -1,111 +0,0 @@
1
- import { getEnvSync } from "../repo/peers.js";
2
- export const isVaultTarget = (target) => getEnvSync().isVaultTarget(target);
3
- const SUMMARIES = {
4
- // ⚠️ "Read-only by construction" is now enforced rather than described:
5
- // `preflight` is the one row with `deployEnv: false`, and `ignoredFor()`
6
- // reads that as "carries only the keys declared `narrowed`". Until it did,
7
- // `env push --target preflight` uploaded all 45 routable keys, including
8
- // SUPABASE_JWT_SIGNING_KEY, SECRET_KEY and GH_APP_PRIVATE_KEY, into a project
9
- // whose GitHub environment `main` can reach.
10
- //
11
- // `DB_URL` is the one key `narrowed: true` opts in today, so this project
12
- // carries nothing else.
13
- preflight: "Credentials for the dry runs that precede a promotion to production. " +
14
- "Read-only by construction: a Postgres role that can see only the " +
15
- "migrations table and nothing else.",
16
- staging: "Everything the two Next apps consume, pointed at staging.",
17
- production: "The live values. Shared with the production-apply environment, which " +
18
- "is the same project behind required reviewers.",
19
- };
20
- /**
21
- * Project *names*, not ids. See `TargetSpec.project` for why.
22
- *
23
- * Assembled from the target table rather than typed out again: a second copy of
24
- * "staging means staging" is a copy that can disagree, and the disagreement
25
- * would be silent in exactly the direction that matters.
26
- */
27
- let cachedSpecs;
28
- /**
29
- * Was a module-level constant, computed eagerly at import time. Now lazy:
30
- * `VAULT_TARGETS`/`projectFor`/`isGuarded` come from the repo-resolved
31
- * `@devdogsuga/env` (see `repo/peers.ts`), which is not available until
32
- * SOMETHING has awaited `loadEnv()` — every real caller already does, via
33
- * `env/discovery.ts`'s `loadRegistry()` or `pick.ts`'s own `loadEnv()` call.
34
- * Caching keeps repeat reads (env commands call this per target) cheap.
35
- */
36
- export function environmentSpecs() {
37
- if (cachedSpecs)
38
- return cachedSpecs;
39
- const env = getEnvSync();
40
- cachedSpecs = Object.fromEntries(env.VAULT_TARGETS.map((target) => [
41
- target,
42
- {
43
- // Non-null by construction: `VAULT_TARGETS` is the targets whose
44
- // `project` is not null.
45
- project: env.projectFor(target),
46
- guarded: env.isGuarded(target),
47
- summary: SUMMARIES[target],
48
- },
49
- ]));
50
- return cachedSpecs;
51
- }
52
- /**
53
- * `--target development` reached `pull`, `push` or `audit`.
54
- *
55
- * Its own error rather than a lookup that returns `undefined`, because the old
56
- * shape of this mistake was silence: `development` was not in the vault enum,
57
- * so nothing named it and nothing said why. It is a real target with a real
58
- * file; what it lacks is a shared credential set to sync against.
59
- */
60
- export class NoVaultProjectError extends Error {
61
- target;
62
- constructor(target) {
63
- super(`--target ${target} has no Bitwarden project, so there is nothing to ` +
64
- `pull from, push to, or audit against. ${getEnvSync().fileFor(target)} is your own ` +
65
- `file — nobody else's copy is meant to match it.`);
66
- this.target = target;
67
- this.name = "NoVaultProjectError";
68
- }
69
- }
70
- /** Said wherever the refusal is reported, so the advice cannot drift. */
71
- export function noVaultProjectHints() {
72
- return [
73
- `Pass --target ${getEnvSync().VAULT_TARGETS.join(" | ")} instead.`,
74
- "`pnpm devtools env reset` blanks the values in a local file, keeping each",
75
- "recoverable as a comment. `pnpm devtools setup` creates a fresh .env.",
76
- ];
77
- }
78
- /**
79
- * Refuses the one target that has no project, and narrows the rest.
80
- *
81
- * Called at the top of every command that talks to a vault, so the refusal is a
82
- * property of the commands rather than of the argument parser alone: a new
83
- * subcommand that forgets to validate its flag still cannot fall through to a
84
- * default project. It narrows as well as throws, so everything after the call
85
- * is typed as a target that HAS a project rather than merely believed to be
86
- * one.
87
- */
88
- export function assertVaultTarget(target) {
89
- if (!getEnvSync().isVaultTarget(target))
90
- throw new NoVaultProjectError(target);
91
- }
92
- // ── Where the key sets went ──────────────────────────────────────────────────
93
- //
94
- // The hand-maintained arrays that used to live here (NEVER_STORE_KEYS,
95
- // NEVER_SECRET_KEYS, APPLY_ONLY_KEYS) are now DERIVED from the declarations in
96
- // each app's env manifest: `neverStoreKeys()`, `neverSecretKeys()`,
97
- // `applyOnlyKeys()` and `storableKeys()` from `@devdogsuga/env`, populated by
98
- // devtools' `env/discovery.ts` (`loadRegistry()`). The arrays were correct;
99
- // what they could not do is STAY correct, because nothing connected them to the
100
- // schemas they described. A key added to an `env.ts` and not here was pushed to
101
- // Bitwarden as a secret by omission.
102
- //
103
- // The reasoning moved with the keys, onto the declarations themselves:
104
- //
105
- // * why `BWS_ACCESS_TOKEN` is refused storage anywhere (a key locked inside
106
- // the box it opens): its `define()` doc in `packages/devtools/env.ts`;
107
- // * why apply-tier is a GITHUB routing rule and not a Bitwarden one (the
108
- // production BWS project deliberately holds the apply credential): the
109
- // `EnvTier` doc in Backstage's `packages/env/src/meta.ts`;
110
- // * why the apply credential stays out of staging/preflight: `ignoredFor()`
111
- // in `../env/selection.ts`.
@@ -1,25 +0,0 @@
1
- /**
2
- * Spacing Secrets Manager calls so the rate limit never fires.
3
- *
4
- * Bitwarden's published server config (enforced on cloud, per IP, per
5
- * endpoint) allows 60 POSTs and 200 GETs per minute and answers 429 beyond.
6
- * Community reports show burst enforcement tighter than that per-minute
7
- * arithmetic. The identity endpoint's login limit is undocumented and tighter
8
- * still; the state-file login cache is what handles that one. Researched
9
- * 2026-08-20: https://bitwarden-server.mintlify.app/services/api
10
- *
11
- * So writes (create, update, delete, login) start no closer than 1.1s apart,
12
- * 60/min with margin, and reads (list, getByIds) no closer than 350ms. A full
13
- * first push (~48 creates) takes a predictable ~55 seconds instead of a sprint
14
- * into a 429; later pushes touch few keys and barely notice. The 429 retry is
15
- * the backstop for whatever the undocumented parts still throw.
16
- *
17
- * One shared gate rather than one per class, and the write interval applies to
18
- * the gap after a write whatever follows it: the limiter cares about the
19
- * request stream from this IP, not our taxonomy of it.
20
- */
21
- export type CallKind = "read" | "write";
22
- export declare const WRITE_GAP_MS = 1100;
23
- export declare const READ_GAP_MS = 350;
24
- export type Pacer = (kind: CallKind) => Promise<void>;
25
- export declare function makePacer(now?: () => number, sleep?: (ms: number) => Promise<void>): Pacer;
package/dist/bws/pace.js DELETED
@@ -1,43 +0,0 @@
1
- /**
2
- * Spacing Secrets Manager calls so the rate limit never fires.
3
- *
4
- * Bitwarden's published server config (enforced on cloud, per IP, per
5
- * endpoint) allows 60 POSTs and 200 GETs per minute and answers 429 beyond.
6
- * Community reports show burst enforcement tighter than that per-minute
7
- * arithmetic. The identity endpoint's login limit is undocumented and tighter
8
- * still; the state-file login cache is what handles that one. Researched
9
- * 2026-08-20: https://bitwarden-server.mintlify.app/services/api
10
- *
11
- * So writes (create, update, delete, login) start no closer than 1.1s apart,
12
- * 60/min with margin, and reads (list, getByIds) no closer than 350ms. A full
13
- * first push (~48 creates) takes a predictable ~55 seconds instead of a sprint
14
- * into a 429; later pushes touch few keys and barely notice. The 429 retry is
15
- * the backstop for whatever the undocumented parts still throw.
16
- *
17
- * One shared gate rather than one per class, and the write interval applies to
18
- * the gap after a write whatever follows it: the limiter cares about the
19
- * request stream from this IP, not our taxonomy of it.
20
- */
21
- export const WRITE_GAP_MS = 1_100;
22
- export const READ_GAP_MS = 350;
23
- const wait = (ms) => new Promise((r) => setTimeout(r, ms));
24
- export function makePacer(now = Date.now, sleep = wait) {
25
- let notBefore = 0;
26
- let chain = Promise.resolve();
27
- return (kind) => {
28
- const gap = kind === "write" ? WRITE_GAP_MS : READ_GAP_MS;
29
- const turn = chain.then(async () => {
30
- const pause = notBefore - now();
31
- if (pause > 0)
32
- await sleep(pause);
33
- // The NEXT call may not start until this one's gap has passed.
34
- // Reserved before the call runs, so concurrent callers queue instead
35
- // of all measuring from the same stale timestamp.
36
- notBefore = now() + gap;
37
- });
38
- // The chain never rejects: a pacer that fails closed would wedge every
39
- // later call behind a rejection that had nothing to do with them.
40
- chain = turn.catch(() => undefined);
41
- return turn;
42
- };
43
- }
@@ -1,12 +0,0 @@
1
- export declare function isRateLimited(err: unknown): boolean;
2
- export interface RetryOptions {
3
- /** Attempts AFTER the first. */
4
- retries?: number;
5
- /** First wait; doubles per retry (5s, 10s, 20s by default). */
6
- baseMs?: number;
7
- /** Injectable for tests. */
8
- sleep?: (ms: number) => Promise<void>;
9
- /** What to call the operation in the wait message. */
10
- doing: string;
11
- }
12
- export declare function withRateLimitRetry<T>(op: () => Promise<T>, { retries, baseMs, sleep, doing }: RetryOptions): Promise<T>;