package-registry-manager 0.26.0 → 0.27.1
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/CHANGELOG.md +10 -0
- package/README.md +8 -0
- package/package.json +2 -2
- package/src/central-gpg.mjs +278 -0
- package/src/central-namespace.mjs +258 -0
- package/src/central-packagist-steps.mjs +454 -0
- package/src/central-page.mjs +156 -0
- package/src/central-publication.mjs +231 -0
- package/src/central-signing.mjs +143 -0
- package/src/central-token-page.mjs +66 -0
- package/src/credential-cycle.mjs +7 -0
- package/src/discovery.mjs +21 -36
- package/src/extra-flows.mjs +22 -13
- package/src/gradle-distribution.mjs +410 -0
- package/src/model.mjs +4 -0
- package/src/nuget-page.mjs +102 -19
- package/src/nuget-policy.mjs +68 -17
- package/src/nuget-setup.mjs +14 -6
- package/src/organization-scan.mjs +34 -2
- package/src/plan.mjs +14 -0
- package/src/publishers.mjs +14 -5
- package/src/scan-sidecars.mjs +132 -0
- package/src/setup.mjs +15 -1
- package/src/swift-distribution.mjs +249 -0
- package/src/swift-release.mjs +180 -0
package/CHANGELOG.md
CHANGED
|
@@ -27,6 +27,13 @@ currently shares its release version with the Rust implementation.
|
|
|
27
27
|
- An npm first publish stops before the sign-in when the packed tarball
|
|
28
28
|
contains repository development directories such as `.github/` or `tests/`,
|
|
29
29
|
which a package.json without a `files` allowlist lets in.
|
|
30
|
+
- SwiftPM packages (`--registry swift`) are discovered from `Package.swift`
|
|
31
|
+
and `swift/VERSION` and verified by exact bare tag, commit, tagged manifest,
|
|
32
|
+
public release and consumer (#77). Kotlin Multiplatform Gradle publications
|
|
33
|
+
are discovered with their platform variants and verified by root POM,
|
|
34
|
+
`.module` metadata and every variant (#78).
|
|
35
|
+
- Generated Central, Packagist and SwiftPM setup steps run their own handlers;
|
|
36
|
+
manual prerequisites stay incomplete without read-back evidence (#79).
|
|
30
37
|
|
|
31
38
|
### Changed
|
|
32
39
|
|
|
@@ -62,6 +69,9 @@ currently shares its release version with the Rust implementation.
|
|
|
62
69
|
packages; they appear with `"steps": []` and a `skipped_reason`, and `setup`
|
|
63
70
|
picks the only publishable package when a registry also has private ones
|
|
64
71
|
(#4).
|
|
72
|
+
- Central and Packagist steps no longer reach the crates.io session (#79).
|
|
73
|
+
Account scans fetch referenced version sidecars at the scanned commit, so
|
|
74
|
+
they agree with local inspection (#80).
|
|
65
75
|
|
|
66
76
|
## [0.19.35] - 2026-09-21
|
|
67
77
|
|
package/README.md
CHANGED
|
@@ -82,3 +82,11 @@ NuGet setup uses an explicit login profile, package owner and exact package IDs.
|
|
|
82
82
|
Trusted publishing is preferred; existing provided fallback credentials are retained.
|
|
83
83
|
See [audit and NuGet configuration](../docs/issue-73-requirements.md) for setup,
|
|
84
84
|
identity decisions and the distinction between verified configuration and release.
|
|
85
|
+
|
|
86
|
+
NuGet setup resolves reusable publishing identities and verifies saved policy
|
|
87
|
+
scopes after reload. Package renames repair the oldest matching policy in place,
|
|
88
|
+
preserving identity and unrelated policies; first publication requires the exact
|
|
89
|
+
new ID and explicit `package:push` authorization. Maven Central setup verifies authenticated namespace
|
|
90
|
+
ownership before a first release, supports explicitly authorized GnuPG signing
|
|
91
|
+
bootstrap, and preserves actual token expiry metadata. Both runtimes use the same
|
|
92
|
+
page adapters and GPG driver. See the [Central setup configuration](../README.md#maven-central).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "package-registry-manager",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.1",
|
|
4
4
|
"description": "Inspect repositories and guide secure package-registry setup",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"audit": "node scripts/npm-audit.mjs"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@link-foundation/gh-manager": "https://codeload.github.com/link-foundation/gh-manager/tar.gz/
|
|
43
|
+
"@link-foundation/gh-manager": "https://codeload.github.com/link-foundation/gh-manager/tar.gz/cab12f0404d1bc90c72eacea533ce3d5ae8f4894",
|
|
44
44
|
"browser-commander": "^0.34.1",
|
|
45
45
|
"command-stream": "^2.6.0",
|
|
46
46
|
"playwright": "^1.63.0",
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { mkdtemp, readFile, writeFile, rm, mkdir } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { randomBytes } from "node:crypto";
|
|
6
|
+
|
|
7
|
+
/** Run exact GPG arguments; private input/output and diagnostics are never logged. */
|
|
8
|
+
async function command(program, args, input = "", options = {}) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const child = spawn(program, args, {
|
|
11
|
+
...options,
|
|
12
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
13
|
+
});
|
|
14
|
+
const output = [];
|
|
15
|
+
let length = 0;
|
|
16
|
+
child.stdout.on("data", (chunk) => {
|
|
17
|
+
length += chunk.length;
|
|
18
|
+
if (length > 2 * 1024 * 1024) {
|
|
19
|
+
child.kill();
|
|
20
|
+
} else {
|
|
21
|
+
output.push(chunk);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
child.stderr.resume();
|
|
25
|
+
child.on("error", () =>
|
|
26
|
+
reject(new Error("GnuPG is unavailable; no signing values were logged")),
|
|
27
|
+
);
|
|
28
|
+
child.on("close", (code) =>
|
|
29
|
+
code === 0
|
|
30
|
+
? resolve(Buffer.concat(output).toString())
|
|
31
|
+
: reject(
|
|
32
|
+
new Error("GnuPG operation failed; private diagnostics withheld"),
|
|
33
|
+
),
|
|
34
|
+
);
|
|
35
|
+
child.stdin.on("error", () => {});
|
|
36
|
+
child.stdin.end(input);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Isolated GPG signing round trip and independent public-key verification. */
|
|
41
|
+
export async function provisionSigning(
|
|
42
|
+
settings,
|
|
43
|
+
slug,
|
|
44
|
+
priorProof,
|
|
45
|
+
run = command,
|
|
46
|
+
) {
|
|
47
|
+
// macOS's per-user temp path can exceed the Unix-domain socket path limit
|
|
48
|
+
// once gpg-agent appends its socket name. /tmp keeps isolated homes short.
|
|
49
|
+
const directory = await mkdtemp(
|
|
50
|
+
path.join(
|
|
51
|
+
process.platform === "darwin" ? "/tmp" : tmpdir(),
|
|
52
|
+
"prm-central-gpg-",
|
|
53
|
+
),
|
|
54
|
+
);
|
|
55
|
+
const signer = path.join(directory, "signer");
|
|
56
|
+
const verifier = path.join(directory, "verifier");
|
|
57
|
+
await mkdir(signer, { mode: 0o700 });
|
|
58
|
+
await mkdir(verifier, { mode: 0o700 });
|
|
59
|
+
const keyserver =
|
|
60
|
+
settings.keyserver ??
|
|
61
|
+
priorProof?.keyserver ??
|
|
62
|
+
"hkps://keyserver.ubuntu.com";
|
|
63
|
+
if (
|
|
64
|
+
keyserver !== "hkps://keyserver.ubuntu.com" &&
|
|
65
|
+
keyserver !== "hkps://keys.openpgp.org"
|
|
66
|
+
) {
|
|
67
|
+
await rm(directory, { recursive: true, force: true });
|
|
68
|
+
throw new Error("use a supported Central public keyserver");
|
|
69
|
+
}
|
|
70
|
+
// Native Windows and MSYS GPG share relative paths but disagree on drive paths.
|
|
71
|
+
// Working inside the keyring avoids conversion and works across temp drives.
|
|
72
|
+
const gpg = (home, args, input) =>
|
|
73
|
+
run("gpg", ["--homedir", ".", "--batch", "--no-tty", ...args], input, {
|
|
74
|
+
cwd: home,
|
|
75
|
+
});
|
|
76
|
+
const listing = async (home, fingerprint) => {
|
|
77
|
+
const result = await gpg(home, [
|
|
78
|
+
"--with-colons",
|
|
79
|
+
"--list-keys",
|
|
80
|
+
fingerprint,
|
|
81
|
+
]);
|
|
82
|
+
const records = result.split("\n").map((line) => line.split(":"));
|
|
83
|
+
const primary = records.find((row) => row[0] === "pub");
|
|
84
|
+
const actual = records.find((row) => row[0] === "fpr")?.[9];
|
|
85
|
+
const expires = primary?.[6] ? Number(primary[6]) * 1000 : null;
|
|
86
|
+
if (
|
|
87
|
+
actual !== fingerprint ||
|
|
88
|
+
!primary ||
|
|
89
|
+
/[red]/.test(primary[1]) ||
|
|
90
|
+
!primary[11]?.includes("s") ||
|
|
91
|
+
!expires ||
|
|
92
|
+
expires <= Date.now()
|
|
93
|
+
) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
"signing primary fingerprint, capability or expiry is invalid",
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
return new Date(expires).toISOString();
|
|
99
|
+
};
|
|
100
|
+
try {
|
|
101
|
+
let fingerprint =
|
|
102
|
+
settings.fingerprint?.toUpperCase() ?? priorProof?.fingerprint;
|
|
103
|
+
let privateKey;
|
|
104
|
+
let passphrase;
|
|
105
|
+
let proof = priorProof;
|
|
106
|
+
if (!proof) {
|
|
107
|
+
passphrase = settings.passphrase_env
|
|
108
|
+
? process.env[settings.passphrase_env]
|
|
109
|
+
: undefined;
|
|
110
|
+
if (settings.private_key_file || fingerprint) {
|
|
111
|
+
if (passphrase === undefined) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
"reuse requires passphrase_env for the existing signing material",
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
if (settings.private_key_file) {
|
|
117
|
+
privateKey = await readFile(settings.private_key_file, "utf8");
|
|
118
|
+
} else {
|
|
119
|
+
const args = [
|
|
120
|
+
"--batch",
|
|
121
|
+
"--no-tty",
|
|
122
|
+
...(settings.key_home ? ["--homedir", "."] : []),
|
|
123
|
+
"--pinentry-mode",
|
|
124
|
+
"loopback",
|
|
125
|
+
"--passphrase-fd",
|
|
126
|
+
"0",
|
|
127
|
+
"--armor",
|
|
128
|
+
"--export-secret-keys",
|
|
129
|
+
fingerprint,
|
|
130
|
+
];
|
|
131
|
+
privateKey = await run("gpg", args, `${passphrase}\n`, {
|
|
132
|
+
cwd: settings.key_home,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
await gpg(signer, ["--import"], privateKey);
|
|
136
|
+
} else if (settings.generate === true) {
|
|
137
|
+
if (
|
|
138
|
+
!settings.identity ||
|
|
139
|
+
/[\r\n]/.test(settings.identity) ||
|
|
140
|
+
!settings.identity.includes(slug)
|
|
141
|
+
) {
|
|
142
|
+
throw new Error("a repository-specific signing identity is required");
|
|
143
|
+
}
|
|
144
|
+
passphrase = randomBytes(32).toString("base64url");
|
|
145
|
+
await gpg(
|
|
146
|
+
signer,
|
|
147
|
+
[
|
|
148
|
+
"--pinentry-mode",
|
|
149
|
+
"loopback",
|
|
150
|
+
"--passphrase-fd",
|
|
151
|
+
"0",
|
|
152
|
+
"--quick-generate-key",
|
|
153
|
+
settings.identity,
|
|
154
|
+
"rsa4096",
|
|
155
|
+
"sign",
|
|
156
|
+
"1y",
|
|
157
|
+
],
|
|
158
|
+
`${passphrase}\n`,
|
|
159
|
+
);
|
|
160
|
+
} else {
|
|
161
|
+
throw new Error(
|
|
162
|
+
"explicit signing reuse or authorized generation settings are required",
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
const keys = await gpg(signer, ["--with-colons", "--list-secret-keys"]);
|
|
166
|
+
const fingerprints = keys
|
|
167
|
+
.split("\n")
|
|
168
|
+
.filter((line) => line.startsWith("fpr:"))
|
|
169
|
+
.map((line) => line.split(":")[9]);
|
|
170
|
+
fingerprint ??= fingerprints[0];
|
|
171
|
+
if (
|
|
172
|
+
!/^[A-F0-9]{40,64}$/.test(fingerprint ?? "") ||
|
|
173
|
+
!fingerprints.includes(fingerprint)
|
|
174
|
+
) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
"selected private signing fingerprint was not imported",
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
const expiresAt = await listing(signer, fingerprint);
|
|
180
|
+
const payload = `package-registry-manager Central signing proof for ${slug}\n${randomBytes(32).toString("hex")}\n`;
|
|
181
|
+
const dataFile = path.join(directory, "proof.txt");
|
|
182
|
+
const signatureFile = path.join(directory, "proof.asc");
|
|
183
|
+
await writeFile(dataFile, payload, { mode: 0o600 });
|
|
184
|
+
await gpg(
|
|
185
|
+
signer,
|
|
186
|
+
[
|
|
187
|
+
"--pinentry-mode",
|
|
188
|
+
"loopback",
|
|
189
|
+
"--passphrase-fd",
|
|
190
|
+
"0",
|
|
191
|
+
"--local-user",
|
|
192
|
+
`${fingerprint}!`,
|
|
193
|
+
"--armor",
|
|
194
|
+
"--detach-sign",
|
|
195
|
+
"--output",
|
|
196
|
+
"../proof.asc",
|
|
197
|
+
"../proof.txt",
|
|
198
|
+
],
|
|
199
|
+
`${passphrase}\n`,
|
|
200
|
+
);
|
|
201
|
+
await gpg(signer, ["--verify", "../proof.asc", "../proof.txt"]);
|
|
202
|
+
// send-keys exports public packets only; never upload the private armor.
|
|
203
|
+
await gpg(signer, [
|
|
204
|
+
"--keyserver",
|
|
205
|
+
keyserver,
|
|
206
|
+
"--keyserver-options",
|
|
207
|
+
"timeout=15",
|
|
208
|
+
"--send-keys",
|
|
209
|
+
fingerprint,
|
|
210
|
+
]);
|
|
211
|
+
privateKey ??= await gpg(
|
|
212
|
+
signer,
|
|
213
|
+
[
|
|
214
|
+
"--pinentry-mode",
|
|
215
|
+
"loopback",
|
|
216
|
+
"--passphrase-fd",
|
|
217
|
+
"0",
|
|
218
|
+
"--armor",
|
|
219
|
+
"--export-secret-keys",
|
|
220
|
+
fingerprint,
|
|
221
|
+
],
|
|
222
|
+
`${passphrase}\n`,
|
|
223
|
+
);
|
|
224
|
+
proof = {
|
|
225
|
+
repository: slug,
|
|
226
|
+
fingerprint,
|
|
227
|
+
expires_at: expiresAt,
|
|
228
|
+
payload,
|
|
229
|
+
signature: await readFile(signatureFile, "utf8"),
|
|
230
|
+
keyserver,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
if (
|
|
234
|
+
proof.repository !== slug ||
|
|
235
|
+
fingerprint !== proof.fingerprint ||
|
|
236
|
+
!proof.payload.startsWith(
|
|
237
|
+
`package-registry-manager Central signing proof for ${slug}\n`,
|
|
238
|
+
) ||
|
|
239
|
+
!/^[A-F0-9]{40,64}$/.test(fingerprint ?? "")
|
|
240
|
+
) {
|
|
241
|
+
throw new Error("signing proof identity mismatch");
|
|
242
|
+
}
|
|
243
|
+
await gpg(verifier, [
|
|
244
|
+
"--keyserver",
|
|
245
|
+
keyserver,
|
|
246
|
+
"--keyserver-options",
|
|
247
|
+
"timeout=15",
|
|
248
|
+
"--recv-keys",
|
|
249
|
+
fingerprint,
|
|
250
|
+
]);
|
|
251
|
+
const expiresAt = await listing(verifier, fingerprint);
|
|
252
|
+
if (expiresAt !== proof.expires_at) {
|
|
253
|
+
throw new Error("public key expiry changed; review signing settings");
|
|
254
|
+
}
|
|
255
|
+
const dataFile = path.join(directory, "verify.txt");
|
|
256
|
+
const signatureFile = path.join(directory, "verify.asc");
|
|
257
|
+
await writeFile(dataFile, proof.payload, { mode: 0o600 });
|
|
258
|
+
await writeFile(signatureFile, proof.signature, { mode: 0o600 });
|
|
259
|
+
const status = await gpg(verifier, [
|
|
260
|
+
"--status-fd",
|
|
261
|
+
"1",
|
|
262
|
+
"--verify",
|
|
263
|
+
"../verify.asc",
|
|
264
|
+
"../verify.txt",
|
|
265
|
+
]);
|
|
266
|
+
if (!status.includes(`[GNUPG:] VALIDSIG ${fingerprint} `)) {
|
|
267
|
+
throw new Error("independent signature fingerprint verification failed");
|
|
268
|
+
}
|
|
269
|
+
return { proof, private_key: privateKey, passphrase };
|
|
270
|
+
} finally {
|
|
271
|
+
for (const home of [signer, verifier]) {
|
|
272
|
+
await run("gpgconf", ["--homedir", ".", "--kill", "gpg-agent"], "", {
|
|
273
|
+
cwd: home,
|
|
274
|
+
}).catch(() => {});
|
|
275
|
+
}
|
|
276
|
+
await rm(directory, { recursive: true, force: true });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import centralPage from "./central-page.mjs";
|
|
4
|
+
import { githubServices } from "./github.mjs";
|
|
5
|
+
import { repositorySlug } from "./repository-identity.mjs";
|
|
6
|
+
|
|
7
|
+
/** Namespace ownership comes from the authenticated Portal, never public search. */
|
|
8
|
+
export async function verifyNamespace(session, step) {
|
|
9
|
+
const namespace = step.namespace;
|
|
10
|
+
if (!namespace) {
|
|
11
|
+
return {
|
|
12
|
+
status: "manual-prerequisite",
|
|
13
|
+
reason: "the package coordinates name no namespace",
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
if (
|
|
17
|
+
session.options.noBrowser ||
|
|
18
|
+
session.options.browserOptions?.attach ||
|
|
19
|
+
session.options.browserOptions?.importScope === "full"
|
|
20
|
+
) {
|
|
21
|
+
return {
|
|
22
|
+
status: "manual-prerequisite",
|
|
23
|
+
namespace,
|
|
24
|
+
reason:
|
|
25
|
+
"authenticated Central ownership read-back requires the dedicated browser",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const config = JSON.parse(
|
|
29
|
+
await readFile(
|
|
30
|
+
path.join(
|
|
31
|
+
session.options.repository ?? ".",
|
|
32
|
+
".package-registry-manager.json",
|
|
33
|
+
),
|
|
34
|
+
"utf8",
|
|
35
|
+
).catch((e) => {
|
|
36
|
+
if (e.code === "ENOENT") {
|
|
37
|
+
return "{}";
|
|
38
|
+
}
|
|
39
|
+
throw e;
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
const expected = {
|
|
43
|
+
namespace,
|
|
44
|
+
account: config.central?.account,
|
|
45
|
+
organization: config.central?.organization,
|
|
46
|
+
};
|
|
47
|
+
const page = await session.automatedPage(
|
|
48
|
+
{ ...session.options.browserOptions, importScope: "domains" },
|
|
49
|
+
true,
|
|
50
|
+
);
|
|
51
|
+
await page.goto("https://central.sonatype.com/publishing/namespaces");
|
|
52
|
+
const inspect = async (action = "read") =>
|
|
53
|
+
page.evaluate(
|
|
54
|
+
`(${centralPage})(${JSON.stringify(expected)},${JSON.stringify(action)})`,
|
|
55
|
+
);
|
|
56
|
+
let state = await inspect();
|
|
57
|
+
const directory = path.join(
|
|
58
|
+
session.options.repository ?? ".",
|
|
59
|
+
".package-registry-manager",
|
|
60
|
+
);
|
|
61
|
+
const obligation = path.join(directory, "central-namespace-proof.json");
|
|
62
|
+
const save = async (value) => {
|
|
63
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
64
|
+
const temporary = `${obligation}.tmp`;
|
|
65
|
+
await writeFile(temporary, JSON.stringify(value), { mode: 0o600 });
|
|
66
|
+
await rename(temporary, obligation);
|
|
67
|
+
};
|
|
68
|
+
const pending = JSON.parse(
|
|
69
|
+
await readFile(obligation, "utf8").catch((e) => {
|
|
70
|
+
if (e.code === "ENOENT") {
|
|
71
|
+
return "null";
|
|
72
|
+
}
|
|
73
|
+
throw e;
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
const github = session.options.github ?? githubServices(session.options);
|
|
77
|
+
const cleanup = async (proof) => {
|
|
78
|
+
if (
|
|
79
|
+
!proof ||
|
|
80
|
+
proof.namespace !== namespace ||
|
|
81
|
+
proof.account !== state.account
|
|
82
|
+
) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
const repo = await github.rest.request(`/repos/${proof.slug}`, {
|
|
87
|
+
allowNotFound: true,
|
|
88
|
+
});
|
|
89
|
+
if (!repo) {
|
|
90
|
+
await save(null);
|
|
91
|
+
state.cleanup = { status: "complete", repository: proof.slug };
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (
|
|
95
|
+
repo.id !== proof.id ||
|
|
96
|
+
repo.owner.id !== proof.owner_id ||
|
|
97
|
+
repo.size !== 0 ||
|
|
98
|
+
repo.default_branch !== proof.default_branch
|
|
99
|
+
) {
|
|
100
|
+
throw new Error("proof repository changed");
|
|
101
|
+
}
|
|
102
|
+
const branches = await github.rest.request(
|
|
103
|
+
`/repos/${proof.slug}/branches`,
|
|
104
|
+
);
|
|
105
|
+
if (!Array.isArray(branches) || branches.length) {
|
|
106
|
+
throw new Error("proof repository is no longer empty");
|
|
107
|
+
}
|
|
108
|
+
await github.rest.request(`/repos/${proof.slug}`, { method: "DELETE" });
|
|
109
|
+
const gone = await github.rest.request(`/repos/${proof.slug}`, {
|
|
110
|
+
allowNotFound: true,
|
|
111
|
+
});
|
|
112
|
+
if (gone) {
|
|
113
|
+
throw new Error("proof repository deletion unverified");
|
|
114
|
+
}
|
|
115
|
+
await save(null);
|
|
116
|
+
state.cleanup = { status: "complete", repository: proof.slug };
|
|
117
|
+
} catch {
|
|
118
|
+
state.cleanup = {
|
|
119
|
+
status: "pending",
|
|
120
|
+
repository: proof.slug,
|
|
121
|
+
reason:
|
|
122
|
+
"delete the recorded empty proof repository through an authorized GitHub UI or existing permissions; no token scopes were expanded",
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
if (state.status === "verified") {
|
|
127
|
+
await cleanup(pending);
|
|
128
|
+
return state;
|
|
129
|
+
}
|
|
130
|
+
if (!["missing", "unverified"].includes(state.status)) {
|
|
131
|
+
return state;
|
|
132
|
+
}
|
|
133
|
+
expected.account = state.account;
|
|
134
|
+
if (
|
|
135
|
+
!session.options.yes &&
|
|
136
|
+
!/^y(?:es)?$/i.test(
|
|
137
|
+
await session.prompt(
|
|
138
|
+
`Register/verify ${namespace} for the signed-in Central account and create its temporary public GitHub proof repository? [y/N] `,
|
|
139
|
+
),
|
|
140
|
+
)
|
|
141
|
+
) {
|
|
142
|
+
return {
|
|
143
|
+
status: "interrupted",
|
|
144
|
+
namespace,
|
|
145
|
+
reason: "namespace setup declined",
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
if (state.status === "missing") {
|
|
149
|
+
state = await inspect("register");
|
|
150
|
+
}
|
|
151
|
+
const owner = /^io\.github\.([\w-]+)$/.exec(namespace)?.[1];
|
|
152
|
+
const slug = repositorySlug(session.plan.repository);
|
|
153
|
+
const key = state.verification_key;
|
|
154
|
+
if (state.status === "verified") {
|
|
155
|
+
return state;
|
|
156
|
+
}
|
|
157
|
+
if (
|
|
158
|
+
!owner ||
|
|
159
|
+
!slug ||
|
|
160
|
+
owner.toLowerCase() !== slug.split("/")[0].toLowerCase() ||
|
|
161
|
+
!/^[\w-]+$/.test(key ?? "")
|
|
162
|
+
) {
|
|
163
|
+
return {
|
|
164
|
+
...state,
|
|
165
|
+
status: "manual-prerequisite",
|
|
166
|
+
reason:
|
|
167
|
+
"the exact namespace GitHub owner or proof challenge is unresolved; complete verification in the official portal",
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
const repository = await github.rest.request(`/repos/${slug}`);
|
|
171
|
+
if (
|
|
172
|
+
!Number.isSafeInteger(repository.owner?.id) ||
|
|
173
|
+
repository.owner.id <= 0 ||
|
|
174
|
+
repository.owner.login?.toLowerCase() !== owner.toLowerCase()
|
|
175
|
+
) {
|
|
176
|
+
return {
|
|
177
|
+
status: "wrong-identity",
|
|
178
|
+
reason: "immutable GitHub namespace owner is unverified",
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
const proofSlug = `${repository.owner.login}/${key}`;
|
|
182
|
+
let proof =
|
|
183
|
+
pending?.slug === proofSlug &&
|
|
184
|
+
pending.account === state.account &&
|
|
185
|
+
pending.namespace === namespace
|
|
186
|
+
? pending
|
|
187
|
+
: null;
|
|
188
|
+
const existing = await github.rest.request(`/repos/${proofSlug}`, {
|
|
189
|
+
allowNotFound: true,
|
|
190
|
+
});
|
|
191
|
+
if (existing && (!proof || proof.id !== existing.id)) {
|
|
192
|
+
return {
|
|
193
|
+
...state,
|
|
194
|
+
status: "manual-prerequisite",
|
|
195
|
+
reason:
|
|
196
|
+
"proof repository already exists without a matching cleanup obligation; it was not changed",
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
if (!existing) {
|
|
200
|
+
// Write the intent first, so an interruption never loses the cleanup obligation.
|
|
201
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
202
|
+
proof = {
|
|
203
|
+
namespace,
|
|
204
|
+
account: expected.account,
|
|
205
|
+
slug: proofSlug,
|
|
206
|
+
owner_id: repository.owner.id,
|
|
207
|
+
status: "creation-pending",
|
|
208
|
+
};
|
|
209
|
+
await save(proof);
|
|
210
|
+
const created = await github.rest.request(
|
|
211
|
+
repository.owner.type === "Organization"
|
|
212
|
+
? `/orgs/${repository.owner.login}/repos`
|
|
213
|
+
: "/user/repos",
|
|
214
|
+
{
|
|
215
|
+
method: "POST",
|
|
216
|
+
body: {
|
|
217
|
+
name: key,
|
|
218
|
+
private: false,
|
|
219
|
+
auto_init: false,
|
|
220
|
+
description: "Temporary Maven Central namespace ownership proof",
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
);
|
|
224
|
+
proof = {
|
|
225
|
+
...proof,
|
|
226
|
+
id: created.id,
|
|
227
|
+
default_branch: created.default_branch,
|
|
228
|
+
status: "cleanup-pending",
|
|
229
|
+
};
|
|
230
|
+
await save(proof);
|
|
231
|
+
if (
|
|
232
|
+
created.owner.id !== repository.owner.id ||
|
|
233
|
+
created.private ||
|
|
234
|
+
created.full_name.toLowerCase() !== proofSlug.toLowerCase()
|
|
235
|
+
) {
|
|
236
|
+
return { status: "wrong-identity", cleanup: proof };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
state = await inspect("verify");
|
|
240
|
+
const deadline = Date.now() + (session.options.waitTimeoutMs ?? 600000);
|
|
241
|
+
while (
|
|
242
|
+
state.status === "unverified" &&
|
|
243
|
+
state.state === "VERIFYING" &&
|
|
244
|
+
Date.now() < deadline
|
|
245
|
+
) {
|
|
246
|
+
await new Promise((resolve) =>
|
|
247
|
+
setTimeout(resolve, session.options.pollIntervalMs ?? 1000),
|
|
248
|
+
);
|
|
249
|
+
await page.goto("https://central.sonatype.com/publishing/namespaces");
|
|
250
|
+
state = await inspect();
|
|
251
|
+
}
|
|
252
|
+
if (state.status === "verified") {
|
|
253
|
+
await cleanup(proof);
|
|
254
|
+
} else {
|
|
255
|
+
state.cleanup = { status: "pending", repository: proofSlug };
|
|
256
|
+
}
|
|
257
|
+
return state;
|
|
258
|
+
}
|