package-registry-manager 0.22.4 → 0.22.6
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 +7 -3
- package/package.json +4 -4
- package/src/bootstrap-reference.mjs +76 -0
- package/src/browser-catalogue.mjs +3 -3
- package/src/cli.mjs +67 -26
- package/src/containers.mjs +8 -1
- package/src/credential-browser.mjs +82 -0
- package/src/credential-cycle.mjs +91 -0
- package/src/credential-setup.mjs +210 -0
- package/src/credential-verification.mjs +96 -0
- package/src/discovery.mjs +113 -8
- package/src/extra-flows.mjs +88 -0
- package/src/flows.mjs +29 -19
- package/src/index.mjs +1 -1
- package/src/model.mjs +15 -0
- package/src/npm-policy.mjs +69 -0
- package/src/package-coverage.mjs +106 -0
- package/src/plan.mjs +51 -30
- package/src/prerequisites.mjs +2 -0
- package/src/publishers.mjs +114 -28
- package/src/python.mjs +149 -0
- package/src/setup-batch.mjs +174 -0
- package/src/setup.mjs +75 -61
- package/src/sign-in-import.mjs +8 -0
- package/src/skips.mjs +3 -0
- package/src/source-code.mjs +61 -0
- package/src/tokens.mjs +8 -0
- package/src/version-guard.mjs +34 -0
- package/src/workflow-proposal.mjs +362 -0
- package/src/workflows.mjs +49 -10
package/README.md
CHANGED
|
@@ -10,12 +10,16 @@ browser, filling forms in a dedicated visible browser profile.
|
|
|
10
10
|
npx package-registry-manager inspect --repository /path/to/repository
|
|
11
11
|
npx package-registry-manager plan --repository /path/to/repository --format json
|
|
12
12
|
npx package-registry-manager setup --repository /path/to/repository --registry npm
|
|
13
|
+
npx package-registry-manager setup --repository /path/to/repository --all --execute
|
|
13
14
|
```
|
|
14
15
|
|
|
15
16
|
The package installs the command as both `package-registry-manager` and
|
|
16
17
|
`package-registry-manager-js`; use the second name when the Rust command of the
|
|
17
18
|
same name is also on your `PATH`.
|
|
18
19
|
|
|
19
|
-
Setup is a dry run unless `--execute` is present
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
Setup is a dry run unless `--execute` is present. It can publish the first
|
|
21
|
+
version after confirmation; later versions publish through CI. Missing CI
|
|
22
|
+
publishing jobs are offered in a draft pull request, and PyPI builds select an
|
|
23
|
+
interpreter satisfying `requires-python`. See the repository README for the
|
|
24
|
+
registry walkthroughs and safety details.
|
|
25
|
+
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.
|
|
3
|
+
"version": "0.22.6",
|
|
4
4
|
"description": "Inspect repositories and guide secure package-registry setup",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"provenance": true
|
|
29
29
|
},
|
|
30
30
|
"scripts": {
|
|
31
|
-
"syntax": "node --check src/cli.mjs && node --check src/direct-execution.mjs && node --check src/index.mjs && node --check src/browser.mjs && node --check src/browser-options.mjs && node --check src/browser-catalogue.mjs && node --check src/automation.mjs && node --check src/webdriver-automation.mjs && node --check src/profile.mjs && node --check src/setup.mjs && node --check src/auth-urls.mjs && node --check src/containers.mjs && node --check src/flows.mjs && node --check src/prerequisites.mjs && node --check src/registry-state.mjs && node --check src/workflows.mjs && node --check src/publishers.mjs && node --check src/skips.mjs && node --check src/tokens.mjs && node --check src/approvals.mjs && node --check src/default-browser.mjs && node --check src/pages.mjs && node --check src/crates-api.mjs && node --check src/sign-in-import.mjs && node --check scripts/publish-to-npm.mjs && node --check scripts/setup-npm.mjs && node --check scripts/sanitize-npm-userconfig.mjs",
|
|
31
|
+
"syntax": "node --check src/cli.mjs && node --check src/direct-execution.mjs && node --check src/index.mjs && node --check src/browser.mjs && node --check src/browser-options.mjs && node --check src/browser-catalogue.mjs && node --check src/automation.mjs && node --check src/webdriver-automation.mjs && node --check src/profile.mjs && node --check src/setup.mjs && node --check src/auth-urls.mjs && node --check src/containers.mjs && node --check src/flows.mjs && node --check src/prerequisites.mjs && node --check src/registry-state.mjs && node --check src/workflows.mjs && node --check src/publishers.mjs && node --check src/skips.mjs && node --check src/tokens.mjs && node --check src/approvals.mjs && node --check src/default-browser.mjs && node --check src/pages.mjs && node --check src/crates-api.mjs && node --check src/sign-in-import.mjs && node --check scripts/publish-to-npm.mjs && node --check scripts/setup-npm.mjs && node --check scripts/sanitize-npm-userconfig.mjs && node --check src/python.mjs && node --check src/setup-batch.mjs && node --check src/workflow-proposal.mjs",
|
|
32
32
|
"lint": "eslint src test scripts --max-warnings 0",
|
|
33
33
|
"lint:fix": "eslint src test scripts --fix --max-warnings 0",
|
|
34
34
|
"format": "prettier --write .",
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"audit": "node scripts/npm-audit.mjs"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"browser-commander": "^0.
|
|
44
|
-
"command-stream": "^
|
|
43
|
+
"browser-commander": "^0.26.2",
|
|
44
|
+
"command-stream": "^2.0.0",
|
|
45
45
|
"playwright": "^1.63.0",
|
|
46
46
|
"selenium-webdriver": "^4.50.0"
|
|
47
47
|
},
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { inspectRepository } from "./discovery.mjs";
|
|
6
|
+
|
|
7
|
+
/** Translate a branch, PR number, or GitHub pull-request URL into a fetch ref. */
|
|
8
|
+
export function bootstrapRef(ref) {
|
|
9
|
+
const pull =
|
|
10
|
+
/^(?:https:\/\/github\.com\/[\w.-]+\/[\w.-]+\/pull\/)?(\d+)$/.exec(ref);
|
|
11
|
+
const result = pull ? `refs/pull/${pull[1]}/head` : ref;
|
|
12
|
+
if (
|
|
13
|
+
!/^[\w][\w./-]*$/.test(result) ||
|
|
14
|
+
result.includes("..") ||
|
|
15
|
+
result.includes("//") ||
|
|
16
|
+
result.endsWith("/") ||
|
|
17
|
+
result.toLowerCase().endsWith(".lock")
|
|
18
|
+
) {
|
|
19
|
+
throw new Error("invalid bootstrap ref");
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Inspect a pushed ref in a disposable worktree without changing the checkout. */
|
|
25
|
+
export async function inspectReference(repository, ref, options) {
|
|
26
|
+
const run = (args) => {
|
|
27
|
+
const result = spawnSync("git", args, {
|
|
28
|
+
cwd: repository,
|
|
29
|
+
encoding: "utf8",
|
|
30
|
+
});
|
|
31
|
+
if (result.status !== 0) {
|
|
32
|
+
throw new Error(`git failed while inspecting ref: ${result.stderr}`);
|
|
33
|
+
}
|
|
34
|
+
return result.stdout.trim();
|
|
35
|
+
};
|
|
36
|
+
const temporary = await mkdtemp(path.join(os.tmpdir(), "prm-ref-"));
|
|
37
|
+
const checkout = path.join(temporary, "checkout");
|
|
38
|
+
let created = false;
|
|
39
|
+
try {
|
|
40
|
+
run(["fetch", "origin", "HEAD"]);
|
|
41
|
+
const main = run(["rev-parse", "FETCH_HEAD"]);
|
|
42
|
+
run(["fetch", "origin", bootstrapRef(ref)]);
|
|
43
|
+
run(["worktree", "add", "--detach", checkout, "FETCH_HEAD"]);
|
|
44
|
+
created = true;
|
|
45
|
+
const inspection = await inspectRepository(checkout, options);
|
|
46
|
+
for (const item of inspection.packages.filter(
|
|
47
|
+
(item) => item.registry === "npm",
|
|
48
|
+
)) {
|
|
49
|
+
const result = spawnSync("git", ["show", `${main}:${item.manifest}`], {
|
|
50
|
+
cwd: repository,
|
|
51
|
+
encoding: "utf8",
|
|
52
|
+
});
|
|
53
|
+
let relationship = "manifest is absent from remote main";
|
|
54
|
+
if (result.status === 0) {
|
|
55
|
+
const data = JSON.parse(result.stdout);
|
|
56
|
+
relationship = `remote main is ${data.name}@${data.version}`;
|
|
57
|
+
}
|
|
58
|
+
item.warnings = [
|
|
59
|
+
...(item.warnings ?? []),
|
|
60
|
+
`bootstrap ${item.name}@${item.version} from ${ref}; ${relationship}; the next main release must use an unpublished version`,
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
inspection.repository.root = await import("node:fs/promises").then(
|
|
64
|
+
({ realpath }) => realpath(repository),
|
|
65
|
+
);
|
|
66
|
+
return inspection;
|
|
67
|
+
} finally {
|
|
68
|
+
try {
|
|
69
|
+
if (created) {
|
|
70
|
+
run(["worktree", "remove", "--force", checkout]);
|
|
71
|
+
}
|
|
72
|
+
} finally {
|
|
73
|
+
await rm(temporary, { recursive: true, force: true });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -11,7 +11,7 @@ export const browserSources = () => BROWSER_IDS.map(findBrowserSource);
|
|
|
11
11
|
/** Catalogue channels with a launch control protocol. */
|
|
12
12
|
export function launchChannels() {
|
|
13
13
|
return browserSources()
|
|
14
|
-
.filter((source) => source.controlProtocol)
|
|
14
|
+
.filter((source) => ["cdp", "bidi"].includes(source.controlProtocol))
|
|
15
15
|
.flatMap((source) => [source.id, ...(source.aliases ?? [])]);
|
|
16
16
|
}
|
|
17
17
|
|
|
@@ -122,11 +122,11 @@ export function installedDescription(options = {}) {
|
|
|
122
122
|
/** Reject unavailable engines before creating an automated profile. */
|
|
123
123
|
export function validateChannel(channel) {
|
|
124
124
|
const known = findBrowserSource(channel);
|
|
125
|
-
if (known?.controlProtocol) {
|
|
125
|
+
if (["cdp", "bidi"].includes(known?.controlProtocol)) {
|
|
126
126
|
return channel;
|
|
127
127
|
}
|
|
128
128
|
const reason = known
|
|
129
|
-
? `${channel} has no launch control protocol
|
|
129
|
+
? `${channel} has no launch control protocol supported by this manager${known.family === "safari" ? " (Safari setup: https://github.com/link-foundation/browser-commander/issues/126)" : ""}`
|
|
130
130
|
: `unknown --browser-channel '${channel}'`;
|
|
131
131
|
throw new Error(
|
|
132
132
|
`${reason}; choose from ${launchChannels().join(", ")}. ${installedDescription()}`,
|
package/src/cli.mjs
CHANGED
|
@@ -9,12 +9,17 @@ import { parseArgs } from "node:util";
|
|
|
9
9
|
import { launchChannels } from "./browser-catalogue.mjs";
|
|
10
10
|
import { parseBrowserOptions } from "./browser-options.mjs";
|
|
11
11
|
import { isDirectExecution } from "./direct-execution.mjs";
|
|
12
|
+
import { inspectReference } from "./bootstrap-reference.mjs";
|
|
12
13
|
import { inspectRepository } from "./discovery.mjs";
|
|
13
14
|
import { parseRegistry } from "./model.mjs";
|
|
14
15
|
import { buildPlans } from "./plan.mjs";
|
|
15
16
|
import { probeEnvironment, renderPrerequisites } from "./prerequisites.mjs";
|
|
16
17
|
import { probeRegistryState } from "./registry-state.mjs";
|
|
17
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
BROWSER_MODES,
|
|
20
|
+
defaultBrowserProfile,
|
|
21
|
+
executePlans,
|
|
22
|
+
} from "./setup.mjs";
|
|
18
23
|
import { importSources } from "./sign-in-import.mjs";
|
|
19
24
|
|
|
20
25
|
export { isDirectExecution };
|
|
@@ -25,6 +30,7 @@ Commands:
|
|
|
25
30
|
inspect Discover supported package manifests
|
|
26
31
|
plan [--registry <registry>] Print ordered setup plans
|
|
27
32
|
setup --registry <registry> Bootstrap or attach trusted publishing
|
|
33
|
+
setup --all Set up every publishable package in one session
|
|
28
34
|
|
|
29
35
|
Global options:
|
|
30
36
|
-h, --help Print this help
|
|
@@ -41,7 +47,10 @@ Plan and setup options:
|
|
|
41
47
|
--workflow <file> Trusted-publisher workflow file in
|
|
42
48
|
.github/workflows, when detection finds
|
|
43
49
|
several or the wrong one
|
|
50
|
+
--add-publish-job Propose missing publishing jobs in a draft PR;
|
|
51
|
+
--workflow chooses its target file
|
|
44
52
|
--environment <name> GitHub environment of the trusted publisher
|
|
53
|
+
--ref <branch|pr> Bootstrap a pushed branch or PR head before merge
|
|
45
54
|
--package <name> Plan only this package, or select it for
|
|
46
55
|
setup when a registry has several
|
|
47
56
|
|
|
@@ -98,17 +107,20 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
98
107
|
allowPositionals: true,
|
|
99
108
|
strict: true,
|
|
100
109
|
options: {
|
|
110
|
+
ref: { type: "string" },
|
|
101
111
|
repository: { type: "string", default: "." },
|
|
102
112
|
format: { type: "string", default: "text" },
|
|
103
113
|
verbose: { type: "boolean", default: false },
|
|
104
114
|
offline: { type: "boolean", default: false },
|
|
105
115
|
"verify-release": { type: "boolean", default: false },
|
|
106
116
|
workflow: { type: "string" },
|
|
117
|
+
"add-publish-job": { type: "boolean", default: false },
|
|
107
118
|
environment: { type: "string" },
|
|
108
119
|
"dry-run": { type: "boolean", default: false },
|
|
109
120
|
registry: { type: "string", multiple: true },
|
|
110
121
|
package: { type: "string" },
|
|
111
122
|
execute: { type: "boolean", default: false },
|
|
123
|
+
all: { type: "boolean", default: false },
|
|
112
124
|
yes: { type: "boolean", default: false },
|
|
113
125
|
"no-browser": { type: "boolean", default: false },
|
|
114
126
|
"open-with": { type: "string" },
|
|
@@ -156,9 +168,11 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
156
168
|
});
|
|
157
169
|
|
|
158
170
|
const repository = path.resolve(values.repository);
|
|
159
|
-
const discovered =
|
|
160
|
-
|
|
161
|
-
|
|
171
|
+
const discovered = values.ref
|
|
172
|
+
? await inspectReference(repository, values.ref, {
|
|
173
|
+
includeSkipped: values.verbose,
|
|
174
|
+
})
|
|
175
|
+
: await inspectRepository(repository, { includeSkipped: values.verbose });
|
|
162
176
|
const inspection = values.offline
|
|
163
177
|
? discovered
|
|
164
178
|
: await probeRegistryState(discovered, { verbose: values.verbose });
|
|
@@ -175,6 +189,12 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
175
189
|
const environment = await probeEnvironment({
|
|
176
190
|
offline: values.offline,
|
|
177
191
|
verbose: values.verbose,
|
|
192
|
+
python: inspection.packages.some(
|
|
193
|
+
(item) =>
|
|
194
|
+
item.registry === "pypi" &&
|
|
195
|
+
item.publishable &&
|
|
196
|
+
(registries.length === 0 || registries.includes("pypi")),
|
|
197
|
+
),
|
|
178
198
|
npm: inspection.packages.some(
|
|
179
199
|
(item) =>
|
|
180
200
|
item.registry === "npm" &&
|
|
@@ -183,9 +203,15 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
183
203
|
),
|
|
184
204
|
});
|
|
185
205
|
const planOptions = {
|
|
206
|
+
ref: values.ref,
|
|
186
207
|
verifyRelease: values["verify-release"],
|
|
187
208
|
manual: values.manual,
|
|
188
|
-
workflow: await workflowOverride(
|
|
209
|
+
workflow: await workflowOverride(
|
|
210
|
+
repository,
|
|
211
|
+
values.workflow,
|
|
212
|
+
values["add-publish-job"],
|
|
213
|
+
),
|
|
214
|
+
addPublishJob: values["add-publish-job"],
|
|
189
215
|
publisherEnvironment: nonEmpty(values.environment, "--environment"),
|
|
190
216
|
environment,
|
|
191
217
|
browser: browserSummary(command, values, browserOptions),
|
|
@@ -198,7 +224,10 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
198
224
|
outputPlans(plans, values.format);
|
|
199
225
|
return;
|
|
200
226
|
}
|
|
201
|
-
if (
|
|
227
|
+
if (values.all && values.package) {
|
|
228
|
+
throw new Error("--all and --package are mutually exclusive");
|
|
229
|
+
}
|
|
230
|
+
if (!values.all && registries.length !== 1) {
|
|
202
231
|
throw new Error("setup requires exactly one --registry <registry>");
|
|
203
232
|
}
|
|
204
233
|
if (values["dry-run"] && values.execute) {
|
|
@@ -215,27 +244,36 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
215
244
|
}
|
|
216
245
|
|
|
217
246
|
const plans = buildPlans(inspection, registries, planOptions);
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
values
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
247
|
+
const selected = values.all
|
|
248
|
+
? plans.filter((plan) => plan.package.publishable)
|
|
249
|
+
: [selectPlan(plans, values.package)];
|
|
250
|
+
outputPlans(selected, values.format);
|
|
251
|
+
await executePlans(
|
|
252
|
+
selected,
|
|
253
|
+
{
|
|
254
|
+
inspection,
|
|
255
|
+
workflow: planOptions.workflow,
|
|
256
|
+
publisherEnvironment: planOptions.publisherEnvironment,
|
|
257
|
+
repository,
|
|
258
|
+
execute: values.execute,
|
|
259
|
+
yes: values.yes,
|
|
260
|
+
noBrowser: values["no-browser"],
|
|
261
|
+
openWith: nonEmpty(values["open-with"], "--open-with"),
|
|
262
|
+
keepSession: values["keep-session"],
|
|
263
|
+
browser: values.browser,
|
|
264
|
+
browserOptions,
|
|
265
|
+
browserProfile: path.resolve(
|
|
266
|
+
values["browser-profile"] ??
|
|
267
|
+
defaultBrowserProfile({ channel: browserOptions.channel }),
|
|
268
|
+
),
|
|
269
|
+
verbose: values.verbose,
|
|
270
|
+
verifyRelease: values["verify-release"],
|
|
271
|
+
},
|
|
272
|
+
values.all,
|
|
273
|
+
);
|
|
236
274
|
}
|
|
237
275
|
|
|
238
|
-
async function workflowOverride(repository, workflow) {
|
|
276
|
+
async function workflowOverride(repository, workflow, adding = false) {
|
|
239
277
|
if (workflow === undefined) {
|
|
240
278
|
return undefined;
|
|
241
279
|
}
|
|
@@ -249,7 +287,7 @@ async function workflowOverride(repository, workflow) {
|
|
|
249
287
|
(metadata) => metadata.isFile(),
|
|
250
288
|
() => false,
|
|
251
289
|
);
|
|
252
|
-
if (!found) {
|
|
290
|
+
if (!found && !adding) {
|
|
253
291
|
throw new Error(`--workflow: .github/workflows/${workflow} does not exist`);
|
|
254
292
|
}
|
|
255
293
|
return workflow;
|
|
@@ -382,6 +420,9 @@ function outputPlans(plans, format) {
|
|
|
382
420
|
for (const plan of plans) {
|
|
383
421
|
const mode = plan.mode ? ` (${plan.mode})` : "";
|
|
384
422
|
process.stdout.write(`${plan.registry}: ${plan.package.name}${mode}\n`);
|
|
423
|
+
process.stdout.write(
|
|
424
|
+
` credentials: ${plan.credential_policy.description}\n`,
|
|
425
|
+
);
|
|
385
426
|
if (plan.mode === "complete") {
|
|
386
427
|
process.stdout.write(" trusted publishing is already in use\n");
|
|
387
428
|
}
|
package/src/containers.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { tokenSecrets } from "./publishers.mjs";
|
|
2
|
+
import { stripComments } from "./source-code.mjs";
|
|
1
3
|
import { grantsPackagesWrite, publishingWorkflow } from "./workflows.mjs";
|
|
2
4
|
|
|
3
5
|
export const CONTAINER_FILES = new Set(["Dockerfile", "Containerfile"]);
|
|
@@ -27,7 +29,8 @@ export function containerPackages(dockerfiles, workflows, coordinates) {
|
|
|
27
29
|
continue;
|
|
28
30
|
}
|
|
29
31
|
const name =
|
|
30
|
-
literalImage(workflow.contents, host) ??
|
|
32
|
+
literalImage(stripComments(workflow.contents), host) ??
|
|
33
|
+
defaultImage(coordinates);
|
|
31
34
|
const item = {
|
|
32
35
|
registry,
|
|
33
36
|
name: name ?? "unknown-image",
|
|
@@ -36,6 +39,10 @@ export function containerPackages(dockerfiles, workflows, coordinates) {
|
|
|
36
39
|
publishable: Boolean(name),
|
|
37
40
|
workflow: workflow.name,
|
|
38
41
|
};
|
|
42
|
+
const secrets = tokenSecrets(workflows, registry);
|
|
43
|
+
if (secrets.length) {
|
|
44
|
+
item.token_secrets = [...new Set(secrets.map((secret) => secret.secret))];
|
|
45
|
+
}
|
|
39
46
|
if (!name) {
|
|
40
47
|
item.problems = [
|
|
41
48
|
"the image name could not be derived from the workflow or a GitHub remote",
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/** Account pages used only in the dedicated automated browser. */
|
|
2
|
+
export const TOKEN_PROVIDERS = Object.freeze({
|
|
3
|
+
"docker-hub": {
|
|
4
|
+
url: "https://app.docker.com/settings/personal-access-tokens/create",
|
|
5
|
+
secret: "DOCKERHUB_TOKEN",
|
|
6
|
+
scope: "Read & Write",
|
|
7
|
+
domains: ["docker.com", "docker.io"],
|
|
8
|
+
},
|
|
9
|
+
"maven-central": {
|
|
10
|
+
url: "https://central.sonatype.com/usertoken",
|
|
11
|
+
secret: "MAVEN_CENTRAL_TOKEN",
|
|
12
|
+
scope: "publish",
|
|
13
|
+
domains: ["sonatype.com"],
|
|
14
|
+
},
|
|
15
|
+
"vscode-marketplace": {
|
|
16
|
+
url: "https://dev.azure.com/_usersSettings/tokens",
|
|
17
|
+
secret: "VSCE_PAT",
|
|
18
|
+
scope: "Marketplace (Manage)",
|
|
19
|
+
domains: ["dev.azure.com", "login.microsoftonline.com"],
|
|
20
|
+
},
|
|
21
|
+
"open-vsx": {
|
|
22
|
+
url: "https://open-vsx.org/user-settings/tokens",
|
|
23
|
+
secret: "OVSX_PAT",
|
|
24
|
+
scope: "publish",
|
|
25
|
+
domains: ["open-vsx.org", "github.com"],
|
|
26
|
+
},
|
|
27
|
+
"chrome-web-store": {
|
|
28
|
+
url: "https://developers.google.com/oauthplayground/",
|
|
29
|
+
secret: "CHROME_WEB_STORE_REFRESH_TOKEN",
|
|
30
|
+
scope: "https://www.googleapis.com/auth/chromewebstore",
|
|
31
|
+
domains: ["google.com"],
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
/** Fill only credential name, publishing scope, repository and expiry. Never widen scope. */
|
|
36
|
+
export function tokenFormScript(provider, name, expiresAt) {
|
|
37
|
+
return `(() => {
|
|
38
|
+
const values = ${JSON.stringify({ name, scope: provider.scope, expires: expiresAt.slice(0, 10) })};
|
|
39
|
+
const fields = { name: ['input[name="name"]', 'input[name="description"]', 'input[name="tokenName"]'], scope: ['select[name="scope"]', 'select[name="permissions"]'], expires: ['input[type="date"]', 'input[name="expires_at"]'] };
|
|
40
|
+
const filled = [];
|
|
41
|
+
for (const [key, selectors] of Object.entries(fields)) {
|
|
42
|
+
const field = selectors.map(selector => document.querySelector(selector)).find(Boolean);
|
|
43
|
+
if (!field) continue;
|
|
44
|
+
if (field.tagName === 'SELECT') {
|
|
45
|
+
const option = [...field.options].find(option => option.textContent.trim() === values[key] || option.value === values[key]);
|
|
46
|
+
if (!option) continue;
|
|
47
|
+
field.value = option.value;
|
|
48
|
+
} else field.value = values[key];
|
|
49
|
+
field.dispatchEvent(new Event('input', { bubbles: true }));
|
|
50
|
+
field.dispatchEvent(new Event('change', { bubbles: true }));
|
|
51
|
+
filled.push(key);
|
|
52
|
+
}
|
|
53
|
+
return { filled };
|
|
54
|
+
})()`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Read the one-time value without returning it in any displayed diagnostic. */
|
|
58
|
+
export const READ_TOKEN = `(() => {
|
|
59
|
+
const read = selectors => selectors.map(selector => document.querySelector(selector)).find(Boolean);
|
|
60
|
+
const value = read(['input[data-testid="token-value"]', 'input[name="token"]', 'textarea[name="token"]', '[data-testid="generated-token"]', 'input[name="refresh_token"]']);
|
|
61
|
+
const id = read(['[data-token-id]', 'input[name="token_id"]']);
|
|
62
|
+
const expires = read(['input[name="expires_at"]', '[data-expires-at]']);
|
|
63
|
+
const rawExpiry = expires?.value || expires?.dataset?.expiresAt;
|
|
64
|
+
const expiry = rawExpiry && /^\\d{4}-\\d{2}-\\d{2}$/.test(rawExpiry) ? rawExpiry + 'T00:00:00Z' : rawExpiry;
|
|
65
|
+
return { value: value?.value || value?.textContent?.trim(), id: id?.dataset?.tokenId || id?.value, expires_at: expiry };
|
|
66
|
+
})()`;
|
|
67
|
+
|
|
68
|
+
/** Verify absence only when the token list itself is visible and identifies rows. */
|
|
69
|
+
export function revokedScript(id) {
|
|
70
|
+
if (typeof id !== "string" || id.length > 512) {
|
|
71
|
+
return "false";
|
|
72
|
+
}
|
|
73
|
+
const characters = Array.from(id);
|
|
74
|
+
if (characters.length === 0 || characters.length > 256) {
|
|
75
|
+
return "false";
|
|
76
|
+
}
|
|
77
|
+
// Only numeric literals enter generated code; registry text remains data.
|
|
78
|
+
const points = characters
|
|
79
|
+
.map((character) => character.codePointAt(0))
|
|
80
|
+
.join(",");
|
|
81
|
+
return `(() => { const id = String.fromCodePoint(${points}); const list = document.querySelector('[data-testid="tokens-list"], [data-token-list]'); if (!list) return false; return ![...list.querySelectorAll('[data-token-id]')].some(row => row.dataset.tokenId === id); })()`;
|
|
82
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/** Registry-specific policy; tokens are never a fallback for OIDC registries. */
|
|
2
|
+
export function credentialPolicy(registry) {
|
|
3
|
+
if (
|
|
4
|
+
["npm", "pypi", "crates-io", "rubygems", "nuget", "jsr"].includes(registry)
|
|
5
|
+
) {
|
|
6
|
+
return {
|
|
7
|
+
mode: "trusted",
|
|
8
|
+
description: `${registry}: trusted publishing; remove unused long-lived tokens after verification`,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
if (registry === "ghcr") {
|
|
12
|
+
return {
|
|
13
|
+
mode: "github-token",
|
|
14
|
+
description:
|
|
15
|
+
"ghcr: GITHUB_TOKEN with packages: write; no registry secret",
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
if (["go-modules", "packagist"].includes(registry)) {
|
|
19
|
+
return {
|
|
20
|
+
mode: "tags",
|
|
21
|
+
description: `${registry}: public repository tags; no publishing token secret`,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
mode: "token",
|
|
26
|
+
description: `${registry}: scoped expiring token; gh-manager stores selected-repository organization secrets by default`,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Unknown expiry also requires attention; GitHub cannot return secret values. */
|
|
31
|
+
export function needsRotation(metadata, now = Date.now(), leadDays = 7) {
|
|
32
|
+
const expires = Date.parse(metadata.expires_at ?? "");
|
|
33
|
+
return (
|
|
34
|
+
metadata.present !== true ||
|
|
35
|
+
metadata.valid === false ||
|
|
36
|
+
!Number.isFinite(expires) ||
|
|
37
|
+
expires <= now + leadDays * 86_400_000
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Provision, store, validate, then revoke. A failed validation preserves the old token. */
|
|
42
|
+
export async function rotateCredential(previous, adapter) {
|
|
43
|
+
let created;
|
|
44
|
+
let stored = false;
|
|
45
|
+
try {
|
|
46
|
+
created = await adapter.create();
|
|
47
|
+
if (
|
|
48
|
+
!created.value ||
|
|
49
|
+
!created.id ||
|
|
50
|
+
!Number.isFinite(Date.parse(created.expires_at)) ||
|
|
51
|
+
Date.parse(created.expires_at) <= Date.now()
|
|
52
|
+
) {
|
|
53
|
+
throw new Error("registry returned no usable expiring credential");
|
|
54
|
+
}
|
|
55
|
+
stored = true; // A failed ensure can have stored the secret before expiry metadata failed.
|
|
56
|
+
await adapter.store(created);
|
|
57
|
+
await adapter.verify();
|
|
58
|
+
if (previous.token_id && previous.token_id !== created.id) {
|
|
59
|
+
await adapter.revoke(previous.token_id);
|
|
60
|
+
if (!(await adapter.revoked(previous.token_id))) {
|
|
61
|
+
throw new Error("previous token revocation could not be verified");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return { token_id: created.id, expires_at: created.expires_at };
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (created?.id && (!stored || adapter.restore)) {
|
|
67
|
+
if (stored) {
|
|
68
|
+
await adapter.restore();
|
|
69
|
+
}
|
|
70
|
+
await adapter.revoke(created.id);
|
|
71
|
+
}
|
|
72
|
+
throw error;
|
|
73
|
+
} finally {
|
|
74
|
+
if (created) {
|
|
75
|
+
created.value = "";
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Token-registry plan; setup obtains the value from its dedicated browser. */
|
|
81
|
+
export function credentialSteps(registry, secret) {
|
|
82
|
+
return [
|
|
83
|
+
{
|
|
84
|
+
id: "manage-registry-token",
|
|
85
|
+
title: `Ensure and rotate ${secret}`,
|
|
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.`,
|
|
88
|
+
confirm: true,
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
}
|