@devdogsuga/backstage 0.1.11 → 0.1.13

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.
package/README.md CHANGED
@@ -153,8 +153,10 @@ a Send another officer made creates a new one, with a new link, under yours.
153
153
 
154
154
  The roster needs production's `DB_URL`: `--db-url`, else `.env.production` in a
155
155
  checkout, else Secrets Manager. The Linear key: `--linear-token`, then
156
- `LINEAR_API_KEY`, then the vault item "DevDogs Linear API key (backstage)", then
157
- a prompt; it is never saved.
156
+ `LINEAR_API_KEY`, then the item "DevDogs Linear API key (backstage)" in your
157
+ personal vault, then a prompt, which offers to save it there. Use your own key,
158
+ never one shared through the DevDogs organization: Linear records every edit
159
+ under the key's owner, and creds ignores organization items of that name.
158
160
 
159
161
  ## Member data: `import` and `export`
160
162
 
@@ -254,8 +256,10 @@ command that is not read-only asks once first (`--yes` answers it).
254
256
  Each command checks for the secrets it uses up front: `deploy <app>` for
255
257
  `CLOUDFLARE_API_TOKEN`, `deploy reconcile` for `CRON_SECRET`, `deploy
256
258
  prune-monitors` for `SENTRY_MONITORS_TOKEN` (skipped without it), `env` for the
257
- Secrets Manager token (flag, environment, then the Bitwarden vault, which `env`
258
- signs in to and unlocks itself, then asking).
259
+ Secrets Manager token (flag, environment, then your personal Bitwarden vault,
260
+ which `env` signs in to and unlocks itself, then asking). The token is each
261
+ person's own: the vault read skips organization items, and a save sets no
262
+ organization, so one person's token can be revoked without the rest.
259
263
 
260
264
  ## Layout
261
265
 
@@ -31,7 +31,7 @@ async function readProductionKey(key) {
31
31
  const value = nonEmpty(EnvDocument.parse(text).get(key));
32
32
  if (value) return value;
33
33
  } catch {}
34
- const { listSecrets, projectIdFor } = await import("./client-DCRfKvpP.js");
34
+ const { listSecrets, projectIdFor } = await import("./client-CJPfmtwW.js");
35
35
  const secrets = await listSecrets(await projectIdFor(PRODUCTION_PROJECT));
36
36
  return nonEmpty(secrets.find((s) => s.key === key)?.value);
37
37
  }
@@ -6,7 +6,7 @@ import { a as wouldRunLine, n as isDryRun, t as dryRunKind } from "./dry-run-OzO
6
6
  import { a as explainError, i as explain, n as bail, o as unwrap, r as errorMessage } from "./ui-RFZyr6k_.js";
7
7
  import { t as DONE } from "./dispatch-D048O65I.js";
8
8
  import { t as EnvDocument } from "./document-DumoZXoO.js";
9
- import { a as listSecrets$1, c as updateSecret, i as createSecret, o as projectIdFor, r as byKey, s as setExplicitAccessToken } from "./client-Djo8fRdS.js";
9
+ import { a as listSecrets$1, c as updateSecret, i as createSecret, o as projectIdFor, r as byKey, s as setExplicitAccessToken } from "./client-CFhbS3nu.js";
10
10
  import { register } from "node:module";
11
11
  import { pathToFileURL } from "node:url";
12
12
  import { appendFileSync, existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
@@ -3681,11 +3681,15 @@ function buildDesiredRulesets(actors) {
3681
3681
  conditions: main$1,
3682
3682
  bypass_actors: [
3683
3683
  {
3684
- actor_id: actors.reviewersTeamId,
3684
+ actor_id: actors.focusLeadsTeamId,
3685
+ actor_type: "Team",
3686
+ bypass_mode: "pull_request"
3687
+ },
3688
+ {
3689
+ actor_id: actors.devopsTeamId,
3685
3690
  actor_type: "Team",
3686
3691
  bypass_mode: "pull_request"
3687
3692
  },
3688
- always(actors.devopsTeamId),
3689
3693
  always(actors.adminsTeamId)
3690
3694
  ],
3691
3695
  rules: [{
@@ -3698,7 +3702,7 @@ function buildDesiredRulesets(actors) {
3698
3702
  target: "branch",
3699
3703
  enforcement: "active",
3700
3704
  conditions: main$1,
3701
- bypass_actors: [always(actors.devopsTeamId), always(actors.adminsTeamId)],
3705
+ bypass_actors: [always(actors.adminsTeamId)],
3702
3706
  rules: [{
3703
3707
  type: "pull_request",
3704
3708
  parameters: {
@@ -3716,7 +3720,7 @@ function buildDesiredRulesets(actors) {
3716
3720
  target: "branch",
3717
3721
  enforcement: "active",
3718
3722
  conditions: main$1,
3719
- bypass_actors: [always(actors.devopsTeamId), always(actors.adminsTeamId)],
3723
+ bypass_actors: [always(actors.adminsTeamId)],
3720
3724
  rules: [{
3721
3725
  type: "required_status_checks",
3722
3726
  parameters: {
@@ -3752,6 +3756,7 @@ function buildDesiredRulesets(actors) {
3752
3756
  exclude: ["refs/heads/main", "refs/heads/team/**"]
3753
3757
  } },
3754
3758
  bypass_actors: [
3759
+ always(actors.focusLeadsTeamId),
3755
3760
  always(actors.devopsTeamId),
3756
3761
  always(actors.adminsTeamId),
3757
3762
  ...actors.renovateAppId === void 0 ? [] : [{
@@ -3778,15 +3783,11 @@ function buildDesiredRulesets(actors) {
3778
3783
  include: ["refs/heads/team/**"],
3779
3784
  exclude: []
3780
3785
  } },
3781
- bypass_actors: [
3782
- {
3783
- actor_id: actors.appId,
3784
- actor_type: "Integration",
3785
- bypass_mode: "always"
3786
- },
3787
- always(actors.devopsTeamId),
3788
- always(actors.adminsTeamId)
3789
- ],
3786
+ bypass_actors: [{
3787
+ actor_id: actors.appId,
3788
+ actor_type: "Integration",
3789
+ bypass_mode: "always"
3790
+ }, always(actors.adminsTeamId)],
3790
3791
  rules: [
3791
3792
  { type: "creation" },
3792
3793
  { type: "deletion" },
@@ -4006,7 +4007,6 @@ const DEFAULT_REPO$1 = "DevDogsUGA";
4006
4007
  const DEFAULT_APP_SLUG = "devdogs-platform";
4007
4008
  const DEFAULT_DEVOPS_SLUG = "devops";
4008
4009
  const DEFAULT_ADMINS_SLUG = "admins";
4009
- const DEFAULT_REVIEWERS_SLUG = "reviewers";
4010
4010
  /** TASK-299 — installed separately from the platform App; may not exist yet. */
4011
4011
  const DEFAULT_RENOVATE_SLUG = "renovate";
4012
4012
  function parseOptions$1(argv) {
@@ -4085,14 +4085,14 @@ async function runGithubRulesets(argv) {
4085
4085
  repo: opts.repo
4086
4086
  };
4087
4087
  let devopsTeamId;
4088
+ let focusLeadsTeamId;
4088
4089
  let adminsTeamId;
4089
- let reviewersTeamId;
4090
4090
  let appId;
4091
4091
  try {
4092
- [devopsTeamId, adminsTeamId, reviewersTeamId, appId] = await Promise.all([
4092
+ [devopsTeamId, focusLeadsTeamId, adminsTeamId, appId] = await Promise.all([
4093
4093
  resolveTeamId(opts.org, DEFAULT_DEVOPS_SLUG),
4094
+ resolveTeamId(opts.org, "focus-leads"),
4094
4095
  resolveTeamId(opts.org, DEFAULT_ADMINS_SLUG),
4095
- resolveTeamId(opts.org, DEFAULT_REVIEWERS_SLUG),
4096
4096
  resolveAppId(opts.org, opts.appSlug)
4097
4097
  ]);
4098
4098
  } catch (err) {
@@ -4107,8 +4107,8 @@ async function runGithubRulesets(argv) {
4107
4107
  }
4108
4108
  const actors = {
4109
4109
  devopsTeamId,
4110
+ focusLeadsTeamId,
4110
4111
  adminsTeamId,
4111
- reviewersTeamId,
4112
4112
  appId,
4113
4113
  renovateAppId
4114
4114
  };
@@ -5389,9 +5389,9 @@ const HANDLERS = {
5389
5389
  graphics: async (rest) => (await import("./commands-BAh2_Nds.js")).handleGraphics(rest),
5390
5390
  qr: async (rest) => (await import("./commands-DILvdMc2.js")).handleQr(rest),
5391
5391
  newsletter: async (rest) => (await import("./commands-C3_hAcZS.js")).handleNewsletter(rest),
5392
- creds: async (rest) => (await import("./commands-C6w8jm6a.js")).handleCreds(rest),
5393
- import: async (rest) => (await import("./commands-DfWNSPra.js")).handleImport(rest),
5394
- export: async (rest) => (await import("./commands-3HvxDq-2.js")).handleExport(rest)
5392
+ creds: async (rest) => (await import("./commands-CTQjJ45_.js")).handleCreds(rest),
5393
+ import: async (rest) => (await import("./commands-DfQKdPJY.js")).handleImport(rest),
5394
+ export: async (rest) => (await import("./commands-BEYZyI4Y.js")).handleExport(rest)
5395
5395
  };
5396
5396
  /**
5397
5397
  * Names that were commands of the contributor CLI, refused with where they
@@ -2,7 +2,7 @@ import { w as findRepoRoot } from "./telemetry-RNK5Wu74.js";
2
2
  import { n as loadEnv } from "./peers-DE07w6nT.js";
3
3
  import { o as unwrap } from "./ui-RFZyr6k_.js";
4
4
  import { t as EnvDocument } from "./document-DumoZXoO.js";
5
- import { a as readTokenFromVault, o as saveTokenToVault, t as VAULT_ITEM_NAME } from "./vault-BpYSYcuT.js";
5
+ import { a as readTokenFromVault, s as saveTokenToVault, t as VAULT_ITEM_NAME } from "./vault-DVmaQC_E.js";
6
6
  import { dirname, join, resolve } from "node:path";
7
7
  import { log, password, select, text } from "@clack/prompts";
8
8
  import { mkdir, readFile, writeFile } from "node:fs/promises";
@@ -66,7 +66,7 @@ function looksLikeAccessToken(value) {
66
66
  async function promptForToken() {
67
67
  if (!process.stdin.isTTY) return void 0;
68
68
  const typed = unwrap(await password({
69
- message: "Paste the Secrets Manager access token for the `admin` account",
69
+ message: "Paste your own Secrets Manager access token (`admin` machine account)",
70
70
  mask: "•",
71
71
  validate: (v) => (v ?? "").trim() === "" ? "An access token is required." : void 0
72
72
  })).trim();
@@ -0,0 +1,2 @@
1
+ import { a as listSecrets, o as projectIdFor } from "./client-CFhbS3nu.js";
2
+ export { listSecrets, projectIdFor };
@@ -2,8 +2,8 @@ import { n as isDryRun } from "./dry-run-OzO0jUhK.js";
2
2
  import { o as unwrap, r as errorMessage, t as UsageError } from "./ui-RFZyr6k_.js";
3
3
  import { i as exportParams, n as EXPORT_KINDS, r as auditFilters, t as EXPORTS } from "./queries-D3mTchag.js";
4
4
  import { t as DONE } from "./dispatch-D048O65I.js";
5
- import { c as ProductionError, d as requireProductionKey, i as plural, l as connect, o as reportFailure, r as expandHome, s as say, t as canPrompt, u as dbError } from "./cli-B7W_-WdD.js";
6
- import { i as parseBound, n as findMeetingsWith, r as pickMeeting, t as describeMeeting } from "./meetings-BAX4Sils.js";
5
+ import { c as ProductionError, d as requireProductionKey, i as plural, l as connect, o as reportFailure, r as expandHome, s as say, t as canPrompt, u as dbError } from "./cli-CqbqBpaT.js";
6
+ import { i as parseBound, n as findMeetingsWith, r as pickMeeting, t as describeMeeting } from "./meetings-QgVEoBIV.js";
7
7
  import { existsSync, readdirSync, statSync } from "node:fs";
8
8
  import { basename, dirname, join } from "node:path";
9
9
  import { autocomplete, confirm, multiselect, text } from "@clack/prompts";
@@ -3,7 +3,7 @@ import { t as nonEmpty } from "./connection-D3XsOE3S.js";
3
3
  import { a as explainError, i as explain, o as unwrap, r as errorMessage, t as UsageError } from "./ui-RFZyr6k_.js";
4
4
  import { t as DONE } from "./dispatch-D048O65I.js";
5
5
  import { t as EnvDocument } from "./document-DumoZXoO.js";
6
- import { i as readPasswordFromVault, n as bwArgs, r as openVault, s as bwCommand } from "./vault-BpYSYcuT.js";
6
+ import { c as bwCommand, i as readPasswordFromVault, n as bwArgs, o as savePasswordToVault, r as openVault } from "./vault-DVmaQC_E.js";
7
7
  import { resolve } from "node:path";
8
8
  import { confirm, log, multiselect, note, password, select, text } from "@clack/prompts";
9
9
  import { readFile } from "node:fs/promises";
@@ -632,7 +632,7 @@ async function readCheckoutDbUrl() {
632
632
  }
633
633
  }
634
634
  async function readSecretsManagerDbUrl() {
635
- const { listSecrets, projectIdFor } = await import("./client-DCRfKvpP.js");
635
+ const { listSecrets, projectIdFor } = await import("./client-CJPfmtwW.js");
636
636
  const secrets = await listSecrets(await projectIdFor(PRODUCTION_PROJECT));
637
637
  return nonEmpty(secrets.find((s) => s.key === "DB_URL")?.value);
638
638
  }
@@ -660,6 +660,12 @@ function rosterQueryFor(url) {
660
660
  }
661
661
  };
662
662
  }
663
+ /**
664
+ * In the officer's PERSONAL vault, never the DevDogs organization: a Linear
665
+ * key acts as the person who made it, so a shared one would put every edit to
666
+ * the document under one name. It would also be an organization login that
667
+ * `creds send` could hand out.
668
+ */
663
669
  const LINEAR_VAULT_ITEM = "DevDogs Linear API key (backstage)";
664
670
  const LINEAR_API = "https://api.linear.app/graphql";
665
671
  function cell(text) {
@@ -695,21 +701,36 @@ function renderReport(items, now, roster) {
695
701
  return `${lines.join("\n")}\n`;
696
702
  }
697
703
  /**
698
- * `--linear-token`, then `LINEAR_API_KEY`, then the vault item, then a prompt.
699
- * Never saved anywhere: the vault is where it lives.
704
+ * `--linear-token`, then `LINEAR_API_KEY`, then your personal vault item, then
705
+ * a prompt, which offers to save what was typed to your personal vault.
700
706
  */
701
707
  async function resolveLinearToken(sources) {
702
- const token = nonEmpty(sources.explicit) ?? nonEmpty(sources.env) ?? await (sources.fromVault ?? readLinearTokenFromVault)() ?? await (sources.prompt ?? promptForLinearToken)();
703
- if (!token) throw new CredsError(`No Linear API key. Set LINEAR_API_KEY, or store one in your vault as "${LINEAR_VAULT_ITEM}".`);
704
- registerSecret(token);
705
- return token;
708
+ const token = nonEmpty(sources.explicit) ?? nonEmpty(sources.env) ?? await (sources.fromVault ?? readLinearTokenFromVault)();
709
+ if (token) {
710
+ registerSecret(token);
711
+ return token;
712
+ }
713
+ const typed = await (sources.prompt ?? promptForLinearToken)();
714
+ if (!typed) throw new CredsError(`No Linear API key. Set LINEAR_API_KEY, or store your own in your personal vault as "${LINEAR_VAULT_ITEM}".`);
715
+ registerSecret(typed);
716
+ await (sources.offerSave ?? offerToSaveLinearToken)(typed);
717
+ return typed;
706
718
  }
707
719
  function readLinearTokenFromVault() {
708
720
  return readPasswordFromVault(LINEAR_VAULT_ITEM, "the Linear API key", "read the Linear API key", "LINEAR_API_KEY");
709
721
  }
722
+ async function offerToSaveLinearToken(token) {
723
+ if (!process.stdin.isTTY) return;
724
+ if (!unwrap(await confirm({
725
+ message: `Save it to your personal Bitwarden vault as "DevDogs Linear API key (backstage)"?`,
726
+ initialValue: true
727
+ }))) return;
728
+ if (await savePasswordToVault("DevDogs Linear API key (backstage)", "Your own Linear API key, read by `backstage creds` to update the Shared Accounts document. Keep it in your personal vault, never the DevDogs organization: Linear records every edit under the key's owner.", token, "save the Linear API key")) log.success(`Stored as "${LINEAR_VAULT_ITEM}" in your vault.`);
729
+ else log.warn("Could not save it; continuing with the key you typed.");
730
+ }
710
731
  async function promptForLinearToken() {
711
732
  if (!process.stdin.isTTY) return void 0;
712
- const value = unwrap(await password({ message: "Linear API key? (Settings → Security & access → API keys)" }));
733
+ const value = unwrap(await password({ message: "Your Linear API key? (Settings → Security & access → API keys)" }));
713
734
  return nonEmpty(value.trim());
714
735
  }
715
736
  /** Replaces the document's content. Returns its URL. */
@@ -2,8 +2,8 @@ import { i as explain } from "./ui-RFZyr6k_.js";
2
2
  //#region src/import/commands.ts
3
3
  const handleImport = async (rest) => {
4
4
  const [what, ...args] = rest;
5
- if (what === "involvement") return (await import("./commands-B25Vafxz.js")).handleInvolvement(args);
6
- if (what === "attendance") return (await import("./commands-D-Vvufb0.js")).handleAttendance(args);
5
+ if (what === "involvement") return (await import("./commands-DsyAE7BO.js")).handleInvolvement(args);
6
+ if (what === "attendance") return (await import("./commands-H4DGzVqb.js")).handleAttendance(args);
7
7
  explain(what ? `Unknown import "${what}". Try involvement or attendance.` : "Name the import: involvement or attendance.", "");
8
8
  process.exitCode = 1;
9
9
  return null;
@@ -1,8 +1,8 @@
1
1
  import { n as isDryRun } from "./dry-run-OzO0jUhK.js";
2
2
  import { r as errorMessage, t as UsageError } from "./ui-RFZyr6k_.js";
3
3
  import { t as DONE } from "./dispatch-D048O65I.js";
4
- import { a as readInputFile, d as requireProductionKey, f as withConnection, i as plural, n as confirmWrite, o as reportFailure, s as say, t as canPrompt } from "./cli-B7W_-WdD.js";
5
- import { a as createAccounts, i as createAccountFor, n as accountFinder, o as readAccounts, t as parseCsv } from "./read-Dd388anq.js";
4
+ import { a as readInputFile, d as requireProductionKey, f as withConnection, i as plural, n as confirmWrite, o as reportFailure, s as say, t as canPrompt } from "./cli-CqbqBpaT.js";
5
+ import { a as createAccounts, i as createAccountFor, n as accountFinder, o as readAccounts, t as parseCsv } from "./read-DMderLHG.js";
6
6
  import { parseArgs } from "node:util";
7
7
  //#region src/involvement/csv.ts
8
8
  /**
@@ -1,9 +1,9 @@
1
1
  import { n as isDryRun } from "./dry-run-OzO0jUhK.js";
2
2
  import { o as unwrap, r as errorMessage, t as UsageError } from "./ui-RFZyr6k_.js";
3
3
  import { t as DONE } from "./dispatch-D048O65I.js";
4
- import { a as readInputFile, c as ProductionError, d as requireProductionKey, f as withConnection, i as plural, l as connect, n as confirmWrite, o as reportFailure, s as say, t as canPrompt } from "./cli-B7W_-WdD.js";
5
- import { a as createAccounts, i as createAccountFor, n as accountFinder, r as accountsWith, t as parseCsv } from "./read-Dd388anq.js";
6
- import { n as findMeetingsWith, r as pickMeeting, t as describeMeeting } from "./meetings-BAX4Sils.js";
4
+ import { a as readInputFile, c as ProductionError, d as requireProductionKey, f as withConnection, i as plural, l as connect, n as confirmWrite, o as reportFailure, s as say, t as canPrompt } from "./cli-CqbqBpaT.js";
5
+ import { a as createAccounts, i as createAccountFor, n as accountFinder, r as accountsWith, t as parseCsv } from "./read-DMderLHG.js";
6
+ import { n as findMeetingsWith, r as pickMeeting, t as describeMeeting } from "./meetings-QgVEoBIV.js";
7
7
  import { text } from "@clack/prompts";
8
8
  import { parseArgs } from "node:util";
9
9
  //#region src/attendance/plan.ts
package/dist/launch.js CHANGED
@@ -322,7 +322,7 @@ async function launchWith(argv, options) {
322
322
  */
323
323
  async function launch(argv) {
324
324
  await launchWith(argv, { dispatch: async (args) => {
325
- const { main } = await import("./cli-B8Lshscc.js");
325
+ const { main } = await import("./cli-DIXhcRjn.js");
326
326
  await main(args);
327
327
  } });
328
328
  }
@@ -1,5 +1,5 @@
1
1
  import { t as UsageError } from "./ui-RFZyr6k_.js";
2
- import "./cli-B7W_-WdD.js";
2
+ import "./cli-CqbqBpaT.js";
3
3
  //#region src/production/time.ts
4
4
  /**
5
5
  * Dates as officers type them. Meetings happen in Athens, so a bare
@@ -1,5 +1,5 @@
1
1
  import { r as errorMessage } from "./ui-RFZyr6k_.js";
2
- import { c as ProductionError, f as withConnection } from "./cli-B7W_-WdD.js";
2
+ import { c as ProductionError, f as withConnection } from "./cli-CqbqBpaT.js";
3
3
  //#region src/production/accounts.ts
4
4
  /**
5
5
  * Production accounts: reading them, finding one by email, and creating the
@@ -50,9 +50,14 @@ function bwCommand(args) {
50
50
  * prerequisite.
51
51
  *
52
52
  * ⚠️ The token never passes through argv in either direction. Reads use
53
- * `bw get password <search>`, which puts only the item NAME on the command
53
+ * `bw list items --search <name>`, which puts only the item NAME on the command
54
54
  * line, and writes pipe base64 JSON through **stdin**, which `bw create item`
55
55
  * documents as an accepted input.
56
+ *
57
+ * ⚠️ Only the PERSONAL vault. A token read from, or saved to, an organization
58
+ * item would be one token for everybody who can see that item: every call made
59
+ * with it looks the same, and revoking one person means revoking everyone.
60
+ * Reads skip organization items and say so; writes set no organization.
56
61
  */
57
62
  const run = promisify(execFile);
58
63
  /**
@@ -177,20 +182,20 @@ function bwArgs(args, key) {
177
182
  key
178
183
  ] : withNoInteraction;
179
184
  }
180
- /**
181
- * The stored token, or `undefined` for every reason it might not be there.
182
- *
183
- * `bw get password` takes a search term and fails when it matches more than one
184
- * item, which is the behaviour worth having: two items called something like
185
- * this means somebody should look, not that this should guess.
186
- */
185
+ /** The stored token, or `undefined` for every reason it might not be there. */
187
186
  async function readTokenFromVault() {
188
187
  return readPasswordFromVault(VAULT_ITEM_NAME, "the access token", "look for the access token", "BWS_ACCESS_TOKEN");
189
188
  }
190
189
  /**
191
- * The password of the one vault item named `itemName`, or `undefined` for
192
- * every reason it might not be there. `what` names it in the spinner; `purpose`
193
- * finishes the sign-in and unlock questions.
190
+ * The password of the one PERSONAL vault item named exactly `itemName`, or
191
+ * `undefined` for every reason it might not be there. `what` names it in the
192
+ * spinner; `purpose` finishes the sign-in and unlock questions.
193
+ *
194
+ * `bw list items --search` rather than `bw get password`: `get` takes the
195
+ * first item it can see, organization items included, and an item of that
196
+ * name in an organization is precisely the shared token this must not use.
197
+ * Two personal items of the same name mean somebody should look, not that
198
+ * this should guess, so that is "not found" too.
194
199
  */
195
200
  async function readPasswordFromVault(itemName, what, purpose, envVar) {
196
201
  const status = await signInIfNeeded(await vaultStatus(), purpose);
@@ -202,34 +207,49 @@ async function readPasswordFromVault(itemName, what, purpose, envVar) {
202
207
  if (status === "locked" && !key) return void 0;
203
208
  const s = spinner();
204
209
  s.start("Looking in your Bitwarden vault");
210
+ let items;
205
211
  try {
206
212
  const { stdout } = await run(...bwCommand(bwArgs([
207
- "get",
208
- "password",
209
- itemName,
210
- "--raw"
211
- ], key)), { shell: false });
212
- const token = stdout.trim();
213
- if (token === "") {
214
- s.stop("Nothing stored in the vault yet");
215
- return;
216
- }
213
+ "list",
214
+ "items",
215
+ "--search",
216
+ itemName
217
+ ], key)), {
218
+ shell: false,
219
+ maxBuffer: 16777216
220
+ });
221
+ items = JSON.parse(stdout).filter((item) => item.name === itemName);
222
+ } catch {
223
+ s.stop("Could not read your Bitwarden vault");
224
+ return;
225
+ }
226
+ const personal = items.filter((item) => !item.organizationId);
227
+ if (personal.length > 1) {
228
+ s.stop(`More than one "${itemName}" in your vault; delete the extras`);
229
+ return;
230
+ }
231
+ const token = personal[0]?.login?.password?.trim();
232
+ if (token) {
217
233
  s.stop(`Read ${what} from your vault ("${itemName}")`);
218
234
  return token;
219
- } catch {
220
- s.stop("No single matching item in your vault");
235
+ }
236
+ if (items.length > personal.length) {
237
+ s.stop(`"${itemName}" is in an organization, not your personal vault`);
238
+ log.warn(`Not using it: an organization item is one ${what} for everyone who can see it, so nothing it does can be told apart. Use your own, saved in your personal vault (no organization).`);
221
239
  return;
222
240
  }
241
+ s.stop("Nothing stored in the vault yet");
223
242
  }
224
243
  /**
225
- * Creates the item, with the token arriving on **stdin** as base64 JSON.
244
+ * Saves `token` as a login item named `itemName` in the PERSONAL vault, the
245
+ * token arriving on **stdin** as base64 JSON.
226
246
  *
227
247
  * `bw create item` documents an encoded-JSON positional AND stdin. Using stdin
228
248
  * is the difference between a live credential that is invisible and one that
229
- * sits in `ps` output for the length of the call.
249
+ * sits in `ps` output for the length of the call. `organizationId: null` is
250
+ * what makes it personal, and it is not a parameter.
230
251
  */
231
- async function saveTokenToVault(token) {
232
- const purpose = "save the access token";
252
+ async function savePasswordToVault(itemName, notes, token, purpose) {
233
253
  const status = await signInIfNeeded(await vaultStatus(), purpose);
234
254
  if (status === "unavailable" || status === "unauthenticated") return false;
235
255
  const key = await session(status, purpose);
@@ -239,8 +259,8 @@ async function saveTokenToVault(token) {
239
259
  collectionIds: null,
240
260
  folderId: null,
241
261
  type: 1,
242
- name: VAULT_ITEM_NAME,
243
- notes: "Bitwarden Secrets Manager access token for the `admin` machine account, read/write on preflight, staging and production.\n\nRead automatically by `backstage env`. Never put this in a .env file: it unlocks all three projects, and the tool refuses to upload it.",
262
+ name: itemName,
263
+ notes,
244
264
  favorite: false,
245
265
  reprompt: 0,
246
266
  login: {
@@ -268,10 +288,14 @@ async function saveTokenToVault(token) {
268
288
  child.stdin.end(Buffer.from(JSON.stringify(item)).toString("base64"));
269
289
  });
270
290
  }
291
+ /** Saves the Secrets Manager access token. See {@link savePasswordToVault}. */
292
+ function saveTokenToVault(token) {
293
+ return savePasswordToVault(VAULT_ITEM_NAME, "Your own Bitwarden Secrets Manager access token for the `admin` machine account, read/write on preflight, staging and production.\n\nRead automatically by `backstage env`. Keep it in your personal vault, never an organization: one token per person is what keeps calls attributable and lets one person's be revoked alone. Never put this in a .env that leaves this machine: it unlocks all three projects, and the tool refuses to upload it.", token, "save the access token");
294
+ }
271
295
  /** Why the vault could not be used, phrased as something to do about it. */
272
296
  function explainVault(status, envVar = "BWS_ACCESS_TOKEN") {
273
297
  if (status === "unavailable") return "The `bw` CLI is not installed, so the vault could not be checked. It ships with backstage, so reinstall it.";
274
298
  if (status === "unauthenticated") return `You are not signed in to Bitwarden, and with no terminal there is nobody to sign in. Set ${envVar} instead.`;
275
299
  }
276
300
  //#endregion
277
- export { readTokenFromVault as a, readPasswordFromVault as i, bwArgs as n, saveTokenToVault as o, openVault as r, bwCommand as s, VAULT_ITEM_NAME as t };
301
+ export { readTokenFromVault as a, bwCommand as c, readPasswordFromVault as i, bwArgs as n, savePasswordToVault as o, openVault as r, saveTokenToVault as s, VAULT_ITEM_NAME as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devdogsuga/backstage",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Officer and production CLI for DevDogsUGA: deploys, env sync with Bitwarden and GitHub, the migration planner role, club graphics and QR codes, GitHub rulesets and settings, and the newsletter. Ships built JS; run it anywhere with `pnpm dlx @devdogsuga/backstage` (no checkout needed to start; commands that read a checkout say so).",
@@ -36,14 +36,14 @@
36
36
  "postgres": "^3.4.9",
37
37
  "react": "^19.2.8",
38
38
  "satori": "^0.33.4",
39
- "sharp": "0.35.3",
39
+ "sharp": "0.35.5",
40
40
  "tsx": "^4.23.12",
41
41
  "typescript": "^6.0.3",
42
42
  "zod": "4.4.3",
43
- "@devdogsuga/brand": "0.1.6",
43
+ "@devdogsuga/brand": "0.1.8",
44
44
  "@devdogsuga/events": "0.1.7",
45
- "@devdogsuga/newsletter": "0.1.13",
46
- "@devdogsuga/telemetry": "0.1.4"
45
+ "@devdogsuga/telemetry": "0.1.4",
46
+ "@devdogsuga/newsletter": "0.1.15"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "@devdogsuga/db": "*",
@@ -64,10 +64,10 @@
64
64
  "eslint": "^9.39.5",
65
65
  "tsdown": "^0.23.0",
66
66
  "vitest": "^4.1.11",
67
- "@devdogsuga/cli-core": "0.0.0",
67
+ "@devdogsuga/config": "0.1.2",
68
68
  "@devdogsuga/db": "0.1.3",
69
- "@devdogsuga/env": "0.1.5",
70
- "@devdogsuga/config": "0.1.2"
69
+ "@devdogsuga/cli-core": "0.0.0",
70
+ "@devdogsuga/env": "0.1.5"
71
71
  },
72
72
  "scripts": {
73
73
  "build": "tsdown && node scripts/write-build-info.mjs && node ../../scripts/check-bundle-imports.mjs",
@@ -1,2 +0,0 @@
1
- import { a as listSecrets, o as projectIdFor } from "./client-Djo8fRdS.js";
2
- export { listSecrets, projectIdFor };