package-registry-manager 0.22.7 → 0.22.9

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
@@ -22,4 +22,18 @@ version after confirmation; later versions publish through CI. Missing CI
22
22
  publishing jobs are offered in a draft pull request, and PyPI builds select an
23
23
  interpreter satisfying `requires-python`. See the repository README for the
24
24
  registry walkthroughs and safety details.
25
+ Account scans and setup are available in both ports:
26
+
27
+ ```sh
28
+ package-registry-manager inspect --org link-foundation
29
+ package-registry-manager plan --user LOGIN --format json
30
+ package-registry-manager setup --org link-foundation --all --execute --browser-import auto
31
+ ```
32
+
33
+ Scans read matched manifests/workflows through gh-manager. Account setup shares
34
+ one browser, groups npm approvals, and uses CI evidence to preserve healthy tokens
35
+ or verify replacements before revocation. `--secret-name '{REGISTRY}_TOKEN_{REPO}'`
36
+ selects custom credential names. Rust requires Node.js and npm for its pinned
37
+ GitHub CLI dependency.
38
+
25
39
  See the [bootstrap, wrapper and credential guide](../docs/registry-setup.md) for branch/PR setup, dual npm names, registry policies and integration limits.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "package-registry-manager",
3
- "version": "0.22.7",
3
+ "version": "0.22.9",
4
4
  "description": "Inspect repositories and guide secure package-registry setup",
5
5
  "type": "module",
6
6
  "bin": {
@@ -40,8 +40,9 @@
40
40
  "audit": "node scripts/npm-audit.mjs"
41
41
  },
42
42
  "dependencies": {
43
- "browser-commander": "^0.26.3",
44
- "command-stream": "^2.0.0",
43
+ "@link-foundation/gh-manager": "https://codeload.github.com/link-foundation/gh-manager/tar.gz/29808e088ac8fd8374361920bbebab07a4d63ac6",
44
+ "browser-commander": "^0.30.1",
45
+ "command-stream": "^2.0.2",
45
46
  "playwright": "^1.63.0",
46
47
  "selenium-webdriver": "^4.50.0"
47
48
  },
@@ -0,0 +1,121 @@
1
+ /** Resolve a caller-selected name before making any GitHub or browser mutations. */
2
+ export function secretName(template, registry, slug) {
3
+ const [owner, repo] = slug.split("/");
4
+ const normalize = (value) =>
5
+ value.toUpperCase().replaceAll(/[^A-Z0-9_]/g, "_");
6
+ const values = {
7
+ REGISTRY: normalize(registry),
8
+ REPO: normalize(repo),
9
+ ORG: normalize(owner),
10
+ OWNER: normalize(owner),
11
+ };
12
+ const name = template.replaceAll(
13
+ /\{([A-Z]+)\}/g,
14
+ (match, key) => values[key] ?? match,
15
+ );
16
+ if (!/^[A-Z_][A-Z0-9_]{0,244}$/.test(name) || name.startsWith("GITHUB_")) {
17
+ throw new Error("invalid registry secret name or naming template");
18
+ }
19
+ return name;
20
+ }
21
+
22
+ /** IDs are revocable only after every tracked consumer is being replaced. */
23
+ export function trackedTokenIds(state, repos, secret) {
24
+ const keys = new Set(repos.map((repo) => `${repo}:${secret}`));
25
+ const retained = new Set(
26
+ Object.entries(state)
27
+ .filter(([key]) => !keys.has(key))
28
+ .flatMap(([, ids]) => ids),
29
+ );
30
+ return [
31
+ ...new Set(repos.flatMap((repo) => state[`${repo}:${secret}`] ?? [])),
32
+ ].filter((id) => !retained.has(id));
33
+ }
34
+
35
+ /** Remember candidates for every consumer, including uncertain verification. */
36
+ export function recordTokenId(state, repos, secret, id) {
37
+ for (const repo of repos) {
38
+ const key = `${repo}:${secret}`;
39
+ state[key] = [...new Set([...(state[key] ?? []), id])];
40
+ }
41
+ }
42
+
43
+ /** Successful verification leaves the replacement associated with every target. */
44
+ export function completeRotation(state, repos, secret, id) {
45
+ for (const repo of repos) {
46
+ state[`${repo}:${secret}`] = [id];
47
+ }
48
+ }
49
+
50
+ /** CI drives rotation. Uncertain tests preserve every existing credential. */
51
+ export async function cycleCredential(adapter) {
52
+ const health = await adapter.health();
53
+ if (health.status === "ok") {
54
+ return { status: "ok", changed: false };
55
+ }
56
+ const previous = await adapter.metadata();
57
+ if (health.status === "unknown" && previous && previous.present !== false) {
58
+ const tested = await adapter.test();
59
+ if (tested.status === "ok") {
60
+ return { status: "ok", changed: false };
61
+ }
62
+ if (tested.status !== "auth-failing") {
63
+ throw new Error(
64
+ "credential verification is unknown; existing credential retained",
65
+ );
66
+ }
67
+ }
68
+ const candidates = [];
69
+ try {
70
+ for (let attempt = 0; attempt < 2; attempt += 1) {
71
+ const candidate = await adapter.create();
72
+ candidates.push(candidate);
73
+ if (
74
+ !candidate.value ||
75
+ !candidate.id ||
76
+ (candidate.expires_at &&
77
+ !(Date.parse(candidate.expires_at) > Date.now()))
78
+ ) {
79
+ throw new Error("registry returned no usable credential");
80
+ }
81
+ const stored = await adapter.ensure(candidate, {
82
+ status: "auth-failing",
83
+ });
84
+ candidate.value = "";
85
+ const tested = await adapter.test(stored);
86
+ if (tested.status === "ok") {
87
+ const ids = new Set([
88
+ previous?.token_id,
89
+ ...(previous?.token_ids ?? []),
90
+ ...candidates.slice(0, -1).map((item) => item.id),
91
+ ]);
92
+ ids.delete(undefined);
93
+ ids.delete(candidate.id);
94
+ for (const id of ids) {
95
+ await adapter.revoke(id);
96
+ if (!(await adapter.revoked(id))) {
97
+ throw new Error("previous token revocation could not be verified");
98
+ }
99
+ }
100
+ return {
101
+ ...stored,
102
+ status: "ok",
103
+ changed: true,
104
+ token_id: candidate.id,
105
+ };
106
+ }
107
+ if (tested.status !== "auth-failing") {
108
+ throw new Error(
109
+ "credential verification is unknown; old and replacement credentials retained",
110
+ );
111
+ }
112
+ }
113
+ throw new Error(
114
+ "credential verification is auth-failing after two candidates; old credentials retained",
115
+ );
116
+ } finally {
117
+ for (const candidate of candidates) {
118
+ candidate.value = "";
119
+ }
120
+ }
121
+ }
package/src/cli.mjs CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  executePlans,
22
22
  } from "./setup.mjs";
23
23
  import { importSources } from "./sign-in-import.mjs";
24
+ import { accountCommand } from "./organization-setup.mjs";
24
25
 
25
26
  export { isDirectExecution };
26
27
 
@@ -36,6 +37,8 @@ Global options:
36
37
  -h, --help Print this help
37
38
  -V, --version Print the version
38
39
  --repository <path> Repository to inspect (default: .)
40
+ --org <org> | --user <login> Scan repositories through gh-manager
41
+ --secret-name <template> Secret name; supports {REGISTRY}, {REPO}, {ORG}
39
42
  --format <text|json> Output format (default: text)
40
43
  --offline Do not look up packages on registries
41
44
  --verbose Print command details and output, and
@@ -109,6 +112,9 @@ export async function main(args = process.argv.slice(2)) {
109
112
  options: {
110
113
  ref: { type: "string" },
111
114
  repository: { type: "string", default: "." },
115
+ org: { type: "string" },
116
+ user: { type: "string" },
117
+ "secret-name": { type: "string" },
112
118
  format: { type: "string", default: "text" },
113
119
  verbose: { type: "boolean", default: false },
114
120
  offline: { type: "boolean", default: false },
@@ -168,6 +174,68 @@ export async function main(args = process.argv.slice(2)) {
168
174
  });
169
175
 
170
176
  const repository = path.resolve(values.repository);
177
+ const command = positionals[0];
178
+ if (values.org || values.user) {
179
+ if (values.org && values.user) {
180
+ throw new Error("--org and --user are mutually exclusive");
181
+ }
182
+ if (values.repository !== "." || values.ref) {
183
+ throw new Error("account scans cannot use --repository or --ref");
184
+ }
185
+ if (!["inspect", "plan", "setup"].includes(command)) {
186
+ throw new Error(`unknown command '${command}'`);
187
+ }
188
+ if (command === "setup" && (!values.all || values.package)) {
189
+ throw new Error("account setup requires --all without --package");
190
+ }
191
+ if (values["dry-run"] && values.execute) {
192
+ throw new Error("--dry-run and --execute are mutually exclusive");
193
+ }
194
+ if (values.yes && !values.execute) {
195
+ throw new Error("--yes requires --execute");
196
+ }
197
+ if (values["no-browser"] && !values.execute) {
198
+ throw new Error("--no-browser requires --execute");
199
+ }
200
+ if (values["open-with"] !== undefined && values["no-browser"]) {
201
+ throw new Error("--open-with and --no-browser are mutually exclusive");
202
+ }
203
+ if (!browserOptions.attach) {
204
+ browserOptions.import ??= { browser: "auto", profile: null };
205
+ }
206
+ return accountCommand(
207
+ command,
208
+ {
209
+ org: values.org,
210
+ user: values.user,
211
+ format: values.format,
212
+ offline: values.offline,
213
+ verbose: values.verbose,
214
+ registries: (values.registry ?? []).map(parseRegistry),
215
+ execute: values.execute,
216
+ yes: values.yes,
217
+ noBrowser: values["no-browser"],
218
+ secretName: values["secret-name"],
219
+ package: values.package,
220
+ browser: values.browser,
221
+ browserOptions,
222
+ browserProfile: path.resolve(
223
+ values["browser-profile"] ??
224
+ defaultBrowserProfile({ channel: browserOptions.channel }),
225
+ ),
226
+ openWith: nonEmpty(values["open-with"], "--open-with"),
227
+ keepSession: values["keep-session"],
228
+ planOptions: {
229
+ workflow: values.workflow,
230
+ publisherEnvironment: values.environment,
231
+ addPublishJob: values["add-publish-job"],
232
+ manual: values.manual,
233
+ verifyRelease: values["verify-release"],
234
+ },
235
+ },
236
+ outputPlans,
237
+ );
238
+ }
171
239
  const discovered = values.ref
172
240
  ? await inspectReference(repository, values.ref, {
173
241
  includeSkipped: values.verbose,
@@ -176,7 +244,6 @@ export async function main(args = process.argv.slice(2)) {
176
244
  const inspection = values.offline
177
245
  ? discovered
178
246
  : await probeRegistryState(discovered, { verbose: values.verbose });
179
- const command = positionals[0];
180
247
  if (command === "inspect") {
181
248
  outputInspection(inspection, values.format);
182
249
  return;
@@ -260,6 +327,10 @@ export async function main(args = process.argv.slice(2)) {
260
327
  noBrowser: values["no-browser"],
261
328
  openWith: nonEmpty(values["open-with"], "--open-with"),
262
329
  keepSession: values["keep-session"],
330
+ secretName: values["secret-name"],
331
+ quietBrowser: selected.some((plan) =>
332
+ plan.steps.some((step) => step.id === "manage-registry-token"),
333
+ ),
263
334
  browser: values.browser,
264
335
  browserOptions,
265
336
  browserProfile: path.resolve(
@@ -84,7 +84,7 @@ export function credentialSteps(registry, secret) {
84
84
  id: "manage-registry-token",
85
85
  title: `Ensure and rotate ${secret}`,
86
86
  kind: "api",
87
- description: `${credentialPolicy(registry).description}. Check expiry and validity, create the narrowest publishing credential in the browser, store through gh-manager stdin, verify in a dry-run workflow, then revoke and verify the replaced token. Requires gh-manager secret support and a configured verification workflow.`,
87
+ description: `${credentialPolicy(registry).description}. Check gh-manager CI health; keep healthy secrets, create missing or auth-failing credentials in the browser, ensure organization access with repository fallback, test through gh-manager, then revoke the replaced token. Retry once only on an authentication failure.`,
88
88
  confirm: true,
89
89
  },
90
90
  ];