@cldmv/gh-broker 0.7.0 → 0.8.0
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 +134 -40
- package/config.example.json +21 -8
- package/config.sample.json +45 -0
- package/deploy/README.md +73 -15
- package/deploy/install.mjs +166 -101
- package/deploy/install.sh +0 -0
- package/deploy/lib/config-merge.mjs +174 -16
- package/deploy/lib/credentials.mjs +143 -0
- package/deploy/lib/install-args.mjs +144 -0
- package/deploy/lib/npm-setup.mjs +164 -0
- package/deploy/lib/secret-store.mjs +20 -2
- package/deploy/lib/system.mjs +26 -2
- package/dist/index.mjs +1638 -202
- package/dist/index.mjs.map +4 -4
- package/docs/TOOLS.md +192 -1
- package/docs/changelog/v0/v0.8.0.md +115 -0
- package/docs/deploy-architecture.md +13 -11
- package/package.json +7 -7
- package/src/availability.ts +42 -0
- package/src/catalog.ts +18 -1
- package/src/config.ts +29 -2
- package/src/router.ts +43 -2
- package/src/secrets.ts +34 -4
- package/src/types.ts +25 -1
- package/src/verbs/ghcr/archive.ts +314 -0
- package/src/verbs/ghcr/config.ts +142 -0
- package/src/verbs/ghcr/jobs.ts +141 -0
- package/src/verbs/ghcr/push.ts +192 -0
- package/src/verbs/ghcr/registry.ts +275 -0
- package/src/verbs/ghcr/tar.ts +203 -0
- package/src/verbs/ghcr_push.ts +264 -0
- package/src/verbs/npm-registry.ts +168 -0
- package/src/verbs/npm-tarball.ts +76 -0
- package/src/verbs/npm_scaffold.ts +325 -0
- package/src/verbs/release-analysis.ts +3 -1
- package/tests/catalog.test.vitest.mts +55 -1
- package/tests/config-merge.test.vitest.mts +275 -8
- package/tests/config.test.vitest.mts +75 -1
- package/tests/credentials.test.vitest.mts +127 -0
- package/tests/ghcr-archive.test.vitest.mts +439 -0
- package/tests/ghcr-config.test.vitest.mts +96 -0
- package/tests/ghcr-push.test.vitest.mts +526 -0
- package/tests/ghcr-registry.test.vitest.mts +206 -0
- package/tests/helpers/ghcr-fixtures.mts +373 -0
- package/tests/install-args.test.vitest.mts +152 -0
- package/tests/issue-labels.test.vitest.mts +15 -0
- package/tests/npm-scaffold.test.vitest.mts +479 -0
- package/tests/npm-setup.test.vitest.mts +196 -0
- package/tests/paths.test.vitest.mts +5 -0
- package/tests/prompt.test.vitest.mts +128 -0
- package/tests/release-prs.test.vitest.mts +10 -0
- package/tests/router.test.vitest.mts +108 -2
- package/tests/secret-store.test.vitest.mts +64 -3
- package/tests/secrets.test.vitest.mts +53 -0
- package/tests/security-alerts.test.vitest.mts +10 -0
- package/tests/server.test.vitest.mts +187 -7
- package/tests/smoke.test.vitest.mts +177 -0
- package/tests/system.test.vitest.mts +355 -0
- package/tests/verb-helpers.test.vitest.mts +46 -0
- package/.configs/fix-headers.json +0 -3
- package/.configs/vitest.config.mjs +0 -44
- package/.github/dependabot.yml +0 -111
- package/.github/workflows/branch-retention.yml +0 -44
- package/.github/workflows/bundle-size.yml +0 -52
- package/.github/workflows/ci.yml +0 -389
- package/.github/workflows/cla.yml +0 -66
- package/.github/workflows/codeql.yml +0 -87
- package/.github/workflows/dependabot-auto-merge.yml +0 -62
- package/.github/workflows/dependabot-recreate.yml +0 -67
- package/.github/workflows/dependency-review.yml +0 -46
- package/.github/workflows/feature-pr.yml +0 -75
- package/.github/workflows/hotfix-redirector.yml +0 -67
- package/.github/workflows/hotfixes-release.yml +0 -70
- package/.github/workflows/labeler.yml +0 -48
- package/.github/workflows/master-commit-audit.yml +0 -56
- package/.github/workflows/member-auto-merge.yml +0 -75
- package/.github/workflows/next-release.yml +0 -76
- package/.github/workflows/next-reset.yml +0 -50
- package/.github/workflows/pr-notify.yml +0 -44
- package/.github/workflows/pr-title-normalizer.yml +0 -52
- package/.github/workflows/provenance.yml +0 -51
- package/.github/workflows/publish.yml +0 -146
- package/.github/workflows/release-merge.yml +0 -104
- package/.github/workflows/release-notify.yml +0 -41
- package/.github/workflows/scorecard.yml +0 -64
- package/.github/workflows/stale.yml +0 -50
- package/.github/workflows/tag-health.yml +0 -70
- package/.github/workflows/update-major-version-tags.yml +0 -94
- package/.github/workflows/v4-bootstrap.yml +0 -115
- package/.github/workflows/welcome.yml +0 -42
package/dist/index.mjs
CHANGED
|
@@ -454,11 +454,11 @@ var require_codegen = __commonJS({
|
|
|
454
454
|
const rhs = this.rhs === void 0 ? "" : ` = ${this.rhs}`;
|
|
455
455
|
return `${varKind} ${this.name}${rhs};` + _n;
|
|
456
456
|
}
|
|
457
|
-
optimizeNames(names2,
|
|
457
|
+
optimizeNames(names2, constants2) {
|
|
458
458
|
if (!names2[this.name.str])
|
|
459
459
|
return;
|
|
460
460
|
if (this.rhs)
|
|
461
|
-
this.rhs = optimizeExpr(this.rhs, names2,
|
|
461
|
+
this.rhs = optimizeExpr(this.rhs, names2, constants2);
|
|
462
462
|
return this;
|
|
463
463
|
}
|
|
464
464
|
get names() {
|
|
@@ -475,10 +475,10 @@ var require_codegen = __commonJS({
|
|
|
475
475
|
render({ _n }) {
|
|
476
476
|
return `${this.lhs} = ${this.rhs};` + _n;
|
|
477
477
|
}
|
|
478
|
-
optimizeNames(names2,
|
|
478
|
+
optimizeNames(names2, constants2) {
|
|
479
479
|
if (this.lhs instanceof code_1.Name && !names2[this.lhs.str] && !this.sideEffects)
|
|
480
480
|
return;
|
|
481
|
-
this.rhs = optimizeExpr(this.rhs, names2,
|
|
481
|
+
this.rhs = optimizeExpr(this.rhs, names2, constants2);
|
|
482
482
|
return this;
|
|
483
483
|
}
|
|
484
484
|
get names() {
|
|
@@ -539,8 +539,8 @@ var require_codegen = __commonJS({
|
|
|
539
539
|
optimizeNodes() {
|
|
540
540
|
return `${this.code}` ? this : void 0;
|
|
541
541
|
}
|
|
542
|
-
optimizeNames(names2,
|
|
543
|
-
this.code = optimizeExpr(this.code, names2,
|
|
542
|
+
optimizeNames(names2, constants2) {
|
|
543
|
+
this.code = optimizeExpr(this.code, names2, constants2);
|
|
544
544
|
return this;
|
|
545
545
|
}
|
|
546
546
|
get names() {
|
|
@@ -569,12 +569,12 @@ var require_codegen = __commonJS({
|
|
|
569
569
|
}
|
|
570
570
|
return nodes.length > 0 ? this : void 0;
|
|
571
571
|
}
|
|
572
|
-
optimizeNames(names2,
|
|
572
|
+
optimizeNames(names2, constants2) {
|
|
573
573
|
const { nodes } = this;
|
|
574
574
|
let i = nodes.length;
|
|
575
575
|
while (i--) {
|
|
576
576
|
const n = nodes[i];
|
|
577
|
-
if (n.optimizeNames(names2,
|
|
577
|
+
if (n.optimizeNames(names2, constants2))
|
|
578
578
|
continue;
|
|
579
579
|
subtractNames(names2, n.names);
|
|
580
580
|
nodes.splice(i, 1);
|
|
@@ -627,12 +627,12 @@ var require_codegen = __commonJS({
|
|
|
627
627
|
return void 0;
|
|
628
628
|
return this;
|
|
629
629
|
}
|
|
630
|
-
optimizeNames(names2,
|
|
630
|
+
optimizeNames(names2, constants2) {
|
|
631
631
|
var _a3;
|
|
632
|
-
this.else = (_a3 = this.else) === null || _a3 === void 0 ? void 0 : _a3.optimizeNames(names2,
|
|
633
|
-
if (!(super.optimizeNames(names2,
|
|
632
|
+
this.else = (_a3 = this.else) === null || _a3 === void 0 ? void 0 : _a3.optimizeNames(names2, constants2);
|
|
633
|
+
if (!(super.optimizeNames(names2, constants2) || this.else))
|
|
634
634
|
return;
|
|
635
|
-
this.condition = optimizeExpr(this.condition, names2,
|
|
635
|
+
this.condition = optimizeExpr(this.condition, names2, constants2);
|
|
636
636
|
return this;
|
|
637
637
|
}
|
|
638
638
|
get names() {
|
|
@@ -655,10 +655,10 @@ var require_codegen = __commonJS({
|
|
|
655
655
|
render(opts) {
|
|
656
656
|
return `for(${this.iteration})` + super.render(opts);
|
|
657
657
|
}
|
|
658
|
-
optimizeNames(names2,
|
|
659
|
-
if (!super.optimizeNames(names2,
|
|
658
|
+
optimizeNames(names2, constants2) {
|
|
659
|
+
if (!super.optimizeNames(names2, constants2))
|
|
660
660
|
return;
|
|
661
|
-
this.iteration = optimizeExpr(this.iteration, names2,
|
|
661
|
+
this.iteration = optimizeExpr(this.iteration, names2, constants2);
|
|
662
662
|
return this;
|
|
663
663
|
}
|
|
664
664
|
get names() {
|
|
@@ -694,10 +694,10 @@ var require_codegen = __commonJS({
|
|
|
694
694
|
render(opts) {
|
|
695
695
|
return `for(${this.varKind} ${this.name} ${this.loop} ${this.iterable})` + super.render(opts);
|
|
696
696
|
}
|
|
697
|
-
optimizeNames(names2,
|
|
698
|
-
if (!super.optimizeNames(names2,
|
|
697
|
+
optimizeNames(names2, constants2) {
|
|
698
|
+
if (!super.optimizeNames(names2, constants2))
|
|
699
699
|
return;
|
|
700
|
-
this.iterable = optimizeExpr(this.iterable, names2,
|
|
700
|
+
this.iterable = optimizeExpr(this.iterable, names2, constants2);
|
|
701
701
|
return this;
|
|
702
702
|
}
|
|
703
703
|
get names() {
|
|
@@ -739,11 +739,11 @@ var require_codegen = __commonJS({
|
|
|
739
739
|
(_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNodes();
|
|
740
740
|
return this;
|
|
741
741
|
}
|
|
742
|
-
optimizeNames(names2,
|
|
742
|
+
optimizeNames(names2, constants2) {
|
|
743
743
|
var _a3, _b;
|
|
744
|
-
super.optimizeNames(names2,
|
|
745
|
-
(_a3 = this.catch) === null || _a3 === void 0 ? void 0 : _a3.optimizeNames(names2,
|
|
746
|
-
(_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNames(names2,
|
|
744
|
+
super.optimizeNames(names2, constants2);
|
|
745
|
+
(_a3 = this.catch) === null || _a3 === void 0 ? void 0 : _a3.optimizeNames(names2, constants2);
|
|
746
|
+
(_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNames(names2, constants2);
|
|
747
747
|
return this;
|
|
748
748
|
}
|
|
749
749
|
get names() {
|
|
@@ -1044,7 +1044,7 @@ var require_codegen = __commonJS({
|
|
|
1044
1044
|
function addExprNames(names2, from) {
|
|
1045
1045
|
return from instanceof code_1._CodeOrName ? addNames(names2, from.names) : names2;
|
|
1046
1046
|
}
|
|
1047
|
-
function optimizeExpr(expr, names2,
|
|
1047
|
+
function optimizeExpr(expr, names2, constants2) {
|
|
1048
1048
|
if (expr instanceof code_1.Name)
|
|
1049
1049
|
return replaceName(expr);
|
|
1050
1050
|
if (!canOptimize(expr))
|
|
@@ -1059,14 +1059,14 @@ var require_codegen = __commonJS({
|
|
|
1059
1059
|
return items;
|
|
1060
1060
|
}, []));
|
|
1061
1061
|
function replaceName(n) {
|
|
1062
|
-
const c =
|
|
1062
|
+
const c = constants2[n.str];
|
|
1063
1063
|
if (c === void 0 || names2[n.str] !== 1)
|
|
1064
1064
|
return n;
|
|
1065
1065
|
delete names2[n.str];
|
|
1066
1066
|
return c;
|
|
1067
1067
|
}
|
|
1068
1068
|
function canOptimize(e) {
|
|
1069
|
-
return e instanceof code_1._Code && e._items.some((c) => c instanceof code_1.Name && names2[c.str] === 1 &&
|
|
1069
|
+
return e instanceof code_1._Code && e._items.some((c) => c instanceof code_1.Name && names2[c.str] === 1 && constants2[c.str] !== void 0);
|
|
1070
1070
|
}
|
|
1071
1071
|
}
|
|
1072
1072
|
function subtractNames(names2, from) {
|
|
@@ -1914,8 +1914,8 @@ var require_keyword = __commonJS({
|
|
|
1914
1914
|
var _a3;
|
|
1915
1915
|
const { gen, keyword, schema, parentSchema, $data, it } = cxt;
|
|
1916
1916
|
checkAsyncKeyword(it, def);
|
|
1917
|
-
const
|
|
1918
|
-
const validateRef = useKeyword(gen, keyword,
|
|
1917
|
+
const validate3 = !$data && def.compile ? def.compile.call(it.self, schema, parentSchema, it) : def.validate;
|
|
1918
|
+
const validateRef = useKeyword(gen, keyword, validate3);
|
|
1919
1919
|
const valid = gen.let("valid");
|
|
1920
1920
|
cxt.block$data(valid, validateKeyword);
|
|
1921
1921
|
cxt.ok((_a3 = def.valid) !== null && _a3 !== void 0 ? _a3 : valid);
|
|
@@ -2279,8 +2279,8 @@ var require_resolve = __commonJS({
|
|
|
2279
2279
|
}
|
|
2280
2280
|
return count;
|
|
2281
2281
|
}
|
|
2282
|
-
function getFullPath(resolver, id = "",
|
|
2283
|
-
if (
|
|
2282
|
+
function getFullPath(resolver, id = "", normalize3) {
|
|
2283
|
+
if (normalize3 !== false)
|
|
2284
2284
|
id = normalizeId(id);
|
|
2285
2285
|
const p = resolver.parse(id);
|
|
2286
2286
|
return _getFullPath(resolver, p);
|
|
@@ -2988,28 +2988,28 @@ var require_compile = __commonJS({
|
|
|
2988
2988
|
if (this.opts.code.process)
|
|
2989
2989
|
sourceCode = this.opts.code.process(sourceCode, sch);
|
|
2990
2990
|
const makeValidate = new Function(`${names_1.default.self}`, `${names_1.default.scope}`, sourceCode);
|
|
2991
|
-
const
|
|
2992
|
-
this.scope.value(validateName, { ref:
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2991
|
+
const validate3 = makeValidate(this, this.scope.get());
|
|
2992
|
+
this.scope.value(validateName, { ref: validate3 });
|
|
2993
|
+
validate3.errors = null;
|
|
2994
|
+
validate3.schema = sch.schema;
|
|
2995
|
+
validate3.schemaEnv = sch;
|
|
2996
2996
|
if (sch.$async)
|
|
2997
|
-
|
|
2997
|
+
validate3.$async = true;
|
|
2998
2998
|
if (this.opts.code.source === true) {
|
|
2999
|
-
|
|
2999
|
+
validate3.source = { validateName, validateCode, scopeValues: gen._values };
|
|
3000
3000
|
}
|
|
3001
3001
|
if (this.opts.unevaluated) {
|
|
3002
3002
|
const { props, items } = schemaCxt;
|
|
3003
|
-
|
|
3003
|
+
validate3.evaluated = {
|
|
3004
3004
|
props: props instanceof codegen_1.Name ? void 0 : props,
|
|
3005
3005
|
items: items instanceof codegen_1.Name ? void 0 : items,
|
|
3006
3006
|
dynamicProps: props instanceof codegen_1.Name,
|
|
3007
3007
|
dynamicItems: items instanceof codegen_1.Name
|
|
3008
3008
|
};
|
|
3009
|
-
if (
|
|
3010
|
-
|
|
3009
|
+
if (validate3.source)
|
|
3010
|
+
validate3.source.evaluated = (0, codegen_1.stringify)(validate3.evaluated);
|
|
3011
3011
|
}
|
|
3012
|
-
sch.validate =
|
|
3012
|
+
sch.validate = validate3;
|
|
3013
3013
|
return sch;
|
|
3014
3014
|
} catch (e) {
|
|
3015
3015
|
delete sch.validate;
|
|
@@ -3875,7 +3875,7 @@ var require_fast_uri = __commonJS({
|
|
|
3875
3875
|
}
|
|
3876
3876
|
return decodedScheme;
|
|
3877
3877
|
}
|
|
3878
|
-
function
|
|
3878
|
+
function normalize3(uri, options) {
|
|
3879
3879
|
if (typeof uri === "string") {
|
|
3880
3880
|
uri = /** @type {T} */
|
|
3881
3881
|
normalizeString(uri, options);
|
|
@@ -4253,7 +4253,7 @@ var require_fast_uri = __commonJS({
|
|
|
4253
4253
|
}
|
|
4254
4254
|
var fastUri = {
|
|
4255
4255
|
SCHEMES,
|
|
4256
|
-
normalize,
|
|
4256
|
+
normalize: normalize3,
|
|
4257
4257
|
resolve,
|
|
4258
4258
|
resolveComponent,
|
|
4259
4259
|
equal,
|
|
@@ -6932,8 +6932,8 @@ var require_formats = __commonJS({
|
|
|
6932
6932
|
"use strict";
|
|
6933
6933
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6934
6934
|
exports.formatNames = exports.fastFormats = exports.fullFormats = void 0;
|
|
6935
|
-
function fmtDef(
|
|
6936
|
-
return { validate:
|
|
6935
|
+
function fmtDef(validate3, compare) {
|
|
6936
|
+
return { validate: validate3, compare };
|
|
6937
6937
|
}
|
|
6938
6938
|
exports.fullFormats = {
|
|
6939
6939
|
// date: http://tools.ietf.org/html/rfc3339#section-5.6
|
|
@@ -7282,12 +7282,12 @@ var require_content_type = __commonJS({
|
|
|
7282
7282
|
if (!string4) {
|
|
7283
7283
|
throw new TypeError("argument string is required");
|
|
7284
7284
|
}
|
|
7285
|
-
var
|
|
7286
|
-
if (typeof
|
|
7285
|
+
var header2 = typeof string4 === "object" ? getcontenttype(string4) : string4;
|
|
7286
|
+
if (typeof header2 !== "string") {
|
|
7287
7287
|
throw new TypeError("argument string is required to be a string");
|
|
7288
7288
|
}
|
|
7289
|
-
var index =
|
|
7290
|
-
var type = index !== -1 ?
|
|
7289
|
+
var index = header2.indexOf(";");
|
|
7290
|
+
var type = index !== -1 ? header2.slice(0, index).trim() : header2.trim();
|
|
7291
7291
|
if (!TYPE_REGEXP.test(type)) {
|
|
7292
7292
|
throw new TypeError("invalid media type");
|
|
7293
7293
|
}
|
|
@@ -7297,7 +7297,7 @@ var require_content_type = __commonJS({
|
|
|
7297
7297
|
var match;
|
|
7298
7298
|
var value;
|
|
7299
7299
|
PARAM_REGEXP.lastIndex = index;
|
|
7300
|
-
while (match = PARAM_REGEXP.exec(
|
|
7300
|
+
while (match = PARAM_REGEXP.exec(header2)) {
|
|
7301
7301
|
if (match.index !== index) {
|
|
7302
7302
|
throw new TypeError("invalid parameter format");
|
|
7303
7303
|
}
|
|
@@ -7312,23 +7312,23 @@ var require_content_type = __commonJS({
|
|
|
7312
7312
|
}
|
|
7313
7313
|
obj.parameters[key2] = value;
|
|
7314
7314
|
}
|
|
7315
|
-
if (index !==
|
|
7315
|
+
if (index !== header2.length) {
|
|
7316
7316
|
throw new TypeError("invalid parameter format");
|
|
7317
7317
|
}
|
|
7318
7318
|
}
|
|
7319
7319
|
return obj;
|
|
7320
7320
|
}
|
|
7321
7321
|
function getcontenttype(obj) {
|
|
7322
|
-
var
|
|
7322
|
+
var header2;
|
|
7323
7323
|
if (typeof obj.getHeader === "function") {
|
|
7324
|
-
|
|
7324
|
+
header2 = obj.getHeader("content-type");
|
|
7325
7325
|
} else if (typeof obj.headers === "object") {
|
|
7326
|
-
|
|
7326
|
+
header2 = obj.headers && obj.headers["content-type"];
|
|
7327
7327
|
}
|
|
7328
|
-
if (typeof
|
|
7328
|
+
if (typeof header2 !== "string") {
|
|
7329
7329
|
throw new TypeError("content-type header is missing from object");
|
|
7330
7330
|
}
|
|
7331
|
-
return
|
|
7331
|
+
return header2;
|
|
7332
7332
|
}
|
|
7333
7333
|
function qstring(val) {
|
|
7334
7334
|
var str = String(val);
|
|
@@ -7349,6 +7349,75 @@ var require_content_type = __commonJS({
|
|
|
7349
7349
|
|
|
7350
7350
|
// src/config.ts
|
|
7351
7351
|
import { readFileSync } from "node:fs";
|
|
7352
|
+
|
|
7353
|
+
// src/verbs/ghcr/config.ts
|
|
7354
|
+
import { isAbsolute, normalize } from "node:path";
|
|
7355
|
+
var DEFAULT_MAX_ARCHIVE_BYTES = 16 * 1024 ** 3;
|
|
7356
|
+
var TOP_KEYS = /* @__PURE__ */ new Set(["dropDir", "protectReleaseTags", "maxArchiveBytes", "owners"]);
|
|
7357
|
+
var OWNER_KEYS = /* @__PURE__ */ new Set(["username", "credential", "images"]);
|
|
7358
|
+
var FORBIDDEN_ROOTS = ["/etc", "/proc", "/sys", "/dev", "/run", "/boot", "/root", "/home", "/tmp", "/usr", "/bin", "/sbin", "/lib", "/lib64", "/var/tmp"];
|
|
7359
|
+
var IMAGE_GLOB = /^[a-z0-9*](?:[a-z0-9._*-]*[a-z0-9*])?(?:\/[a-z0-9*](?:[a-z0-9._*-]*[a-z0-9*])?)*$/;
|
|
7360
|
+
var OWNER_NAME = /^[a-z0-9](?:[a-z0-9-]{0,38})$/;
|
|
7361
|
+
var fail = (msg) => {
|
|
7362
|
+
throw new Error(`config.ghcr${msg}`);
|
|
7363
|
+
};
|
|
7364
|
+
function strictKeys(o, allowed, where) {
|
|
7365
|
+
const extra = Object.keys(o).filter((k) => !allowed.has(k));
|
|
7366
|
+
if (extra.length) fail(`${where}: unknown key(s) ${extra.join(", ")} \u2014 expected ${[...allowed].join(", ")}`);
|
|
7367
|
+
}
|
|
7368
|
+
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
7369
|
+
function parseGhcrConfig(raw) {
|
|
7370
|
+
if (!isObject(raw)) return fail(": must be an object");
|
|
7371
|
+
strictKeys(raw, TOP_KEYS, "");
|
|
7372
|
+
const dropDir = raw.dropDir;
|
|
7373
|
+
if (typeof dropDir !== "string" || !isAbsolute(dropDir)) fail('.dropDir: must be an absolute path, e.g. "/srv/gh-broker/ghcr-drop"');
|
|
7374
|
+
const dir = normalize(dropDir).replace(/\/+$/, "");
|
|
7375
|
+
if (!dir || dir !== dropDir.replace(/\/+$/, "")) fail(".dropDir: must be a normalized path (no ., .. or doubled slashes)");
|
|
7376
|
+
if (dir.split("/").filter(Boolean).length < 2) fail(".dropDir: must be at least two levels deep, e.g. /srv/gh-broker/ghcr-drop");
|
|
7377
|
+
for (const root of FORBIDDEN_ROOTS) {
|
|
7378
|
+
if (dir === root || dir.startsWith(`${root}/`)) fail(`.dropDir: may not be under ${root}`);
|
|
7379
|
+
}
|
|
7380
|
+
if (raw.protectReleaseTags !== void 0 && typeof raw.protectReleaseTags !== "boolean") fail(".protectReleaseTags: must be a boolean");
|
|
7381
|
+
let maxArchiveBytes = DEFAULT_MAX_ARCHIVE_BYTES;
|
|
7382
|
+
if (raw.maxArchiveBytes !== void 0) {
|
|
7383
|
+
const m = raw.maxArchiveBytes;
|
|
7384
|
+
if (typeof m !== "number" || !Number.isSafeInteger(m) || m <= 0) fail(".maxArchiveBytes: must be a positive integer");
|
|
7385
|
+
maxArchiveBytes = m;
|
|
7386
|
+
}
|
|
7387
|
+
if (!isObject(raw.owners) || Object.keys(raw.owners).length === 0) fail(".owners: must be an object with at least one owner");
|
|
7388
|
+
const owners = {};
|
|
7389
|
+
for (const [name, v] of Object.entries(raw.owners)) {
|
|
7390
|
+
const owner2 = name.toLowerCase();
|
|
7391
|
+
if (!OWNER_NAME.test(owner2)) fail(`.owners: "${name}" is not a valid GitHub owner name`);
|
|
7392
|
+
if (!isObject(v)) fail(`.owners.${name}: must be an object`);
|
|
7393
|
+
const o = v;
|
|
7394
|
+
strictKeys(o, OWNER_KEYS, `.owners.${name}`);
|
|
7395
|
+
if (typeof o.username !== "string" || !/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/.test(o.username)) fail(`.owners.${name}.username: must be the GitHub login that owns the PAT`);
|
|
7396
|
+
if (typeof o.credential !== "string" || !/^cred:[A-Za-z0-9._-]+$/.test(o.credential) || /^cred:\.\.?$/.test(o.credential)) {
|
|
7397
|
+
fail(`.owners.${name}.credential: must be a "cred:<name>" ref`);
|
|
7398
|
+
}
|
|
7399
|
+
if (!Array.isArray(o.images) || o.images.length === 0) fail(`.owners.${name}.images: must be a non-empty list of image names or globs`);
|
|
7400
|
+
for (const img of o.images) {
|
|
7401
|
+
if (typeof img !== "string" || img.length > 255 || !IMAGE_GLOB.test(img)) {
|
|
7402
|
+
fail(`.owners.${name}.images: "${String(img)}" is not a valid image name or glob (lowercase, [a-z0-9._-], "/" between segments, "*" within a segment)`);
|
|
7403
|
+
}
|
|
7404
|
+
}
|
|
7405
|
+
owners[owner2] = { username: o.username, credential: o.credential, images: [...o.images] };
|
|
7406
|
+
}
|
|
7407
|
+
return { dropDir: dir, protectReleaseTags: raw.protectReleaseTags !== false, maxArchiveBytes, owners };
|
|
7408
|
+
}
|
|
7409
|
+
function ghcrCredRefs(cfg) {
|
|
7410
|
+
return cfg ? Object.values(cfg.owners).map((o) => o.credential) : [];
|
|
7411
|
+
}
|
|
7412
|
+
function imageGlobToRegExp(glob) {
|
|
7413
|
+
const body = glob.split("*").map((part) => part.replace(/[.\\^$+?()[\]{}|-]/g, "\\$&")).join("[a-z0-9._-]*");
|
|
7414
|
+
return new RegExp(`^${body}$`);
|
|
7415
|
+
}
|
|
7416
|
+
function imageAllowed(owner2, image2) {
|
|
7417
|
+
return owner2.images.some((g) => imageGlobToRegExp(g).test(image2));
|
|
7418
|
+
}
|
|
7419
|
+
|
|
7420
|
+
// src/config.ts
|
|
7352
7421
|
var MODES = ["read", "write"];
|
|
7353
7422
|
function loadConfig(path) {
|
|
7354
7423
|
let raw;
|
|
@@ -7382,15 +7451,15 @@ function loadConfig(path) {
|
|
|
7382
7451
|
});
|
|
7383
7452
|
if (typeof c.owners !== "object" || c.owners === null) throw new Error("config.owners: must be an object");
|
|
7384
7453
|
const owners = {};
|
|
7385
|
-
for (const [
|
|
7454
|
+
for (const [owner2, v] of Object.entries(c.owners)) {
|
|
7386
7455
|
const o = v;
|
|
7387
7456
|
if (typeof o.read !== "string" || typeof o.write !== "string") {
|
|
7388
|
-
throw new Error(`config.owners.${
|
|
7457
|
+
throw new Error(`config.owners.${owner2}: needs "read" and "write" credential refs`);
|
|
7389
7458
|
}
|
|
7390
7459
|
if (o.installationId !== void 0 && typeof o.installationId !== "number") {
|
|
7391
|
-
throw new Error(`config.owners.${
|
|
7460
|
+
throw new Error(`config.owners.${owner2}.installationId: must be a number`);
|
|
7392
7461
|
}
|
|
7393
|
-
owners[
|
|
7462
|
+
owners[owner2.toLowerCase()] = {
|
|
7394
7463
|
read: o.read,
|
|
7395
7464
|
write: o.write,
|
|
7396
7465
|
...typeof o.installationId === "number" ? { installationId: o.installationId } : {}
|
|
@@ -7432,6 +7501,29 @@ function loadConfig(path) {
|
|
|
7432
7501
|
...typeof s.path === "string" ? { path: s.path } : {}
|
|
7433
7502
|
};
|
|
7434
7503
|
}
|
|
7504
|
+
let npm;
|
|
7505
|
+
if (c.npm !== void 0) {
|
|
7506
|
+
if (typeof c.npm !== "object" || c.npm === null || Array.isArray(c.npm)) throw new Error("config.npm: must be an object");
|
|
7507
|
+
const n = c.npm;
|
|
7508
|
+
if (typeof n.token !== "string" || !n.token.startsWith("cred:")) throw new Error('config.npm.token: must be a "cred:<name>" ref');
|
|
7509
|
+
if (!Array.isArray(n.scopes) || n.scopes.length === 0) throw new Error('config.npm.scopes: must be a non-empty array of scopes, e.g. ["@cldmv"]');
|
|
7510
|
+
const scopes = n.scopes.map((s, i) => {
|
|
7511
|
+
if (typeof s !== "string" || !/^@[a-z0-9][a-z0-9._~-]*$/.test(s)) throw new Error(`config.npm.scopes[${i}]: must be a lowercase npm scope like "@cldmv"`);
|
|
7512
|
+
return s;
|
|
7513
|
+
});
|
|
7514
|
+
if (n.registry !== void 0 && (typeof n.registry !== "string" || !/^https:\/\//.test(n.registry))) {
|
|
7515
|
+
throw new Error("config.npm.registry: must be an https URL");
|
|
7516
|
+
}
|
|
7517
|
+
if (n.tokenOrgs !== void 0 && (!Array.isArray(n.tokenOrgs) || !n.tokenOrgs.every((o) => typeof o === "string" && /^[a-z0-9][a-z0-9._~-]*$/.test(o)))) {
|
|
7518
|
+
throw new Error('config.npm.tokenOrgs: must be an array of lowercase npm org names, e.g. ["cldmv"]');
|
|
7519
|
+
}
|
|
7520
|
+
npm = {
|
|
7521
|
+
token: n.token,
|
|
7522
|
+
scopes,
|
|
7523
|
+
...typeof n.registry === "string" ? { registry: n.registry } : {},
|
|
7524
|
+
...Array.isArray(n.tokenOrgs) ? { tokenOrgs: n.tokenOrgs } : {}
|
|
7525
|
+
};
|
|
7526
|
+
}
|
|
7435
7527
|
return {
|
|
7436
7528
|
listen: c.listen,
|
|
7437
7529
|
upstream: c.upstream,
|
|
@@ -7439,7 +7531,9 @@ function loadConfig(path) {
|
|
|
7439
7531
|
owners,
|
|
7440
7532
|
...app ? { app } : {},
|
|
7441
7533
|
...secrets ? { secrets } : {},
|
|
7442
|
-
...defaultOwner ? { defaultOwner } : {}
|
|
7534
|
+
...defaultOwner ? { defaultOwner } : {},
|
|
7535
|
+
...npm ? { npm } : {},
|
|
7536
|
+
...c.ghcr !== void 0 ? { ghcr: parseGhcrConfig(c.ghcr) } : {}
|
|
7443
7537
|
};
|
|
7444
7538
|
}
|
|
7445
7539
|
function indexTools(tools) {
|
|
@@ -7463,6 +7557,20 @@ function loadSecretStore(config2) {
|
|
|
7463
7557
|
}
|
|
7464
7558
|
}
|
|
7465
7559
|
var SAFE_CRED_NAME = /^[A-Za-z0-9._-]+$/;
|
|
7560
|
+
function storeFrom(read) {
|
|
7561
|
+
return {
|
|
7562
|
+
resolve: (ref) => read(credName(ref)),
|
|
7563
|
+
has: (ref) => {
|
|
7564
|
+
const name = credName(ref);
|
|
7565
|
+
try {
|
|
7566
|
+
read(name);
|
|
7567
|
+
return true;
|
|
7568
|
+
} catch {
|
|
7569
|
+
return false;
|
|
7570
|
+
}
|
|
7571
|
+
}
|
|
7572
|
+
};
|
|
7573
|
+
}
|
|
7466
7574
|
function assertSafeName(name) {
|
|
7467
7575
|
if (name === "." || name === ".." || !SAFE_CRED_NAME.test(name)) {
|
|
7468
7576
|
throw new Error(`secrets: unsafe credential name "${name}" (allowed: letters, digits, ".", "_", "-")`);
|
|
@@ -7474,9 +7582,9 @@ function credName(ref) {
|
|
|
7474
7582
|
return assertSafeName(ref.slice(CRED_PREFIX.length));
|
|
7475
7583
|
}
|
|
7476
7584
|
function validateRefs(config2, read) {
|
|
7477
|
-
for (const [
|
|
7585
|
+
for (const [owner2, creds] of Object.entries(config2.owners)) {
|
|
7478
7586
|
for (const ref of [creds.read, creds.write]) {
|
|
7479
|
-
if (!ref.startsWith(CRED_PREFIX)) throw new Error(`secrets: owner "${
|
|
7587
|
+
if (!ref.startsWith(CRED_PREFIX)) throw new Error(`secrets: owner "${owner2}" ref must be "cred:<name>", got "${ref}"`);
|
|
7480
7588
|
read(assertSafeName(ref.slice(CRED_PREFIX.length)));
|
|
7481
7589
|
}
|
|
7482
7590
|
}
|
|
@@ -7486,6 +7594,11 @@ function validateRefs(config2, read) {
|
|
|
7486
7594
|
}
|
|
7487
7595
|
read(assertSafeName(config2.app.privateKey.slice(CRED_PREFIX.length)));
|
|
7488
7596
|
}
|
|
7597
|
+
if (config2.npm) {
|
|
7598
|
+
if (!config2.npm.token.startsWith(CRED_PREFIX)) throw new Error(`secrets: config.npm.token must be a "cred:<name>" ref, got "${config2.npm.token}"`);
|
|
7599
|
+
assertSafeName(config2.npm.token.slice(CRED_PREFIX.length));
|
|
7600
|
+
}
|
|
7601
|
+
for (const ref of ghcrCredRefs(config2.ghcr)) credName(ref);
|
|
7489
7602
|
}
|
|
7490
7603
|
function systemdCredsStore(config2) {
|
|
7491
7604
|
const dir = process.env.CREDENTIALS_DIRECTORY;
|
|
@@ -7507,7 +7620,7 @@ function systemdCredsStore(config2) {
|
|
|
7507
7620
|
return val;
|
|
7508
7621
|
};
|
|
7509
7622
|
validateRefs(config2, read);
|
|
7510
|
-
return
|
|
7623
|
+
return storeFrom(read);
|
|
7511
7624
|
}
|
|
7512
7625
|
function fileStore(config2) {
|
|
7513
7626
|
const path = resolveSecretsPath(config2.secrets?.path);
|
|
@@ -7529,7 +7642,7 @@ function fileStore(config2) {
|
|
|
7529
7642
|
return token;
|
|
7530
7643
|
};
|
|
7531
7644
|
validateRefs(config2, read);
|
|
7532
|
-
return
|
|
7645
|
+
return storeFrom(read);
|
|
7533
7646
|
}
|
|
7534
7647
|
|
|
7535
7648
|
// node_modules/zod/v4/core/core.js
|
|
@@ -7644,7 +7757,7 @@ __export(util_exports, {
|
|
|
7644
7757
|
getParsedType: () => getParsedType,
|
|
7645
7758
|
getSizableOrigin: () => getSizableOrigin,
|
|
7646
7759
|
hexToUint8Array: () => hexToUint8Array,
|
|
7647
|
-
isObject: () =>
|
|
7760
|
+
isObject: () => isObject2,
|
|
7648
7761
|
isPlainObject: () => isPlainObject,
|
|
7649
7762
|
issue: () => issue,
|
|
7650
7763
|
joinValues: () => joinValues,
|
|
@@ -7807,7 +7920,7 @@ function slugify(input) {
|
|
|
7807
7920
|
}
|
|
7808
7921
|
var captureStackTrace = "captureStackTrace" in Error ? Error.captureStackTrace : (..._args) => {
|
|
7809
7922
|
};
|
|
7810
|
-
function
|
|
7923
|
+
function isObject2(data) {
|
|
7811
7924
|
return typeof data === "object" && data !== null && !Array.isArray(data);
|
|
7812
7925
|
}
|
|
7813
7926
|
var allowsEval = /* @__PURE__ */ cached(() => {
|
|
@@ -7826,7 +7939,7 @@ var allowsEval = /* @__PURE__ */ cached(() => {
|
|
|
7826
7939
|
}
|
|
7827
7940
|
});
|
|
7828
7941
|
function isPlainObject(o) {
|
|
7829
|
-
if (
|
|
7942
|
+
if (isObject2(o) === false)
|
|
7830
7943
|
return false;
|
|
7831
7944
|
const ctor = o.constructor;
|
|
7832
7945
|
if (ctor === void 0)
|
|
@@ -7834,7 +7947,7 @@ function isPlainObject(o) {
|
|
|
7834
7947
|
if (typeof ctor !== "function")
|
|
7835
7948
|
return true;
|
|
7836
7949
|
const prot = ctor.prototype;
|
|
7837
|
-
if (
|
|
7950
|
+
if (isObject2(prot) === false)
|
|
7838
7951
|
return false;
|
|
7839
7952
|
if (Object.prototype.hasOwnProperty.call(prot, "isPrototypeOf") === false) {
|
|
7840
7953
|
return false;
|
|
@@ -9332,10 +9445,10 @@ function isValidJWT(token, algorithm = null) {
|
|
|
9332
9445
|
const tokensParts = token.split(".");
|
|
9333
9446
|
if (tokensParts.length !== 3)
|
|
9334
9447
|
return false;
|
|
9335
|
-
const [
|
|
9336
|
-
if (!
|
|
9448
|
+
const [header2] = tokensParts;
|
|
9449
|
+
if (!header2)
|
|
9337
9450
|
return false;
|
|
9338
|
-
const parsedHeader = JSON.parse(atob(
|
|
9451
|
+
const parsedHeader = JSON.parse(atob(header2));
|
|
9339
9452
|
if ("typ" in parsedHeader && parsedHeader?.typ !== "JWT")
|
|
9340
9453
|
return false;
|
|
9341
9454
|
if (!parsedHeader.alg)
|
|
@@ -9639,13 +9752,13 @@ var $ZodObject = /* @__PURE__ */ $constructor("$ZodObject", (inst, def) => {
|
|
|
9639
9752
|
}
|
|
9640
9753
|
return propValues;
|
|
9641
9754
|
});
|
|
9642
|
-
const
|
|
9755
|
+
const isObject3 = isObject2;
|
|
9643
9756
|
const catchall = def.catchall;
|
|
9644
9757
|
let value;
|
|
9645
9758
|
inst._zod.parse = (payload, ctx) => {
|
|
9646
9759
|
value ?? (value = _normalized.value);
|
|
9647
9760
|
const input = payload.value;
|
|
9648
|
-
if (!
|
|
9761
|
+
if (!isObject3(input)) {
|
|
9649
9762
|
payload.issues.push({
|
|
9650
9763
|
expected: "object",
|
|
9651
9764
|
code: "invalid_type",
|
|
@@ -9772,7 +9885,7 @@ var $ZodObjectJIT = /* @__PURE__ */ $constructor("$ZodObjectJIT", (inst, def) =>
|
|
|
9772
9885
|
return (payload, ctx) => fn(shape, payload, ctx);
|
|
9773
9886
|
};
|
|
9774
9887
|
let fastpass;
|
|
9775
|
-
const
|
|
9888
|
+
const isObject3 = isObject2;
|
|
9776
9889
|
const jit = !globalConfig.jitless;
|
|
9777
9890
|
const allowsEval2 = allowsEval;
|
|
9778
9891
|
const fastEnabled = jit && allowsEval2.value;
|
|
@@ -9781,7 +9894,7 @@ var $ZodObjectJIT = /* @__PURE__ */ $constructor("$ZodObjectJIT", (inst, def) =>
|
|
|
9781
9894
|
inst._zod.parse = (payload, ctx) => {
|
|
9782
9895
|
value ?? (value = _normalized.value);
|
|
9783
9896
|
const input = payload.value;
|
|
9784
|
-
if (!
|
|
9897
|
+
if (!isObject3(input)) {
|
|
9785
9898
|
payload.issues.push({
|
|
9786
9899
|
expected: "object",
|
|
9787
9900
|
code: "invalid_type",
|
|
@@ -9904,7 +10017,7 @@ var $ZodDiscriminatedUnion = /* @__PURE__ */ $constructor("$ZodDiscriminatedUnio
|
|
|
9904
10017
|
});
|
|
9905
10018
|
inst._zod.parse = (payload, ctx) => {
|
|
9906
10019
|
const input = payload.value;
|
|
9907
|
-
if (!
|
|
10020
|
+
if (!isObject2(input)) {
|
|
9908
10021
|
payload.issues.push({
|
|
9909
10022
|
code: "invalid_type",
|
|
9910
10023
|
expected: "object",
|
|
@@ -16295,15 +16408,15 @@ var Client = class extends Protocol {
|
|
|
16295
16408
|
|
|
16296
16409
|
// node_modules/@modelcontextprotocol/sdk/dist/esm/shared/mediaType.js
|
|
16297
16410
|
var import_content_type = __toESM(require_content_type(), 1);
|
|
16298
|
-
function mediaTypeEssence(
|
|
16299
|
-
if (!
|
|
16411
|
+
function mediaTypeEssence(header2) {
|
|
16412
|
+
if (!header2) {
|
|
16300
16413
|
return void 0;
|
|
16301
16414
|
}
|
|
16302
16415
|
try {
|
|
16303
|
-
return import_content_type.default.parse(
|
|
16416
|
+
return import_content_type.default.parse(header2).type;
|
|
16304
16417
|
} catch {
|
|
16305
|
-
const essence = (
|
|
16306
|
-
if (essence === "" ||
|
|
16418
|
+
const essence = (header2.split(";", 1)[0] ?? "").trim().toLowerCase();
|
|
16419
|
+
if (essence === "" || header2.slice(essence.length).includes(",")) {
|
|
16307
16420
|
return void 0;
|
|
16308
16421
|
}
|
|
16309
16422
|
return essence;
|
|
@@ -18402,18 +18515,18 @@ var NullObject = /* @__PURE__ */ (() => {
|
|
|
18402
18515
|
C.prototype = /* @__PURE__ */ Object.create(null);
|
|
18403
18516
|
return C;
|
|
18404
18517
|
})();
|
|
18405
|
-
function parse4(
|
|
18518
|
+
function parse4(header2, options) {
|
|
18406
18519
|
const stopChar = options?.comma === true ? COMMA : 65536;
|
|
18407
|
-
const len =
|
|
18408
|
-
let index = skipOWS(
|
|
18520
|
+
const len = header2.length;
|
|
18521
|
+
let index = skipOWS(header2, options?.start ?? 0, len);
|
|
18409
18522
|
const valueStart = index;
|
|
18410
|
-
index = skipValue(
|
|
18411
|
-
const valueEnd = trailingOWS(
|
|
18412
|
-
const type =
|
|
18523
|
+
index = skipValue(header2, index, len, stopChar);
|
|
18524
|
+
const valueEnd = trailingOWS(header2, valueStart, index);
|
|
18525
|
+
const type = header2.slice(valueStart, valueEnd).toLowerCase();
|
|
18413
18526
|
if (options?.parameters === false) {
|
|
18414
18527
|
return { type, index, parameters: new NullObject() };
|
|
18415
18528
|
}
|
|
18416
|
-
return parseParameters(
|
|
18529
|
+
return parseParameters(header2, type, index, len, stopChar);
|
|
18417
18530
|
}
|
|
18418
18531
|
var SP = 32;
|
|
18419
18532
|
var HTAB = 9;
|
|
@@ -18422,36 +18535,36 @@ var EQ = 61;
|
|
|
18422
18535
|
var DQUOTE = 34;
|
|
18423
18536
|
var BSLASH = 92;
|
|
18424
18537
|
var COMMA = 44;
|
|
18425
|
-
function parseParameters(
|
|
18538
|
+
function parseParameters(header2, type, index, len, stopChar) {
|
|
18426
18539
|
const parameters = new NullObject();
|
|
18427
18540
|
parameter: while (index < len) {
|
|
18428
|
-
if (
|
|
18541
|
+
if (header2.charCodeAt(index) === stopChar)
|
|
18429
18542
|
break;
|
|
18430
|
-
index = skipOWS(
|
|
18543
|
+
index = skipOWS(header2, index + 1, len);
|
|
18431
18544
|
const keyStart = index;
|
|
18432
18545
|
while (index < len) {
|
|
18433
|
-
const code =
|
|
18546
|
+
const code = header2.charCodeAt(index);
|
|
18434
18547
|
if (code === stopChar)
|
|
18435
18548
|
break parameter;
|
|
18436
18549
|
if (code === SEMI)
|
|
18437
18550
|
continue parameter;
|
|
18438
18551
|
if (code === EQ) {
|
|
18439
|
-
const keyEnd = trailingOWS(
|
|
18440
|
-
const key2 =
|
|
18441
|
-
index = skipOWS(
|
|
18442
|
-
if (index < len &&
|
|
18552
|
+
const keyEnd = trailingOWS(header2, keyStart, index);
|
|
18553
|
+
const key2 = header2.slice(keyStart, keyEnd).toLowerCase();
|
|
18554
|
+
index = skipOWS(header2, index + 1, len);
|
|
18555
|
+
if (index < len && header2.charCodeAt(index) === DQUOTE) {
|
|
18443
18556
|
index++;
|
|
18444
18557
|
let value = "";
|
|
18445
18558
|
while (index < len) {
|
|
18446
|
-
const code2 =
|
|
18559
|
+
const code2 = header2.charCodeAt(index++);
|
|
18447
18560
|
if (code2 === DQUOTE) {
|
|
18448
|
-
index = skipValue(
|
|
18561
|
+
index = skipValue(header2, index, len, stopChar);
|
|
18449
18562
|
if (parameters[key2] === void 0)
|
|
18450
18563
|
parameters[key2] = value;
|
|
18451
18564
|
break;
|
|
18452
18565
|
}
|
|
18453
18566
|
if (code2 === BSLASH && index < len) {
|
|
18454
|
-
value +=
|
|
18567
|
+
value += header2[index++];
|
|
18455
18568
|
continue;
|
|
18456
18569
|
}
|
|
18457
18570
|
value += String.fromCharCode(code2);
|
|
@@ -18459,10 +18572,10 @@ function parseParameters(header, type, index, len, stopChar) {
|
|
|
18459
18572
|
continue parameter;
|
|
18460
18573
|
}
|
|
18461
18574
|
const valueStart = index;
|
|
18462
|
-
index = skipValue(
|
|
18575
|
+
index = skipValue(header2, index, len, stopChar);
|
|
18463
18576
|
if (parameters[key2] === void 0) {
|
|
18464
|
-
const valueEnd = trailingOWS(
|
|
18465
|
-
parameters[key2] =
|
|
18577
|
+
const valueEnd = trailingOWS(header2, valueStart, index);
|
|
18578
|
+
parameters[key2] = header2.slice(valueStart, valueEnd);
|
|
18466
18579
|
}
|
|
18467
18580
|
continue parameter;
|
|
18468
18581
|
}
|
|
@@ -18480,18 +18593,18 @@ function skipValue(str, index, len, stopChar) {
|
|
|
18480
18593
|
}
|
|
18481
18594
|
return index;
|
|
18482
18595
|
}
|
|
18483
|
-
function skipOWS(
|
|
18596
|
+
function skipOWS(header2, index, len) {
|
|
18484
18597
|
while (index < len) {
|
|
18485
|
-
const char =
|
|
18598
|
+
const char = header2.charCodeAt(index);
|
|
18486
18599
|
if (char !== SP && char !== HTAB)
|
|
18487
18600
|
break;
|
|
18488
18601
|
index++;
|
|
18489
18602
|
}
|
|
18490
18603
|
return index;
|
|
18491
18604
|
}
|
|
18492
|
-
function trailingOWS(
|
|
18605
|
+
function trailingOWS(header2, start, end) {
|
|
18493
18606
|
while (end > start) {
|
|
18494
|
-
const char =
|
|
18607
|
+
const char = header2.charCodeAt(end - 1);
|
|
18495
18608
|
if (char !== SP && char !== HTAB)
|
|
18496
18609
|
break;
|
|
18497
18610
|
end--;
|
|
@@ -18519,8 +18632,8 @@ var stringifyIteratively = (rootValue, replacer, spaceParam) => {
|
|
|
18519
18632
|
const isFunctionReplacer = typeof replacer === "function";
|
|
18520
18633
|
const propertyList = Array.isArray(replacer) ? new Set(replacer.map(String)) : null;
|
|
18521
18634
|
const prepareVal = (parent, key2, val) => {
|
|
18522
|
-
const
|
|
18523
|
-
const hasToJSON =
|
|
18635
|
+
const isObject3 = val !== null && typeof val === "object";
|
|
18636
|
+
const hasToJSON = isObject3 && typeof val.toJSON === "function";
|
|
18524
18637
|
if (hasToJSON) {
|
|
18525
18638
|
val = val.toJSON(key2);
|
|
18526
18639
|
}
|
|
@@ -19352,6 +19465,23 @@ function audit(entry) {
|
|
|
19352
19465
|
console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), ...entry }));
|
|
19353
19466
|
}
|
|
19354
19467
|
|
|
19468
|
+
// src/availability.ts
|
|
19469
|
+
var GHCR_TOOLS = /* @__PURE__ */ new Set(["ghcr_push", "ghcr_push_status", "ghcr_tags"]);
|
|
19470
|
+
function unavailableReason(rule, config2, secrets) {
|
|
19471
|
+
if (rule.name === "npm_scaffold") {
|
|
19472
|
+
if (!config2.npm) return "the npm integration is not configured (enable it with the installer's --npm)";
|
|
19473
|
+
if (!secrets.has(config2.npm.token)) return `its credential "${config2.npm.token.replace(/^cred:/, "")}" is not stored (re-run the installer with --npm)`;
|
|
19474
|
+
}
|
|
19475
|
+
if (GHCR_TOOLS.has(rule.name)) {
|
|
19476
|
+
if (!config2.ghcr) return "GHCR pushes are not configured (config.ghcr)";
|
|
19477
|
+
const refs = ghcrCredRefs(config2.ghcr);
|
|
19478
|
+
if (!refs.some((ref) => secrets.has(ref))) {
|
|
19479
|
+
return `no GHCR credential is stored (${refs.map((r) => `"${r.replace(/^cred:/, "")}"`).join(", ")}; re-run the installer to add one)`;
|
|
19480
|
+
}
|
|
19481
|
+
}
|
|
19482
|
+
return void 0;
|
|
19483
|
+
}
|
|
19484
|
+
|
|
19355
19485
|
// src/verbs/review_thread.ts
|
|
19356
19486
|
var RESOLVE_THREAD = `
|
|
19357
19487
|
mutation($threadId: ID!) {
|
|
@@ -19505,14 +19635,14 @@ async function paginateAfter(octokit, budget, route, params, limit) {
|
|
|
19505
19635
|
}
|
|
19506
19636
|
return out.length > limit ? out.slice(0, limit) : out;
|
|
19507
19637
|
}
|
|
19508
|
-
async function listOwnerRepos(octokit, budget,
|
|
19638
|
+
async function listOwnerRepos(octokit, budget, owner2, maxRepos) {
|
|
19509
19639
|
let raw;
|
|
19510
19640
|
try {
|
|
19511
|
-
raw = await paginate(octokit, budget, "GET /orgs/{owner}/repos", { owner, type: "all" }, maxRepos);
|
|
19641
|
+
raw = await paginate(octokit, budget, "GET /orgs/{owner}/repos", { owner: owner2, type: "all" }, maxRepos);
|
|
19512
19642
|
} catch (err) {
|
|
19513
19643
|
if (err instanceof RateLimited || err instanceof CallBudgetExhausted) throw err;
|
|
19514
19644
|
if (err.status !== 404) throw err;
|
|
19515
|
-
raw = await paginate(octokit, budget, "GET /users/{username}/repos", { username:
|
|
19645
|
+
raw = await paginate(octokit, budget, "GET /users/{username}/repos", { username: owner2, type: "all" }, maxRepos);
|
|
19516
19646
|
}
|
|
19517
19647
|
return raw.filter((r) => !r.archived).map((r) => r.name);
|
|
19518
19648
|
}
|
|
@@ -19524,19 +19654,19 @@ var DEFAULT_PER_REPO_LIMIT = 200;
|
|
|
19524
19654
|
var HARD_PER_REPO_LIMIT = 1e3;
|
|
19525
19655
|
var DEFAULT_MAX_API_CALLS = 250;
|
|
19526
19656
|
var HARD_MAX_API_CALLS = 1e3;
|
|
19527
|
-
async function listUnlabeledIssues(octokit, budget,
|
|
19528
|
-
const raw = await paginate(octokit, budget, "GET /repos/{owner}/{repo}/issues", { owner, repo, state }, limit);
|
|
19657
|
+
async function listUnlabeledIssues(octokit, budget, owner2, repo, state, limit) {
|
|
19658
|
+
const raw = await paginate(octokit, budget, "GET /repos/{owner}/{repo}/issues", { owner: owner2, repo, state }, limit);
|
|
19529
19659
|
const issuesOnly = raw.filter((it) => !("pull_request" in it));
|
|
19530
|
-
const unlabeled = issuesOnly.filter((it) => it.labels.length === 0).map((it) => toUnlabeledItem(
|
|
19660
|
+
const unlabeled = issuesOnly.filter((it) => it.labels.length === 0).map((it) => toUnlabeledItem(owner2, repo, "issue", it));
|
|
19531
19661
|
return { scanned: issuesOnly.length, unlabeled };
|
|
19532
19662
|
}
|
|
19533
|
-
async function listUnlabeledPulls(octokit, budget,
|
|
19534
|
-
const raw = await paginate(octokit, budget, "GET /repos/{owner}/{repo}/pulls", { owner, repo, state }, limit);
|
|
19535
|
-
const unlabeled = raw.filter((it) => includeDrafts || !it.draft).filter((it) => it.labels.length === 0).map((it) => toUnlabeledItem(
|
|
19663
|
+
async function listUnlabeledPulls(octokit, budget, owner2, repo, state, limit, includeDrafts) {
|
|
19664
|
+
const raw = await paginate(octokit, budget, "GET /repos/{owner}/{repo}/pulls", { owner: owner2, repo, state }, limit);
|
|
19665
|
+
const unlabeled = raw.filter((it) => includeDrafts || !it.draft).filter((it) => it.labels.length === 0).map((it) => toUnlabeledItem(owner2, repo, "pull", it));
|
|
19536
19666
|
return { scanned: raw.length, unlabeled };
|
|
19537
19667
|
}
|
|
19538
|
-
function toUnlabeledItem(
|
|
19539
|
-
return { repo: `${
|
|
19668
|
+
function toUnlabeledItem(owner2, repo, type, it) {
|
|
19669
|
+
return { repo: `${owner2}/${repo}`, type, number: it.number, title: it.title, url: it.html_url, createdAt: it.created_at };
|
|
19540
19670
|
}
|
|
19541
19671
|
async function reportUnlabeled(octokit, input) {
|
|
19542
19672
|
const target = input.target ?? "both";
|
|
@@ -19605,11 +19735,11 @@ async function getOrUndefined(octokit, budget, route, params) {
|
|
|
19605
19735
|
throw err;
|
|
19606
19736
|
}
|
|
19607
19737
|
}
|
|
19608
|
-
async function classifyRepo(octokit, budget,
|
|
19609
|
-
const info = await getOrUndefined(octokit, budget, "GET /repos/{owner}/{repo}", { owner, repo });
|
|
19738
|
+
async function classifyRepo(octokit, budget, owner2, repo) {
|
|
19739
|
+
const info = await getOrUndefined(octokit, budget, "GET /repos/{owner}/{repo}", { owner: owner2, repo });
|
|
19610
19740
|
const defaultBranch = info?.default_branch;
|
|
19611
19741
|
const workflow = await getOrUndefined(octokit, budget, "GET /repos/{owner}/{repo}/contents/{path}", {
|
|
19612
|
-
owner,
|
|
19742
|
+
owner: owner2,
|
|
19613
19743
|
repo,
|
|
19614
19744
|
path: ".github/workflows/next-release.yml",
|
|
19615
19745
|
...defaultBranch ? { ref: defaultBranch } : {}
|
|
@@ -19617,7 +19747,7 @@ async function classifyRepo(octokit, budget, owner, repo) {
|
|
|
19617
19747
|
const v4Workflow = workflow !== void 0;
|
|
19618
19748
|
if (!v4Workflow) return { tier: "non-v4", signals: { v4Workflow, ...defaultBranch ? { defaultBranch } : {} } };
|
|
19619
19749
|
const sha = async (branch) => {
|
|
19620
|
-
const b = await getOrUndefined(octokit, budget, "GET /repos/{owner}/{repo}/branches/{branch}", { owner, repo, branch });
|
|
19750
|
+
const b = await getOrUndefined(octokit, budget, "GET /repos/{owner}/{repo}/branches/{branch}", { owner: owner2, repo, branch });
|
|
19621
19751
|
return b?.commit?.sha;
|
|
19622
19752
|
};
|
|
19623
19753
|
const [defaultSha, nextSha, devSha] = await Promise.all([defaultBranch ? sha(defaultBranch) : void 0, sha("next"), sha("dev")]);
|
|
@@ -19643,10 +19773,10 @@ var HARD_ALERT_LIMIT = 5e3;
|
|
|
19643
19773
|
var DEFAULT_MAX_API_CALLS2 = 700;
|
|
19644
19774
|
var HARD_MAX_API_CALLS2 = 3e3;
|
|
19645
19775
|
var SEVERITY_RANK = { critical: 0, high: 1, error: 1, medium: 2, moderate: 2, warning: 3, low: 4, note: 5 };
|
|
19646
|
-
function normalizeDependabot(
|
|
19776
|
+
function normalizeDependabot(owner2, repo, a) {
|
|
19647
19777
|
const vuln = a.security_vulnerability;
|
|
19648
19778
|
const out = {
|
|
19649
|
-
repo: a.repository?.full_name ?? `${
|
|
19779
|
+
repo: a.repository?.full_name ?? `${owner2}/${repo}`,
|
|
19650
19780
|
type: "dependabot",
|
|
19651
19781
|
number: a.number,
|
|
19652
19782
|
severity: vuln?.severity ?? a.security_advisory?.severity ?? "unknown",
|
|
@@ -19664,9 +19794,9 @@ function normalizeDependabot(owner, repo, a) {
|
|
|
19664
19794
|
if (vuln?.first_patched_version?.identifier) out.patchedVersion = vuln.first_patched_version.identifier;
|
|
19665
19795
|
return out;
|
|
19666
19796
|
}
|
|
19667
|
-
function normalizeCodeql(
|
|
19797
|
+
function normalizeCodeql(owner2, repo, a) {
|
|
19668
19798
|
const out = {
|
|
19669
|
-
repo: a.repository?.full_name ?? `${
|
|
19799
|
+
repo: a.repository?.full_name ?? `${owner2}/${repo}`,
|
|
19670
19800
|
type: "codeql",
|
|
19671
19801
|
number: a.number,
|
|
19672
19802
|
severity: a.rule?.security_severity_level ?? a.rule?.severity ?? "unknown",
|
|
@@ -19680,9 +19810,9 @@ function normalizeCodeql(owner, repo, a) {
|
|
|
19680
19810
|
if (loc?.path) out.location = loc.start_line !== void 0 ? `${loc.path}:${loc.start_line}` : loc.path;
|
|
19681
19811
|
return out;
|
|
19682
19812
|
}
|
|
19683
|
-
function normalizeSecret(
|
|
19813
|
+
function normalizeSecret(owner2, repo, a) {
|
|
19684
19814
|
const out = {
|
|
19685
|
-
repo: a.repository?.full_name ?? `${
|
|
19815
|
+
repo: a.repository?.full_name ?? `${owner2}/${repo}`,
|
|
19686
19816
|
type: "secrets",
|
|
19687
19817
|
number: a.number,
|
|
19688
19818
|
severity: "open",
|
|
@@ -19737,9 +19867,9 @@ async function reportSecurityAlerts(octokit, input) {
|
|
|
19737
19867
|
const disabled = {};
|
|
19738
19868
|
let truncatedReason;
|
|
19739
19869
|
let retryAfterSeconds;
|
|
19740
|
-
const addAll = (type,
|
|
19870
|
+
const addAll = (type, owner2, repo, raws) => {
|
|
19741
19871
|
const spec = SPECS[type];
|
|
19742
|
-
for (const raw of raws) alerts.push(spec.normalize(
|
|
19872
|
+
for (const raw of raws) alerts.push(spec.normalize(owner2, repo, raw));
|
|
19743
19873
|
};
|
|
19744
19874
|
try {
|
|
19745
19875
|
let sweep = [];
|
|
@@ -19827,8 +19957,8 @@ async function reportSecurityAlerts(octokit, input) {
|
|
|
19827
19957
|
if (input.classify) {
|
|
19828
19958
|
try {
|
|
19829
19959
|
for (const row of byRepo) {
|
|
19830
|
-
const [
|
|
19831
|
-
const t = await classifyRepo(octokit, budget,
|
|
19960
|
+
const [owner2, repo] = row.repo.includes("/") ? row.repo.split("/") : [input.owner, row.repo];
|
|
19961
|
+
const t = await classifyRepo(octokit, budget, owner2, repo);
|
|
19832
19962
|
row.tier = t.tier;
|
|
19833
19963
|
row.tierSignals = t.signals;
|
|
19834
19964
|
}
|
|
@@ -19960,9 +20090,9 @@ function decodeContent(data) {
|
|
|
19960
20090
|
if (!d || typeof d.content !== "string") return void 0;
|
|
19961
20091
|
return d.encoding === "base64" ? Buffer.from(d.content, "base64").toString("utf8") : d.content;
|
|
19962
20092
|
}
|
|
19963
|
-
async function getText(octokit, budget,
|
|
20093
|
+
async function getText(octokit, budget, owner2, repo, path, ref) {
|
|
19964
20094
|
try {
|
|
19965
|
-
return decodeContent(await reqOne(octokit, budget, "GET /repos/{owner}/{repo}/contents/{path}", { owner, repo, path, ref }));
|
|
20095
|
+
return decodeContent(await reqOne(octokit, budget, "GET /repos/{owner}/{repo}/contents/{path}", { owner: owner2, repo, path, ref }));
|
|
19966
20096
|
} catch (err) {
|
|
19967
20097
|
if (err instanceof RateLimited || err instanceof CallBudgetExhausted) throw err;
|
|
19968
20098
|
if (err.status === 404) return void 0;
|
|
@@ -19970,14 +20100,14 @@ async function getText(octokit, budget, owner, repo, path, ref) {
|
|
|
19970
20100
|
}
|
|
19971
20101
|
}
|
|
19972
20102
|
async function analyzeRelease(octokit, budget, args) {
|
|
19973
|
-
const { owner, repo } = args;
|
|
19974
|
-
const open = await paginate(octokit, budget, "GET /repos/{owner}/{repo}/pulls", { owner, repo, state: "open", base: args.head }, 200);
|
|
20103
|
+
const { owner: owner2, repo } = args;
|
|
20104
|
+
const open = await paginate(octokit, budget, "GET /repos/{owner}/{repo}/pulls", { owner: owner2, repo, state: "open", base: args.head }, 200);
|
|
19975
20105
|
const openPrs = open.map(classifyOpenPr);
|
|
19976
20106
|
let freshness;
|
|
19977
20107
|
if (args.changelogPath && args.version) {
|
|
19978
|
-
const commits = await paginate(octokit, budget, "GET /repos/{owner}/{repo}/pulls/{pull_number}/commits", { owner, repo, pull_number: args.pull }, 300);
|
|
19979
|
-
const changelog = await getText(octokit, budget,
|
|
19980
|
-
const readme = await getText(octokit, budget,
|
|
20108
|
+
const commits = await paginate(octokit, budget, "GET /repos/{owner}/{repo}/pulls/{pull_number}/commits", { owner: owner2, repo, pull_number: args.pull }, 300);
|
|
20109
|
+
const changelog = await getText(octokit, budget, owner2, repo, args.changelogPath, args.head) ?? "";
|
|
20110
|
+
const readme = await getText(octokit, budget, owner2, repo, "README.md", args.head);
|
|
19981
20111
|
freshness = assessFreshness(args.version, changelog, releaseCommitPrs(commits.map((c) => c.commit?.message ?? "")), readme);
|
|
19982
20112
|
}
|
|
19983
20113
|
const decision = decideVerdict({ hasChangelog: Boolean(args.changelogPath), unknownVersion: args.version === null, freshness, openPrs, version: args.version });
|
|
@@ -20006,10 +20136,10 @@ function repoFromUrl(repositoryUrl) {
|
|
|
20006
20136
|
const parts = repositoryUrl.split("/");
|
|
20007
20137
|
return { owner: parts[parts.length - 2], repo: parts[parts.length - 1] };
|
|
20008
20138
|
}
|
|
20009
|
-
async function findChangelog(octokit, budget,
|
|
20139
|
+
async function findChangelog(octokit, budget, owner2, repo, ref, paths) {
|
|
20010
20140
|
for (const path of paths) {
|
|
20011
20141
|
try {
|
|
20012
|
-
await reqOne(octokit, budget, "GET /repos/{owner}/{repo}/contents/{path}", { owner, repo, path, ref });
|
|
20142
|
+
await reqOne(octokit, budget, "GET /repos/{owner}/{repo}/contents/{path}", { owner: owner2, repo, path, ref });
|
|
20013
20143
|
return path;
|
|
20014
20144
|
} catch (err) {
|
|
20015
20145
|
if (err instanceof RateLimited || err instanceof CallBudgetExhausted) throw err;
|
|
@@ -20035,11 +20165,11 @@ async function reportReleasePrs(octokit, input) {
|
|
|
20035
20165
|
if (search.total_count > search.items.length) truncatedReason = "max-results";
|
|
20036
20166
|
else if (search.incomplete_results) truncatedReason = "incomplete-search";
|
|
20037
20167
|
for (const hit of search.items) {
|
|
20038
|
-
const { owner, repo } = repoFromUrl(hit.repository_url);
|
|
20039
|
-
const pull = await reqOne(octokit, budget, "GET /repos/{owner}/{repo}/pulls/{pull_number}", { owner, repo, pull_number: hit.number });
|
|
20168
|
+
const { owner: owner2, repo } = repoFromUrl(hit.repository_url);
|
|
20169
|
+
const pull = await reqOne(octokit, budget, "GET /repos/{owner}/{repo}/pulls/{pull_number}", { owner: owner2, repo, pull_number: hit.number });
|
|
20040
20170
|
const version2 = parseReleaseVersion(hit.title);
|
|
20041
20171
|
const base = {
|
|
20042
|
-
repo: `${
|
|
20172
|
+
repo: `${owner2}/${repo}`,
|
|
20043
20173
|
pull: hit.number,
|
|
20044
20174
|
url: hit.html_url,
|
|
20045
20175
|
title: hit.title,
|
|
@@ -20053,12 +20183,12 @@ async function reportReleasePrs(octokit, input) {
|
|
|
20053
20183
|
entry = { ...base, status: "unknown-version" };
|
|
20054
20184
|
} else {
|
|
20055
20185
|
const paths = changelogPaths(version2);
|
|
20056
|
-
const found = await findChangelog(octokit, budget,
|
|
20186
|
+
const found = await findChangelog(octokit, budget, owner2, repo, pull.head.ref, paths);
|
|
20057
20187
|
entry = found ? { ...base, status: "ready", changelogPath: found } : { ...base, status: "needs-changelog", checkedPaths: paths };
|
|
20058
20188
|
}
|
|
20059
20189
|
releasePrs.push(entry);
|
|
20060
20190
|
if (input.analyze) {
|
|
20061
|
-
entry.analysis = await analyzeRelease(octokit, budget, { owner, repo, pull: hit.number, head: pull.head.ref, version: version2, ...entry.changelogPath ? { changelogPath: entry.changelogPath } : {} });
|
|
20191
|
+
entry.analysis = await analyzeRelease(octokit, budget, { owner: owner2, repo, pull: hit.number, head: pull.head.ref, version: version2, ...entry.changelogPath ? { changelogPath: entry.changelogPath } : {} });
|
|
20062
20192
|
}
|
|
20063
20193
|
}
|
|
20064
20194
|
} catch (err) {
|
|
@@ -20109,18 +20239,18 @@ var DEFAULT_COLOR = "ededed";
|
|
|
20109
20239
|
var MAX_LABELS = 100;
|
|
20110
20240
|
var INPUT_KEYS = /* @__PURE__ */ new Set(["owner", "repo", "issue_number", "add", "remove", "swap", "set", "create_missing", "color"]);
|
|
20111
20241
|
var SWAP_KEYS = /* @__PURE__ */ new Set(["prefix", "to"]);
|
|
20112
|
-
var
|
|
20242
|
+
var fail2 = (msg) => {
|
|
20113
20243
|
throw new Error(`issue_labels: ${msg}`);
|
|
20114
20244
|
};
|
|
20115
20245
|
function toList(v, field) {
|
|
20116
20246
|
if (v === void 0) return [];
|
|
20117
20247
|
const arr = Array.isArray(v) ? v : [v];
|
|
20118
|
-
if (arr.length === 0)
|
|
20248
|
+
if (arr.length === 0) fail2(`"${field}" is empty \u2014 pass at least one label name`);
|
|
20119
20249
|
for (const x of arr) {
|
|
20120
|
-
if (typeof x !== "string" || !x.trim())
|
|
20250
|
+
if (typeof x !== "string" || !x.trim()) fail2(`"${field}" must be a label name or a list of label names`);
|
|
20121
20251
|
}
|
|
20122
20252
|
const list = dedupe(arr.map((x) => x.trim()));
|
|
20123
|
-
if (list.length > MAX_LABELS)
|
|
20253
|
+
if (list.length > MAX_LABELS) fail2(`"${field}" has ${list.length} labels \u2014 at most ${MAX_LABELS} per call`);
|
|
20124
20254
|
return list;
|
|
20125
20255
|
}
|
|
20126
20256
|
var key = (s) => s.toLowerCase();
|
|
@@ -20134,15 +20264,15 @@ function statusOf(err) {
|
|
|
20134
20264
|
var fmt = (list) => `[${list.join(", ")}]`;
|
|
20135
20265
|
var names = (data) => Array.isArray(data) ? data.map((l) => l.name) : [];
|
|
20136
20266
|
function validate(input) {
|
|
20137
|
-
if (typeof input !== "object" || input === null || Array.isArray(input))
|
|
20267
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) fail2("arguments must be an object");
|
|
20138
20268
|
const a = input;
|
|
20139
20269
|
const unknown2 = Object.keys(a).filter((k) => !INPUT_KEYS.has(k));
|
|
20140
|
-
if (unknown2.length)
|
|
20141
|
-
if (typeof a.owner !== "string" || !a.owner.trim())
|
|
20142
|
-
if (typeof a.repo !== "string" || !a.repo.trim())
|
|
20143
|
-
if (typeof a.issue_number !== "number" || !Number.isInteger(a.issue_number) || a.issue_number <= 0)
|
|
20144
|
-
if (a.create_missing !== void 0 && typeof a.create_missing !== "boolean")
|
|
20145
|
-
if (a.color !== void 0 && (typeof a.color !== "string" || !/^#?[0-9a-f]{6}$/i.test(a.color)))
|
|
20270
|
+
if (unknown2.length) fail2(`unknown argument(s): ${unknown2.join(", ")} \u2014 expected ${[...INPUT_KEYS].join(", ")}`);
|
|
20271
|
+
if (typeof a.owner !== "string" || !a.owner.trim()) fail2('"owner" must be a non-empty string');
|
|
20272
|
+
if (typeof a.repo !== "string" || !a.repo.trim()) fail2('"repo" must be a non-empty string');
|
|
20273
|
+
if (typeof a.issue_number !== "number" || !Number.isInteger(a.issue_number) || a.issue_number <= 0) fail2('"issue_number" must be a positive integer');
|
|
20274
|
+
if (a.create_missing !== void 0 && typeof a.create_missing !== "boolean") fail2('"create_missing" must be a boolean');
|
|
20275
|
+
if (a.color !== void 0 && (typeof a.color !== "string" || !/^#?[0-9a-f]{6}$/i.test(a.color))) fail2('"color" must be a 6-digit hex color, e.g. "ededed"');
|
|
20146
20276
|
const add = toList(a.add, "add");
|
|
20147
20277
|
const remove = toList(a.remove, "remove");
|
|
20148
20278
|
const set2 = a.set === void 0 ? void 0 : toList(a.set, "set");
|
|
@@ -20150,23 +20280,23 @@ function validate(input) {
|
|
|
20150
20280
|
let swapPrefix;
|
|
20151
20281
|
if (a.swap !== void 0) {
|
|
20152
20282
|
const sw = a.swap;
|
|
20153
|
-
if (typeof sw !== "object" || sw === null || Array.isArray(sw))
|
|
20283
|
+
if (typeof sw !== "object" || sw === null || Array.isArray(sw)) fail2('"swap" must be an object { prefix, to }');
|
|
20154
20284
|
const extra = Object.keys(sw).filter((k) => !SWAP_KEYS.has(k));
|
|
20155
|
-
if (extra.length)
|
|
20156
|
-
if (typeof sw.prefix !== "string" || !sw.prefix)
|
|
20157
|
-
if (sw.to === void 0)
|
|
20285
|
+
if (extra.length) fail2(`unknown "swap" key(s): ${extra.join(", ")} \u2014 expected prefix, to`);
|
|
20286
|
+
if (typeof sw.prefix !== "string" || !sw.prefix) fail2('"swap.prefix" is required');
|
|
20287
|
+
if (sw.to === void 0) fail2('"swap.to" needs at least one label');
|
|
20158
20288
|
swapPrefix = sw.prefix;
|
|
20159
20289
|
swapTo = toList(sw.to, "swap.to");
|
|
20160
20290
|
}
|
|
20161
20291
|
if (set2 !== void 0 && (add.length || remove.length || swapPrefix !== void 0)) {
|
|
20162
|
-
|
|
20292
|
+
fail2('"set" replaces the whole label set and cannot be combined with add/remove/swap');
|
|
20163
20293
|
}
|
|
20164
|
-
if (set2 === void 0 && !add.length && !remove.length && swapPrefix === void 0)
|
|
20294
|
+
if (set2 === void 0 && !add.length && !remove.length && swapPrefix === void 0) fail2("nothing to do \u2014 pass add, remove, swap or set");
|
|
20165
20295
|
const addSide = dedupe([...add, ...swapTo]);
|
|
20166
|
-
if (addSide.length > MAX_LABELS)
|
|
20296
|
+
if (addSide.length > MAX_LABELS) fail2(`${addSide.length} labels to add \u2014 at most ${MAX_LABELS} per call`);
|
|
20167
20297
|
const removeKeys = new Set(remove.map(key));
|
|
20168
20298
|
const clash = addSide.filter((l) => removeKeys.has(key(l)));
|
|
20169
|
-
if (clash.length)
|
|
20299
|
+
if (clash.length) fail2(`label(s) both added and removed: ${clash.join(", ")}`);
|
|
20170
20300
|
return {
|
|
20171
20301
|
owner: a.owner,
|
|
20172
20302
|
repo: a.repo,
|
|
@@ -20180,9 +20310,9 @@ function validate(input) {
|
|
|
20180
20310
|
};
|
|
20181
20311
|
}
|
|
20182
20312
|
async function issueLabels(octokit, input) {
|
|
20183
|
-
const { owner, repo, issue_number, remove, set: set2, swapPrefix, addSide, createMissing, color } = validate(input);
|
|
20184
|
-
const issue2 = `${
|
|
20185
|
-
const base = { owner, repo, issue_number };
|
|
20313
|
+
const { owner: owner2, repo, issue_number, remove, set: set2, swapPrefix, addSide, createMissing, color } = validate(input);
|
|
20314
|
+
const issue2 = `${owner2}/${repo}#${issue_number}`;
|
|
20315
|
+
const base = { owner: owner2, repo, issue_number };
|
|
20186
20316
|
const readLabels = async () => names((await octokit.request("GET /repos/{owner}/{repo}/issues/{issue_number}/labels", { ...base, per_page: MAX_LABELS })).data);
|
|
20187
20317
|
const before = await readLabels();
|
|
20188
20318
|
const beforeKeys = new Set(before.map(key));
|
|
@@ -20192,7 +20322,7 @@ async function issueLabels(octokit, input) {
|
|
|
20192
20322
|
const missing = [];
|
|
20193
20323
|
for (const name of toResolve) {
|
|
20194
20324
|
try {
|
|
20195
|
-
const res = await octokit.request("GET /repos/{owner}/{repo}/labels/{name}", { owner, repo, name });
|
|
20325
|
+
const res = await octokit.request("GET /repos/{owner}/{repo}/labels/{name}", { owner: owner2, repo, name });
|
|
20196
20326
|
resolved.push(res.data.name);
|
|
20197
20327
|
} catch (err) {
|
|
20198
20328
|
if (statusOf(err) !== 404) throw err;
|
|
@@ -20200,7 +20330,7 @@ async function issueLabels(octokit, input) {
|
|
|
20200
20330
|
}
|
|
20201
20331
|
}
|
|
20202
20332
|
if (missing.length && !createMissing) {
|
|
20203
|
-
|
|
20333
|
+
fail2(`label(s) not found in ${owner2}/${repo}: ${missing.join(", ")} \u2014 nothing was changed. Check the spelling (list_label), or pass create_missing: true to create them.`);
|
|
20204
20334
|
}
|
|
20205
20335
|
const removals = [];
|
|
20206
20336
|
if (set2 === void 0) {
|
|
@@ -20223,7 +20353,7 @@ async function issueLabels(octokit, input) {
|
|
|
20223
20353
|
const canon = (n) => canonical().find((c) => key(c) === key(n)) ?? n;
|
|
20224
20354
|
try {
|
|
20225
20355
|
for (const name of missing) {
|
|
20226
|
-
const res = await octokit.request("POST /repos/{owner}/{repo}/labels", { owner, repo, name, color });
|
|
20356
|
+
const res = await octokit.request("POST /repos/{owner}/{repo}/labels", { owner: owner2, repo, name, color });
|
|
20227
20357
|
created.push(res.data.name);
|
|
20228
20358
|
}
|
|
20229
20359
|
if (set2 !== void 0) {
|
|
@@ -20336,7 +20466,7 @@ function truncationFrom(err) {
|
|
|
20336
20466
|
if (err instanceof CallBudgetExhausted) return { truncated: true, truncatedReason: "max-api-calls" };
|
|
20337
20467
|
return void 0;
|
|
20338
20468
|
}
|
|
20339
|
-
async function computeForPull(octokit, budget,
|
|
20469
|
+
async function computeForPull(octokit, budget, owner2, repo, pullNumber, headSha, verbosity, logLines, fetchFailedJobLogs) {
|
|
20340
20470
|
const summary = { passed: 0, failed: 0, pending: 0 };
|
|
20341
20471
|
const failures = [];
|
|
20342
20472
|
try {
|
|
@@ -20344,7 +20474,7 @@ async function computeForPull(octokit, budget, owner, repo, pullNumber, headSha,
|
|
|
20344
20474
|
octokit,
|
|
20345
20475
|
budget,
|
|
20346
20476
|
"GET /repos/{owner}/{repo}/actions/runs",
|
|
20347
|
-
{ owner, repo, head_sha: headSha },
|
|
20477
|
+
{ owner: owner2, repo, head_sha: headSha },
|
|
20348
20478
|
100,
|
|
20349
20479
|
(d) => d.workflow_runs
|
|
20350
20480
|
);
|
|
@@ -20360,7 +20490,7 @@ async function computeForPull(octokit, budget, owner, repo, pullNumber, headSha,
|
|
|
20360
20490
|
octokit,
|
|
20361
20491
|
budget,
|
|
20362
20492
|
"GET /repos/{owner}/{repo}/actions/runs/{run_id}/jobs",
|
|
20363
|
-
{ owner, repo, run_id: run.id },
|
|
20493
|
+
{ owner: owner2, repo, run_id: run.id },
|
|
20364
20494
|
100,
|
|
20365
20495
|
(d) => d.jobs
|
|
20366
20496
|
);
|
|
@@ -20389,12 +20519,12 @@ async function computeForPull(octokit, budget, owner, repo, pullNumber, headSha,
|
|
|
20389
20519
|
function clampVerbosity(v) {
|
|
20390
20520
|
return Math.min(Math.max(v ?? 0, 0), 3);
|
|
20391
20521
|
}
|
|
20392
|
-
async function ciStatusForHeadSha(octokit,
|
|
20522
|
+
async function ciStatusForHeadSha(octokit, owner2, repo, pullNumber, headSha, opts = {}) {
|
|
20393
20523
|
const budget = new CallBudget(Math.min(opts.maxApiCalls ?? DEFAULT_MAX_API_CALLS4, HARD_MAX_API_CALLS4));
|
|
20394
20524
|
return computeForPull(
|
|
20395
20525
|
octokit,
|
|
20396
20526
|
budget,
|
|
20397
|
-
|
|
20527
|
+
owner2,
|
|
20398
20528
|
repo,
|
|
20399
20529
|
pullNumber,
|
|
20400
20530
|
headSha,
|
|
@@ -20468,13 +20598,1279 @@ var REPORT_CI_STATUS_SCHEMA = {
|
|
|
20468
20598
|
}
|
|
20469
20599
|
};
|
|
20470
20600
|
|
|
20601
|
+
// src/verbs/npm-registry.ts
|
|
20602
|
+
var DEFAULT_REGISTRY = "https://registry.npmjs.org";
|
|
20603
|
+
var NpmOtpRequired = class extends Error {
|
|
20604
|
+
constructor(what) {
|
|
20605
|
+
super(`npm: ${what} needs a one-time password (missing, expired or already used)`);
|
|
20606
|
+
this.name = "NpmOtpRequired";
|
|
20607
|
+
}
|
|
20608
|
+
};
|
|
20609
|
+
var NpmHttpError = class extends Error {
|
|
20610
|
+
status;
|
|
20611
|
+
constructor(what, status, detail) {
|
|
20612
|
+
super(`npm: ${what} failed (HTTP ${status})${detail ? `: ${detail}` : ""}`);
|
|
20613
|
+
this.name = "NpmHttpError";
|
|
20614
|
+
this.status = status;
|
|
20615
|
+
}
|
|
20616
|
+
};
|
|
20617
|
+
function escapePackageName(name) {
|
|
20618
|
+
return name.replace("/", "%2f");
|
|
20619
|
+
}
|
|
20620
|
+
function errorDetail(body) {
|
|
20621
|
+
try {
|
|
20622
|
+
const j = JSON.parse(body);
|
|
20623
|
+
const msg = [j.error, j.message, j.reason].find((v) => typeof v === "string" && v);
|
|
20624
|
+
if (typeof msg === "string") return msg.slice(0, 300);
|
|
20625
|
+
} catch {
|
|
20626
|
+
}
|
|
20627
|
+
return body.trim().slice(0, 300);
|
|
20628
|
+
}
|
|
20629
|
+
var NpmRegistry = class {
|
|
20630
|
+
base;
|
|
20631
|
+
token;
|
|
20632
|
+
fetchImpl;
|
|
20633
|
+
constructor(opts) {
|
|
20634
|
+
this.base = (opts.registry ?? DEFAULT_REGISTRY).replace(/\/+$/, "");
|
|
20635
|
+
this.token = opts.token;
|
|
20636
|
+
this.fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
20637
|
+
}
|
|
20638
|
+
get registry() {
|
|
20639
|
+
return this.base;
|
|
20640
|
+
}
|
|
20641
|
+
async send(what, method, path, opts = {}) {
|
|
20642
|
+
const headers = {
|
|
20643
|
+
authorization: `Bearer ${this.token}`,
|
|
20644
|
+
accept: "application/json",
|
|
20645
|
+
"user-agent": "gh-broker (npm_scaffold)"
|
|
20646
|
+
};
|
|
20647
|
+
if (opts.otp) headers["npm-otp"] = opts.otp;
|
|
20648
|
+
if (opts.body !== void 0) headers["content-type"] = "application/json";
|
|
20649
|
+
const res = await this.fetchImpl(`${this.base}${path}`, {
|
|
20650
|
+
method,
|
|
20651
|
+
headers,
|
|
20652
|
+
...opts.body !== void 0 ? { body: JSON.stringify(opts.body) } : {}
|
|
20653
|
+
});
|
|
20654
|
+
const body = await res.text();
|
|
20655
|
+
if (res.status === 401) {
|
|
20656
|
+
const auth7 = (res.headers.get("www-authenticate") ?? "").toLowerCase();
|
|
20657
|
+
if (auth7.includes("otp") || /one-time pass/i.test(body)) throw new NpmOtpRequired(what);
|
|
20658
|
+
}
|
|
20659
|
+
return { status: res.status, body };
|
|
20660
|
+
}
|
|
20661
|
+
/**
|
|
20662
|
+
* The package's registry document, or undefined when it does not exist (or is not visible to
|
|
20663
|
+
* the token). `?write=true` reads from the primary rather than a cache, as npm itself does
|
|
20664
|
+
* before a write: a stale 404 here is the one thing that could let a placeholder land on a
|
|
20665
|
+
* package that already exists.
|
|
20666
|
+
*/
|
|
20667
|
+
async packument(name) {
|
|
20668
|
+
const res = await this.send("package lookup", "GET", `/${escapePackageName(name)}?write=true`);
|
|
20669
|
+
if (res.status === 404) return void 0;
|
|
20670
|
+
if (res.status !== 200) throw new NpmHttpError("package lookup", res.status, errorDetail(res.body));
|
|
20671
|
+
return JSON.parse(res.body);
|
|
20672
|
+
}
|
|
20673
|
+
/** First publish of a package: PUT of the full document with the tarball attached. */
|
|
20674
|
+
async publish(document, name, otp) {
|
|
20675
|
+
const res = await this.send("publish", "PUT", `/${escapePackageName(name)}`, { otp, body: document });
|
|
20676
|
+
if (res.status < 200 || res.status >= 300) throw new NpmHttpError("publish", res.status, errorDetail(res.body));
|
|
20677
|
+
}
|
|
20678
|
+
/**
|
|
20679
|
+
* Adds a GitHub Actions trusted publisher. A 409 means the package already has a trusted
|
|
20680
|
+
* publisher, which may or may not be this one, so it is reported as `conflict` for the
|
|
20681
|
+
* caller to check against listGithubTrust rather than as success.
|
|
20682
|
+
*/
|
|
20683
|
+
async addGithubTrust(name, claims, otp) {
|
|
20684
|
+
const body = [{ type: "github", claims: { repository: claims.repository, workflow_ref: { file: claims.file } }, permissions: ["createPackage"] }];
|
|
20685
|
+
const res = await this.send("trusted publisher setup", "POST", `/-/package/${escapePackageName(name)}/trust`, { otp, body });
|
|
20686
|
+
if (res.status === 409) return { conflict: true };
|
|
20687
|
+
if (res.status < 200 || res.status >= 300) throw new NpmHttpError("trusted publisher setup", res.status, errorDetail(res.body));
|
|
20688
|
+
let parsed;
|
|
20689
|
+
try {
|
|
20690
|
+
parsed = JSON.parse(res.body);
|
|
20691
|
+
} catch {
|
|
20692
|
+
return { conflict: false };
|
|
20693
|
+
}
|
|
20694
|
+
const first = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
20695
|
+
return { config: first && typeof first === "object" ? first : void 0, conflict: false };
|
|
20696
|
+
}
|
|
20697
|
+
/** The package's trusted-publisher configurations (`npm trust list`). May need an OTP. */
|
|
20698
|
+
async listTrust(name, otp) {
|
|
20699
|
+
const res = await this.send("trusted publisher lookup", "GET", `/-/package/${escapePackageName(name)}/trust`, otp ? { otp } : {});
|
|
20700
|
+
if (res.status !== 200) throw new NpmHttpError("trusted publisher lookup", res.status, errorDetail(res.body));
|
|
20701
|
+
let parsed;
|
|
20702
|
+
try {
|
|
20703
|
+
parsed = JSON.parse(res.body);
|
|
20704
|
+
} catch {
|
|
20705
|
+
throw new NpmHttpError("trusted publisher lookup", res.status, "response is not JSON");
|
|
20706
|
+
}
|
|
20707
|
+
return (Array.isArray(parsed) ? parsed : [parsed]).filter((c) => Boolean(c) && typeof c === "object");
|
|
20708
|
+
}
|
|
20709
|
+
};
|
|
20710
|
+
|
|
20711
|
+
// src/verbs/npm-tarball.ts
|
|
20712
|
+
import { createHash } from "node:crypto";
|
|
20713
|
+
import { gzipSync } from "node:zlib";
|
|
20714
|
+
var MTIME = 499162500;
|
|
20715
|
+
var BLOCK = 512;
|
|
20716
|
+
function header(name, size) {
|
|
20717
|
+
const h = Buffer.alloc(BLOCK, 0);
|
|
20718
|
+
const put = (value, offset, length) => {
|
|
20719
|
+
h.write(value, offset, length, "ascii");
|
|
20720
|
+
};
|
|
20721
|
+
const octal = (n, length) => n.toString(8).padStart(length - 1, "0") + "\0";
|
|
20722
|
+
put(name, 0, 100);
|
|
20723
|
+
put(octal(420, 8), 100, 8);
|
|
20724
|
+
put(octal(0, 8), 108, 8);
|
|
20725
|
+
put(octal(0, 8), 116, 8);
|
|
20726
|
+
put(octal(size, 12), 124, 12);
|
|
20727
|
+
put(octal(MTIME, 12), 136, 12);
|
|
20728
|
+
put(" ", 148, 8);
|
|
20729
|
+
put("0", 156, 1);
|
|
20730
|
+
put("ustar\0", 257, 6);
|
|
20731
|
+
put("00", 263, 2);
|
|
20732
|
+
let sum = 0;
|
|
20733
|
+
for (const byte of h) sum += byte;
|
|
20734
|
+
put(sum.toString(8).padStart(6, "0") + "\0 ", 148, 8);
|
|
20735
|
+
return h;
|
|
20736
|
+
}
|
|
20737
|
+
function buildTarball(files) {
|
|
20738
|
+
const parts = [];
|
|
20739
|
+
for (const f of files) {
|
|
20740
|
+
const name = `package/${f.path}`;
|
|
20741
|
+
if (!/^[\x20-\x7e]+$/.test(name) || name.length > 100) throw new Error(`npm-tarball: unsupported path "${f.path}"`);
|
|
20742
|
+
const body = Buffer.from(f.content, "utf8");
|
|
20743
|
+
parts.push(header(name, body.length), body, Buffer.alloc((BLOCK - body.length % BLOCK) % BLOCK, 0));
|
|
20744
|
+
}
|
|
20745
|
+
parts.push(Buffer.alloc(BLOCK * 2, 0));
|
|
20746
|
+
return gzipSync(Buffer.concat(parts), { level: 9 });
|
|
20747
|
+
}
|
|
20748
|
+
function tarballDigests(tarball) {
|
|
20749
|
+
return {
|
|
20750
|
+
integrity: `sha512-${createHash("sha512").update(tarball).digest("base64")}`,
|
|
20751
|
+
shasum: createHash("sha1").update(tarball).digest("hex")
|
|
20752
|
+
};
|
|
20753
|
+
}
|
|
20754
|
+
|
|
20755
|
+
// src/verbs/npm_scaffold.ts
|
|
20756
|
+
var PLACEHOLDER_VERSION = "0.0.0-stage";
|
|
20757
|
+
var SCAFFOLD_MARKER = "gh-broker";
|
|
20758
|
+
var DEFAULT_WORKFLOW = "publish.yml";
|
|
20759
|
+
var INPUT_KEYS2 = /* @__PURE__ */ new Set(["owner", "repo", "package", "otp", "workflow"]);
|
|
20760
|
+
var PACKAGE_NAME = /^@[a-z0-9][a-z0-9._~-]*\/[a-z0-9][a-z0-9._~-]*$/;
|
|
20761
|
+
var NPM_SCAFFOLD_SCHEMA = {
|
|
20762
|
+
type: "object",
|
|
20763
|
+
properties: {
|
|
20764
|
+
owner: { type: "string", description: "Repository owner (a configured owner)." },
|
|
20765
|
+
repo: { type: "string", description: "Repository whose package.json names the package." },
|
|
20766
|
+
package: { type: "string", description: 'Scoped package name, e.g. "@cldmv/new-thing". Must be under a scope in config.npm.scopes.' },
|
|
20767
|
+
otp: { type: "string", description: "One-time password the user read from their authenticator. Ask the user for it right before calling; codes expire in about 30 seconds." },
|
|
20768
|
+
workflow: { type: "string", description: `The repo's publish workflow file in .github/workflows. Default "publish.yml".` }
|
|
20769
|
+
},
|
|
20770
|
+
required: ["owner", "repo", "package", "otp"],
|
|
20771
|
+
additionalProperties: false
|
|
20772
|
+
};
|
|
20773
|
+
var fail3 = (msg) => {
|
|
20774
|
+
throw new Error(`npm_scaffold: ${msg}`);
|
|
20775
|
+
};
|
|
20776
|
+
function validate2(input) {
|
|
20777
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) fail3("arguments must be an object");
|
|
20778
|
+
const a = input;
|
|
20779
|
+
const unknown2 = Object.keys(a).filter((k) => !INPUT_KEYS2.has(k));
|
|
20780
|
+
if (unknown2.length) fail3(`unknown argument(s): ${unknown2.join(", ")} \u2014 expected ${[...INPUT_KEYS2].join(", ")}`);
|
|
20781
|
+
if (typeof a.owner !== "string" || !/^[A-Za-z0-9-]+$/.test(a.owner)) fail3('"owner" must be a GitHub owner name');
|
|
20782
|
+
if (typeof a.repo !== "string" || !/^[A-Za-z0-9._-]+$/.test(a.repo)) fail3('"repo" must be a repository name');
|
|
20783
|
+
if (typeof a.package !== "string" || a.package.length > 214 || !PACKAGE_NAME.test(a.package)) fail3('"package" must be a lowercase scoped npm package name, e.g. "@cldmv/new-thing"');
|
|
20784
|
+
if (typeof a.otp !== "string" || !/^[0-9]{6,8}$/.test(a.otp)) fail3('"otp" must be the 6-8 digit code from the authenticator');
|
|
20785
|
+
if (a.workflow !== void 0 && (typeof a.workflow !== "string" || !/^[A-Za-z0-9._-]+\.ya?ml$/.test(a.workflow))) {
|
|
20786
|
+
fail3('"workflow" must be a workflow file name in .github/workflows, e.g. "publish.yml"');
|
|
20787
|
+
}
|
|
20788
|
+
return {
|
|
20789
|
+
owner: a.owner,
|
|
20790
|
+
repo: a.repo,
|
|
20791
|
+
package: a.package,
|
|
20792
|
+
otp: a.otp,
|
|
20793
|
+
workflow: a.workflow ?? DEFAULT_WORKFLOW
|
|
20794
|
+
};
|
|
20795
|
+
}
|
|
20796
|
+
function statusOf2(err) {
|
|
20797
|
+
return err?.status;
|
|
20798
|
+
}
|
|
20799
|
+
async function repoFile(octokit, owner2, repo, path, ref) {
|
|
20800
|
+
try {
|
|
20801
|
+
const res = await octokit.request("GET /repos/{owner}/{repo}/contents/{path}", { owner: owner2, repo, path, ref });
|
|
20802
|
+
const d = res.data;
|
|
20803
|
+
if (d.type !== "file" || typeof d.content !== "string") return void 0;
|
|
20804
|
+
return d.encoding === "base64" ? Buffer.from(d.content, "base64").toString("utf8") : d.content;
|
|
20805
|
+
} catch (err) {
|
|
20806
|
+
if (statusOf2(err) === 404) return void 0;
|
|
20807
|
+
throw err;
|
|
20808
|
+
}
|
|
20809
|
+
}
|
|
20810
|
+
async function checkRepo(octokit, args) {
|
|
20811
|
+
let info;
|
|
20812
|
+
try {
|
|
20813
|
+
info = (await octokit.request("GET /repos/{owner}/{repo}", { owner: args.owner, repo: args.repo })).data;
|
|
20814
|
+
} catch (err) {
|
|
20815
|
+
if (statusOf2(err) === 404) fail3(`repository ${args.owner}/${args.repo} not found`);
|
|
20816
|
+
throw err;
|
|
20817
|
+
}
|
|
20818
|
+
const asked = `${args.owner}/${args.repo}`;
|
|
20819
|
+
const repository = info.full_name ?? asked;
|
|
20820
|
+
if (repository.toLowerCase() !== asked.toLowerCase()) fail3(`${asked} redirects to ${repository} (renamed or transferred) \u2014 call with the repository's current owner and name`);
|
|
20821
|
+
if (info.archived) fail3(`repository ${repository} is archived`);
|
|
20822
|
+
const ref = info.default_branch ?? "HEAD";
|
|
20823
|
+
const raw = await repoFile(octokit, args.owner, args.repo, "package.json", ref);
|
|
20824
|
+
if (raw === void 0) fail3(`${repository} has no package.json on ${ref}`);
|
|
20825
|
+
let pkg;
|
|
20826
|
+
try {
|
|
20827
|
+
pkg = JSON.parse(raw);
|
|
20828
|
+
} catch {
|
|
20829
|
+
return fail3(`${repository}'s package.json on ${ref} is not valid JSON`);
|
|
20830
|
+
}
|
|
20831
|
+
if (pkg.name !== args.package) fail3(`${repository}'s package.json on ${ref} names ${JSON.stringify(pkg.name)}, not "${args.package}"`);
|
|
20832
|
+
if (pkg.private === true) fail3(`${repository}'s package.json is marked "private": true, so it is not meant to be published`);
|
|
20833
|
+
if (await repoFile(octokit, args.owner, args.repo, `.github/workflows/${args.workflow}`, ref) === void 0) {
|
|
20834
|
+
fail3(`${repository} has no .github/workflows/${args.workflow} on ${ref} \u2014 the trusted publisher must name the workflow that publishes`);
|
|
20835
|
+
}
|
|
20836
|
+
return {
|
|
20837
|
+
repository,
|
|
20838
|
+
isPrivate: info.private !== false,
|
|
20839
|
+
...typeof pkg.license === "string" ? { license: pkg.license } : {},
|
|
20840
|
+
...typeof pkg.description === "string" ? { description: pkg.description } : {}
|
|
20841
|
+
};
|
|
20842
|
+
}
|
|
20843
|
+
function placeholderDocument(name, facts, access, registry2) {
|
|
20844
|
+
const repoUrl = `https://github.com/${facts.repository}`;
|
|
20845
|
+
const manifest = {
|
|
20846
|
+
name,
|
|
20847
|
+
version: PLACEHOLDER_VERSION,
|
|
20848
|
+
description: `Placeholder for ${name}. Releases are published from ${repoUrl} by CI.`,
|
|
20849
|
+
license: facts.license ?? "UNLICENSED",
|
|
20850
|
+
repository: { type: "git", url: `git+${repoUrl}.git` },
|
|
20851
|
+
homepage: `${repoUrl}#readme`,
|
|
20852
|
+
scaffoldedBy: SCAFFOLD_MARKER
|
|
20853
|
+
};
|
|
20854
|
+
const readme = `# ${name}
|
|
20855
|
+
|
|
20856
|
+
This version is a placeholder that reserves the package name so the repository's release workflow can publish it with npm trusted publishing.
|
|
20857
|
+
|
|
20858
|
+
${facts.description ? `${facts.description}
|
|
20859
|
+
|
|
20860
|
+
` : ""}Source and real releases: ${repoUrl}
|
|
20861
|
+
`;
|
|
20862
|
+
const tarball = buildTarball([
|
|
20863
|
+
{ path: "package.json", content: JSON.stringify(manifest, null, " ") + "\n" },
|
|
20864
|
+
{ path: "README.md", content: readme }
|
|
20865
|
+
]);
|
|
20866
|
+
const { integrity, shasum } = tarballDigests(tarball);
|
|
20867
|
+
const tarballName = `${name}-${PLACEHOLDER_VERSION}.tgz`;
|
|
20868
|
+
const unscoped = name.split("/")[1];
|
|
20869
|
+
return {
|
|
20870
|
+
_id: name,
|
|
20871
|
+
name,
|
|
20872
|
+
description: manifest.description,
|
|
20873
|
+
"dist-tags": { latest: PLACEHOLDER_VERSION },
|
|
20874
|
+
access,
|
|
20875
|
+
versions: {
|
|
20876
|
+
[PLACEHOLDER_VERSION]: {
|
|
20877
|
+
...manifest,
|
|
20878
|
+
readme,
|
|
20879
|
+
_id: `${name}@${PLACEHOLDER_VERSION}`,
|
|
20880
|
+
dist: { integrity, shasum, tarball: `${registry2}/${name}/-/${unscoped}-${PLACEHOLDER_VERSION}.tgz` }
|
|
20881
|
+
}
|
|
20882
|
+
},
|
|
20883
|
+
_attachments: { [tarballName]: { content_type: "application/octet-stream", data: tarball.toString("base64"), length: tarball.length } }
|
|
20884
|
+
};
|
|
20885
|
+
}
|
|
20886
|
+
async function npmScaffold(input, deps) {
|
|
20887
|
+
const args = validate2(input);
|
|
20888
|
+
const scope = args.package.split("/")[0];
|
|
20889
|
+
if (!deps.scopes.includes(scope)) fail3(`scope ${scope} is not in config.npm.scopes (${deps.scopes.join(", ")})`);
|
|
20890
|
+
const facts = await checkRepo(deps.octokit, args);
|
|
20891
|
+
const access = facts.isPrivate ? "restricted" : "public";
|
|
20892
|
+
const base = {
|
|
20893
|
+
package: args.package,
|
|
20894
|
+
repository: facts.repository,
|
|
20895
|
+
workflow: args.workflow,
|
|
20896
|
+
access,
|
|
20897
|
+
version: PLACEHOLDER_VERSION,
|
|
20898
|
+
url: `https://www.npmjs.com/package/${args.package}`
|
|
20899
|
+
};
|
|
20900
|
+
const existing = await deps.registry.packument(args.package);
|
|
20901
|
+
if (!existing) {
|
|
20902
|
+
try {
|
|
20903
|
+
await deps.registry.publish(placeholderDocument(args.package, facts, access, deps.registry.registry), args.package, args.otp);
|
|
20904
|
+
} catch (err) {
|
|
20905
|
+
if (err instanceof NpmOtpRequired) {
|
|
20906
|
+
return { ...base, status: "needs_otp", done: [], remaining: ["publish", "trust"], message: "npm refused the one-time password before publishing; nothing was written. Call again with a fresh code." };
|
|
20907
|
+
}
|
|
20908
|
+
throw err;
|
|
20909
|
+
}
|
|
20910
|
+
return {
|
|
20911
|
+
...base,
|
|
20912
|
+
status: "needs_otp",
|
|
20913
|
+
done: ["publish"],
|
|
20914
|
+
remaining: ["trust"],
|
|
20915
|
+
message: "The placeholder is published. Each npm write needs its own one-time password: call again with the next code (wait for the authenticator to change) to add the trusted publisher."
|
|
20916
|
+
};
|
|
20917
|
+
}
|
|
20918
|
+
const versions = Object.keys(existing.versions ?? {});
|
|
20919
|
+
const placeholder = existing.versions?.[PLACEHOLDER_VERSION];
|
|
20920
|
+
const ours = versions.length === 1 && versions[0] === PLACEHOLDER_VERSION && placeholder?.scaffoldedBy === SCAFFOLD_MARKER;
|
|
20921
|
+
if (!ours) fail3(`${args.package} already exists on the registry (${versions.length} version(s)) and was not created by npm_scaffold \u2014 set up its trusted publisher on npmjs.com instead`);
|
|
20922
|
+
const madeFor = placeholder?.repository?.url;
|
|
20923
|
+
if (typeof madeFor !== "string" || madeFor.toLowerCase() !== `git+https://github.com/${facts.repository}.git`.toLowerCase()) {
|
|
20924
|
+
fail3(`${args.package}'s placeholder was published for ${typeof madeFor === "string" ? madeFor : "an unrecorded repository"}, not ${facts.repository}`);
|
|
20925
|
+
}
|
|
20926
|
+
let out;
|
|
20927
|
+
try {
|
|
20928
|
+
out = await deps.registry.addGithubTrust(args.package, { repository: facts.repository, file: args.workflow }, args.otp);
|
|
20929
|
+
} catch (err) {
|
|
20930
|
+
if (err instanceof NpmOtpRequired) {
|
|
20931
|
+
return {
|
|
20932
|
+
...base,
|
|
20933
|
+
status: "needs_otp",
|
|
20934
|
+
done: [],
|
|
20935
|
+
remaining: ["trust"],
|
|
20936
|
+
skipped: ["publish"],
|
|
20937
|
+
message: "The placeholder is published, but npm refused the one-time password for the trusted publisher (expired or already used). Call again with a new code to finish."
|
|
20938
|
+
};
|
|
20939
|
+
}
|
|
20940
|
+
throw err;
|
|
20941
|
+
}
|
|
20942
|
+
let alreadyThere = false;
|
|
20943
|
+
if (out.conflict) {
|
|
20944
|
+
const access2 = `https://www.npmjs.com/package/${args.package}/access`;
|
|
20945
|
+
let configs;
|
|
20946
|
+
try {
|
|
20947
|
+
configs = await deps.registry.listTrust(args.package);
|
|
20948
|
+
} catch (err) {
|
|
20949
|
+
if (err instanceof NpmOtpRequired) fail3(`${args.package} already has a trusted publisher, and npm needs another one-time password to show which; check it at ${access2}`);
|
|
20950
|
+
throw err;
|
|
20951
|
+
}
|
|
20952
|
+
const claimsOf = (c) => c.claims ?? {};
|
|
20953
|
+
const matches = configs.find((c) => {
|
|
20954
|
+
const cl = claimsOf(c);
|
|
20955
|
+
return c.type === "github" && typeof cl.repository === "string" && cl.repository.toLowerCase() === facts.repository.toLowerCase() && cl.workflow_ref?.file === args.workflow;
|
|
20956
|
+
});
|
|
20957
|
+
if (!matches) {
|
|
20958
|
+
const existingList = configs.map((c) => `${String(c.type)} ${String(claimsOf(c).repository)} (${String(claimsOf(c).workflow_ref?.file)})`).join("; ") || "unreadable";
|
|
20959
|
+
fail3(`${args.package} already has a different trusted publisher: ${existingList}. Change it at ${access2}`);
|
|
20960
|
+
}
|
|
20961
|
+
alreadyThere = true;
|
|
20962
|
+
out = { config: matches, conflict: true };
|
|
20963
|
+
}
|
|
20964
|
+
const trustId = typeof out.config?.id === "string" ? out.config.id : void 0;
|
|
20965
|
+
return {
|
|
20966
|
+
...base,
|
|
20967
|
+
status: "done",
|
|
20968
|
+
done: alreadyThere ? [] : ["trust"],
|
|
20969
|
+
skipped: alreadyThere ? ["publish", "trust"] : ["publish"],
|
|
20970
|
+
...trustId ? { trustId } : {},
|
|
20971
|
+
message: `${args.package} exists with a trusted publisher for ${facts.repository} (.github/workflows/${args.workflow}); its next release can publish through OIDC.`
|
|
20972
|
+
};
|
|
20973
|
+
}
|
|
20974
|
+
|
|
20975
|
+
// src/verbs/ghcr/archive.ts
|
|
20976
|
+
import { closeSync, constants, fstatSync, openSync, readlinkSync, realpathSync } from "node:fs";
|
|
20977
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
20978
|
+
import { dirname, isAbsolute as isAbsolute2, join as join3, normalize as normalize2 } from "node:path";
|
|
20979
|
+
|
|
20980
|
+
// src/verbs/ghcr/tar.ts
|
|
20981
|
+
import { readSync } from "node:fs";
|
|
20982
|
+
import { posix } from "node:path";
|
|
20983
|
+
var BLOCK2 = 512;
|
|
20984
|
+
var MAX_TAR_MEMBERS = 1e5;
|
|
20985
|
+
var MAX_EXT_HEADER = 1024 * 1024;
|
|
20986
|
+
var MAX_LINK_DEPTH = 16;
|
|
20987
|
+
var TarError = class extends Error {
|
|
20988
|
+
};
|
|
20989
|
+
var fail4 = (msg) => {
|
|
20990
|
+
throw new TarError(`archive: ${msg}`);
|
|
20991
|
+
};
|
|
20992
|
+
function readAt(fd, position, length) {
|
|
20993
|
+
const buf = Buffer.alloc(length);
|
|
20994
|
+
let got = 0;
|
|
20995
|
+
while (got < length) {
|
|
20996
|
+
const n = readSync(fd, buf, got, length - got, position + got);
|
|
20997
|
+
if (n === 0) break;
|
|
20998
|
+
got += n;
|
|
20999
|
+
}
|
|
21000
|
+
if (got !== length) fail4(`truncated at byte ${position + got}`);
|
|
21001
|
+
return buf;
|
|
21002
|
+
}
|
|
21003
|
+
function cstr(buf, start, len) {
|
|
21004
|
+
const slice = buf.subarray(start, start + len);
|
|
21005
|
+
const nul = slice.indexOf(0);
|
|
21006
|
+
return slice.subarray(0, nul === -1 ? len : nul).toString("utf8");
|
|
21007
|
+
}
|
|
21008
|
+
function parseSize(buf) {
|
|
21009
|
+
const field = buf.subarray(124, 136);
|
|
21010
|
+
if (field[0] & 128) {
|
|
21011
|
+
let n = 0;
|
|
21012
|
+
for (let i = 1; i < field.length; i++) n = n * 256 + field[i];
|
|
21013
|
+
if (!Number.isSafeInteger(n)) fail4("member size out of range");
|
|
21014
|
+
return n;
|
|
21015
|
+
}
|
|
21016
|
+
const s = cstr(field, 0, 12).trim();
|
|
21017
|
+
if (!/^[0-7]*$/.test(s)) fail4("bad size field in tar header");
|
|
21018
|
+
return s ? parseInt(s, 8) : 0;
|
|
21019
|
+
}
|
|
21020
|
+
function checksumOk(buf) {
|
|
21021
|
+
const stored = cstr(buf, 148, 8).trim();
|
|
21022
|
+
if (!/^[0-7]+$/.test(stored)) return false;
|
|
21023
|
+
let sum = 0;
|
|
21024
|
+
for (let i = 0; i < BLOCK2; i++) sum += i >= 148 && i < 156 ? 32 : buf[i];
|
|
21025
|
+
return sum === parseInt(stored, 8);
|
|
21026
|
+
}
|
|
21027
|
+
function parsePax(data) {
|
|
21028
|
+
const out = {};
|
|
21029
|
+
let i = 0;
|
|
21030
|
+
while (i < data.length) {
|
|
21031
|
+
const sp = data.indexOf(32, i);
|
|
21032
|
+
if (sp === -1) break;
|
|
21033
|
+
const len = parseInt(data.subarray(i, sp).toString("ascii"), 10);
|
|
21034
|
+
if (!Number.isInteger(len) || len <= 0 || i + len > data.length) fail4("bad PAX header record");
|
|
21035
|
+
const rec = data.subarray(sp + 1, i + len - 1).toString("utf8");
|
|
21036
|
+
const eq = rec.indexOf("=");
|
|
21037
|
+
if (eq > 0) out[rec.slice(0, eq)] = rec.slice(eq + 1);
|
|
21038
|
+
i += len;
|
|
21039
|
+
}
|
|
21040
|
+
return out;
|
|
21041
|
+
}
|
|
21042
|
+
function normName(name) {
|
|
21043
|
+
const n = posix.normalize(`/${name}`).slice(1);
|
|
21044
|
+
return n.replace(/\/+$/, "");
|
|
21045
|
+
}
|
|
21046
|
+
function indexTar(fd, size) {
|
|
21047
|
+
const files = /* @__PURE__ */ new Map();
|
|
21048
|
+
const links = /* @__PURE__ */ new Map();
|
|
21049
|
+
let pos = 0;
|
|
21050
|
+
let members = 0;
|
|
21051
|
+
let pax = {};
|
|
21052
|
+
let longName;
|
|
21053
|
+
let longLink;
|
|
21054
|
+
while (pos + BLOCK2 <= size) {
|
|
21055
|
+
const hdr = readAt(fd, pos, BLOCK2);
|
|
21056
|
+
if (hdr.every((b) => b === 0)) break;
|
|
21057
|
+
if (!checksumOk(hdr)) fail4(`bad tar header checksum at byte ${pos}`);
|
|
21058
|
+
if (++members > MAX_TAR_MEMBERS) fail4(`more than ${MAX_TAR_MEMBERS} members`);
|
|
21059
|
+
const type = String.fromCharCode(hdr[156]);
|
|
21060
|
+
let memberSize = parseSize(hdr);
|
|
21061
|
+
const dataAt = pos + BLOCK2;
|
|
21062
|
+
const next = dataAt + Math.ceil(memberSize / BLOCK2) * BLOCK2;
|
|
21063
|
+
if (type === "x" || type === "g" || type === "L" || type === "K") {
|
|
21064
|
+
if (memberSize > MAX_EXT_HEADER) fail4("extended header too large");
|
|
21065
|
+
const data = readAt(fd, dataAt, memberSize);
|
|
21066
|
+
if (type === "x") pax = parsePax(data);
|
|
21067
|
+
else if (type === "L") longName = cstr(data, 0, data.length);
|
|
21068
|
+
else if (type === "K") longLink = cstr(data, 0, data.length);
|
|
21069
|
+
pos = next;
|
|
21070
|
+
continue;
|
|
21071
|
+
}
|
|
21072
|
+
const prefix = cstr(hdr, 345, 155);
|
|
21073
|
+
const rawName = pax.path ?? longName ?? (prefix ? `${prefix}/${cstr(hdr, 0, 100)}` : cstr(hdr, 0, 100));
|
|
21074
|
+
const linkName = pax.linkpath ?? longLink ?? cstr(hdr, 157, 100);
|
|
21075
|
+
if (pax.size !== void 0) {
|
|
21076
|
+
if (!/^\d+$/.test(pax.size)) fail4("bad PAX size");
|
|
21077
|
+
memberSize = Number(pax.size);
|
|
21078
|
+
}
|
|
21079
|
+
const dataEnd = dataAt + Math.ceil(memberSize / BLOCK2) * BLOCK2;
|
|
21080
|
+
pax = {};
|
|
21081
|
+
longName = void 0;
|
|
21082
|
+
longLink = void 0;
|
|
21083
|
+
const name = normName(rawName);
|
|
21084
|
+
if (type === "0" || type === "\0" || type === "7") {
|
|
21085
|
+
if (dataAt + memberSize > size) fail4(`member "${name}" runs past the end of the file`);
|
|
21086
|
+
files.set(name, { offset: dataAt, size: memberSize });
|
|
21087
|
+
links.delete(name);
|
|
21088
|
+
} else if (type === "1") {
|
|
21089
|
+
links.set(name, { target: normName(linkName), hard: true });
|
|
21090
|
+
files.delete(name);
|
|
21091
|
+
} else if (type === "2") {
|
|
21092
|
+
links.set(name, { target: normName(posix.join(posix.dirname(name), linkName)), hard: false });
|
|
21093
|
+
files.delete(name);
|
|
21094
|
+
}
|
|
21095
|
+
pos = dataEnd;
|
|
21096
|
+
}
|
|
21097
|
+
for (const [name, link] of links) {
|
|
21098
|
+
let target = link.target;
|
|
21099
|
+
for (let depth = 0; ; depth++) {
|
|
21100
|
+
if (depth > MAX_LINK_DEPTH) fail4(`link loop at "${name}"`);
|
|
21101
|
+
const hit = files.get(target);
|
|
21102
|
+
if (hit) {
|
|
21103
|
+
files.set(name, hit);
|
|
21104
|
+
break;
|
|
21105
|
+
}
|
|
21106
|
+
const hop = links.get(target);
|
|
21107
|
+
if (!hop) break;
|
|
21108
|
+
target = hop.target;
|
|
21109
|
+
}
|
|
21110
|
+
}
|
|
21111
|
+
return { files };
|
|
21112
|
+
}
|
|
21113
|
+
function readMember(fd, m, max, what) {
|
|
21114
|
+
if (m.size > max) fail4(`${what} is ${m.size} bytes \u2014 larger than the ${max}-byte limit`);
|
|
21115
|
+
return readAt(fd, m.offset, m.size);
|
|
21116
|
+
}
|
|
21117
|
+
function peekMember(fd, m, n) {
|
|
21118
|
+
return readAt(fd, m.offset, Math.min(n, m.size));
|
|
21119
|
+
}
|
|
21120
|
+
|
|
21121
|
+
// src/verbs/ghcr/archive.ts
|
|
21122
|
+
var MEDIA = {
|
|
21123
|
+
ociIndex: "application/vnd.oci.image.index.v1+json",
|
|
21124
|
+
ociManifest: "application/vnd.oci.image.manifest.v1+json",
|
|
21125
|
+
ociConfig: "application/vnd.oci.image.config.v1+json",
|
|
21126
|
+
ociLayer: "application/vnd.oci.image.layer.v1.tar",
|
|
21127
|
+
ociLayerGzip: "application/vnd.oci.image.layer.v1.tar+gzip",
|
|
21128
|
+
ociLayerZstd: "application/vnd.oci.image.layer.v1.tar+zstd",
|
|
21129
|
+
dockerList: "application/vnd.docker.distribution.manifest.list.v2+json",
|
|
21130
|
+
dockerManifest: "application/vnd.docker.distribution.manifest.v2+json",
|
|
21131
|
+
dockerConfig: "application/vnd.docker.container.image.v1+json",
|
|
21132
|
+
dockerLayer: "application/vnd.docker.image.rootfs.diff.tar",
|
|
21133
|
+
dockerLayerGzip: "application/vnd.docker.image.rootfs.diff.tar.gzip"
|
|
21134
|
+
};
|
|
21135
|
+
var MANIFEST_TYPES = /* @__PURE__ */ new Set([MEDIA.ociManifest, MEDIA.dockerManifest]);
|
|
21136
|
+
var INDEX_TYPES = /* @__PURE__ */ new Set([MEDIA.ociIndex, MEDIA.dockerList]);
|
|
21137
|
+
var LAYER_TYPES = /* @__PURE__ */ new Set([MEDIA.ociLayer, MEDIA.ociLayerGzip, MEDIA.ociLayerZstd, MEDIA.dockerLayer, MEDIA.dockerLayerGzip]);
|
|
21138
|
+
var CONFIG_TYPES = /* @__PURE__ */ new Set([MEDIA.ociConfig, MEDIA.dockerConfig]);
|
|
21139
|
+
var DIGEST = /^sha256:[a-f0-9]{64}$/;
|
|
21140
|
+
var MAX_JSON = 4 * 1024 * 1024;
|
|
21141
|
+
var MAX_LAYERS = 256;
|
|
21142
|
+
var ArchiveError = class extends Error {
|
|
21143
|
+
};
|
|
21144
|
+
var fail5 = (msg) => {
|
|
21145
|
+
throw new ArchiveError(`archive: ${msg}`);
|
|
21146
|
+
};
|
|
21147
|
+
function openArchive(input, dropDir, maxBytes) {
|
|
21148
|
+
if (typeof input !== "string" || !input || input.length > 4096 || input.includes("\0")) fail5('"archive" must be a file path inside the drop directory');
|
|
21149
|
+
const candidate = normalize2(isAbsolute2(input) ? input : join3(dropDir, input));
|
|
21150
|
+
const outside = `"archive" must be an existing file inside the drop directory ${dropDir}`;
|
|
21151
|
+
if (!candidate.startsWith(`${dropDir}/`)) fail5(outside);
|
|
21152
|
+
let realDrop;
|
|
21153
|
+
try {
|
|
21154
|
+
realDrop = realpathSync(dropDir);
|
|
21155
|
+
} catch {
|
|
21156
|
+
return fail5(`drop directory ${dropDir} does not exist \u2014 the installer creates it`);
|
|
21157
|
+
}
|
|
21158
|
+
let realParent;
|
|
21159
|
+
try {
|
|
21160
|
+
realParent = realpathSync(dirname(candidate));
|
|
21161
|
+
} catch {
|
|
21162
|
+
return fail5(outside);
|
|
21163
|
+
}
|
|
21164
|
+
if (realParent !== realDrop && !realParent.startsWith(`${realDrop}/`)) fail5(outside);
|
|
21165
|
+
let fd;
|
|
21166
|
+
try {
|
|
21167
|
+
fd = openSync(candidate, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
21168
|
+
} catch (err) {
|
|
21169
|
+
const code = err.code;
|
|
21170
|
+
if (code === "ELOOP") return fail5(`${candidate} is a symlink \u2014 put the archive file itself in the drop directory`);
|
|
21171
|
+
if (code === "ENOENT" || code === "ENOTDIR") return fail5(`${candidate} not found`);
|
|
21172
|
+
if (code === "EACCES") return fail5(`${candidate} is not readable by the broker \u2014 make it group-readable (chmod g+r)`);
|
|
21173
|
+
return fail5(`cannot open ${candidate} (${code ?? "error"})`);
|
|
21174
|
+
}
|
|
21175
|
+
try {
|
|
21176
|
+
const real = readlinkSync(`/proc/self/fd/${fd}`);
|
|
21177
|
+
if (!real.startsWith(`${realDrop}/`)) fail5(outside);
|
|
21178
|
+
const st = fstatSync(fd);
|
|
21179
|
+
if (!st.isFile()) fail5(`${candidate} is not a regular file`);
|
|
21180
|
+
if (st.nlink !== 1) fail5(`${candidate} has ${st.nlink} hard links \u2014 copy the archive instead of linking it`);
|
|
21181
|
+
if (st.size === 0) fail5(`${candidate} is empty`);
|
|
21182
|
+
if (st.size > maxBytes) fail5(`${candidate} is ${st.size} bytes \u2014 over the ${maxBytes}-byte limit (ghcr.maxArchiveBytes)`);
|
|
21183
|
+
let closed = false;
|
|
21184
|
+
return {
|
|
21185
|
+
fd,
|
|
21186
|
+
path: real,
|
|
21187
|
+
size: st.size,
|
|
21188
|
+
close() {
|
|
21189
|
+
if (closed) return;
|
|
21190
|
+
closed = true;
|
|
21191
|
+
closeSync(fd);
|
|
21192
|
+
}
|
|
21193
|
+
};
|
|
21194
|
+
} catch (err) {
|
|
21195
|
+
closeSync(fd);
|
|
21196
|
+
throw err;
|
|
21197
|
+
}
|
|
21198
|
+
}
|
|
21199
|
+
var sha256 = (b) => `sha256:${createHash2("sha256").update(b).digest("hex")}`;
|
|
21200
|
+
function parseJson(buf, what) {
|
|
21201
|
+
try {
|
|
21202
|
+
return JSON.parse(buf.toString("utf8"));
|
|
21203
|
+
} catch {
|
|
21204
|
+
return fail5(`${what} is not valid JSON`);
|
|
21205
|
+
}
|
|
21206
|
+
}
|
|
21207
|
+
function blobMember(index, digest, what) {
|
|
21208
|
+
if (typeof digest !== "string" || !DIGEST.test(digest)) fail5(`${what} has an unsupported digest (only sha256 is accepted)`);
|
|
21209
|
+
const m = index.files.get(`blobs/sha256/${digest.slice("sha256:".length)}`);
|
|
21210
|
+
if (!m) fail5(`${what} ${digest} is missing from the archive`);
|
|
21211
|
+
return m;
|
|
21212
|
+
}
|
|
21213
|
+
function readBlobJson(fd, index, d, what) {
|
|
21214
|
+
const m = blobMember(index, d.digest, what);
|
|
21215
|
+
const bytes = readMember(fd, m, MAX_JSON, what);
|
|
21216
|
+
if (sha256(bytes) !== d.digest) fail5(`${what} does not match its digest`);
|
|
21217
|
+
return { bytes, json: parseJson(bytes, what) };
|
|
21218
|
+
}
|
|
21219
|
+
function images(manifests, index) {
|
|
21220
|
+
if (!Array.isArray(manifests)) return fail5("index has no manifests list");
|
|
21221
|
+
const list = manifests.filter(
|
|
21222
|
+
(d) => d?.annotations?.["vnd.docker.reference.type"] !== "attestation-manifest" && !(d?.platform?.os === "unknown" && d?.platform?.architecture === "unknown")
|
|
21223
|
+
);
|
|
21224
|
+
if (list.length <= 1) return list;
|
|
21225
|
+
return list.filter((d) => typeof d?.digest === "string" && DIGEST.test(d.digest) && index.files.has(`blobs/sha256/${d.digest.slice("sha256:".length)}`));
|
|
21226
|
+
}
|
|
21227
|
+
function sniff(fd, m) {
|
|
21228
|
+
const head = peekMember(fd, m, 4);
|
|
21229
|
+
if (head.length >= 2 && head[0] === 31 && head[1] === 139) return "gzip";
|
|
21230
|
+
if (head.length >= 4 && head.readUInt32LE(0) === 4247762216) return "zstd";
|
|
21231
|
+
return "none";
|
|
21232
|
+
}
|
|
21233
|
+
function layerPlan(fd, m, declared, sourceDigest) {
|
|
21234
|
+
if (declared !== void 0 && !LAYER_TYPES.has(declared)) fail5(`layer media type "${declared}" is not supported (non-distributable and foreign layers are refused)`);
|
|
21235
|
+
const kind = sniff(fd, m);
|
|
21236
|
+
if (kind === "gzip") {
|
|
21237
|
+
const mediaType = declared === MEDIA.ociLayerGzip || declared === MEDIA.dockerLayerGzip ? declared : MEDIA.ociLayerGzip;
|
|
21238
|
+
return { member: m, mediaType, sourceDigest, sourceSize: m.size, compress: false };
|
|
21239
|
+
}
|
|
21240
|
+
if (kind === "zstd") return { member: m, mediaType: MEDIA.ociLayerZstd, sourceDigest, sourceSize: m.size, compress: false };
|
|
21241
|
+
return { member: m, mediaType: MEDIA.ociLayerGzip, sourceDigest, sourceSize: m.size, compress: true };
|
|
21242
|
+
}
|
|
21243
|
+
function planImage(fd, size) {
|
|
21244
|
+
const index = indexTar(fd, size);
|
|
21245
|
+
const top = index.files.get("index.json");
|
|
21246
|
+
if (top) return planOciLayout(fd, index, top);
|
|
21247
|
+
const legacy = index.files.get("manifest.json");
|
|
21248
|
+
if (legacy) return planDockerArchive(fd, index, legacy);
|
|
21249
|
+
return fail5("not an image archive (no index.json or manifest.json) \u2014 create it with `docker save <image> -o <file>`");
|
|
21250
|
+
}
|
|
21251
|
+
function planOciLayout(fd, index, top) {
|
|
21252
|
+
const root = parseJson(readMember(fd, top, MAX_JSON, "index.json"), "index.json");
|
|
21253
|
+
let list = images(root.manifests, index);
|
|
21254
|
+
let desc = list.length === 1 ? list[0] : void 0;
|
|
21255
|
+
if (desc && INDEX_TYPES.has(desc.mediaType ?? "")) {
|
|
21256
|
+
const nested = readBlobJson(fd, index, desc, "nested index").json;
|
|
21257
|
+
list = images(nested.manifests, index);
|
|
21258
|
+
desc = list.length === 1 ? list[0] : void 0;
|
|
21259
|
+
}
|
|
21260
|
+
if (!desc) fail5(`archive holds ${list.length} images \u2014 save exactly one single-platform image per archive`);
|
|
21261
|
+
const d = desc;
|
|
21262
|
+
if (!MANIFEST_TYPES.has(d.mediaType ?? "")) fail5(`manifest media type "${d.mediaType}" is not supported`);
|
|
21263
|
+
const { bytes, json } = readBlobJson(fd, index, d, "image manifest");
|
|
21264
|
+
const man = json;
|
|
21265
|
+
if (!man.config || !Array.isArray(man.layers)) fail5("image manifest has no config or layers");
|
|
21266
|
+
if (man.layers.length > MAX_LAYERS) fail5(`image has ${man.layers.length} layers \u2014 at most ${MAX_LAYERS}`);
|
|
21267
|
+
const cfg = man.config;
|
|
21268
|
+
if (!CONFIG_TYPES.has(cfg.mediaType ?? "")) fail5(`config media type "${cfg.mediaType}" is not supported`);
|
|
21269
|
+
const cfgMember = blobMember(index, cfg.digest, "image config");
|
|
21270
|
+
if (cfgMember.size !== cfg.size) fail5("image config size does not match its descriptor");
|
|
21271
|
+
let changed = false;
|
|
21272
|
+
const layers = man.layers.map((l, i) => {
|
|
21273
|
+
const m = blobMember(index, l.digest, `layer ${i + 1}`);
|
|
21274
|
+
if (m.size !== l.size) fail5(`layer ${i + 1} size does not match its descriptor`);
|
|
21275
|
+
const plan = layerPlan(fd, m, l.mediaType, l.digest);
|
|
21276
|
+
if (plan.compress || plan.mediaType !== l.mediaType) changed = true;
|
|
21277
|
+
return plan;
|
|
21278
|
+
});
|
|
21279
|
+
return {
|
|
21280
|
+
format: "oci-layout",
|
|
21281
|
+
config: { member: cfgMember, mediaType: cfg.mediaType, digest: cfg.digest, sourceDigest: cfg.digest, sourceSize: cfgMember.size, compress: false },
|
|
21282
|
+
layers,
|
|
21283
|
+
...changed ? {} : { verbatimManifest: { bytes, mediaType: d.mediaType, digest: d.digest } },
|
|
21284
|
+
...man.annotations ? { annotations: man.annotations } : {}
|
|
21285
|
+
};
|
|
21286
|
+
}
|
|
21287
|
+
function planDockerArchive(fd, index, legacy) {
|
|
21288
|
+
const list = parseJson(readMember(fd, legacy, MAX_JSON, "manifest.json"), "manifest.json");
|
|
21289
|
+
if (!Array.isArray(list) || list.length !== 1) fail5(`archive holds ${Array.isArray(list) ? list.length : 0} images \u2014 save exactly one image per archive`);
|
|
21290
|
+
const entry = list[0];
|
|
21291
|
+
if (typeof entry.Config !== "string" || !Array.isArray(entry.Layers)) fail5("manifest.json entry has no Config or Layers");
|
|
21292
|
+
if (entry.Layers.length > MAX_LAYERS) fail5(`image has ${entry.Layers.length} layers \u2014 at most ${MAX_LAYERS}`);
|
|
21293
|
+
const member = (name, what) => {
|
|
21294
|
+
const m = typeof name === "string" ? index.files.get(name.replace(/^\.?\//, "")) : void 0;
|
|
21295
|
+
if (!m) fail5(`${what} "${String(name)}" is missing from the archive`);
|
|
21296
|
+
return m;
|
|
21297
|
+
};
|
|
21298
|
+
const cfgMember = member(entry.Config, "image config");
|
|
21299
|
+
const cfgBytes = readMember(fd, cfgMember, MAX_JSON, "image config");
|
|
21300
|
+
parseJson(cfgBytes, "image config");
|
|
21301
|
+
const layers = entry.Layers.map((name, i) => layerPlan(fd, member(name, `layer ${i + 1}`), void 0, void 0));
|
|
21302
|
+
return {
|
|
21303
|
+
format: "docker-archive",
|
|
21304
|
+
config: { member: cfgMember, mediaType: MEDIA.ociConfig, digest: sha256(cfgBytes), sourceSize: cfgMember.size, compress: false },
|
|
21305
|
+
layers
|
|
21306
|
+
};
|
|
21307
|
+
}
|
|
21308
|
+
|
|
21309
|
+
// src/verbs/ghcr/registry.ts
|
|
21310
|
+
import { request as httpRequest } from "node:http";
|
|
21311
|
+
import { request as httpsRequest } from "node:https";
|
|
21312
|
+
var GHCR_BASE = "https://ghcr.io";
|
|
21313
|
+
var MAX_BODY = 1024 * 1024;
|
|
21314
|
+
var IDLE_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
21315
|
+
var MAX_TAG_PAGES = 20;
|
|
21316
|
+
var MANIFEST_ACCEPT = [
|
|
21317
|
+
"application/vnd.oci.image.manifest.v1+json",
|
|
21318
|
+
"application/vnd.oci.image.index.v1+json",
|
|
21319
|
+
"application/vnd.docker.distribution.manifest.v2+json",
|
|
21320
|
+
"application/vnd.docker.distribution.manifest.list.v2+json"
|
|
21321
|
+
].join(", ");
|
|
21322
|
+
var RegistryError = class extends Error {
|
|
21323
|
+
status;
|
|
21324
|
+
constructor(message, status) {
|
|
21325
|
+
super(message);
|
|
21326
|
+
this.status = status;
|
|
21327
|
+
}
|
|
21328
|
+
};
|
|
21329
|
+
var Registry = class {
|
|
21330
|
+
base;
|
|
21331
|
+
username;
|
|
21332
|
+
password;
|
|
21333
|
+
repository;
|
|
21334
|
+
token;
|
|
21335
|
+
scope;
|
|
21336
|
+
constructor(opts) {
|
|
21337
|
+
this.base = new URL(opts.base ?? GHCR_BASE);
|
|
21338
|
+
this.username = opts.username;
|
|
21339
|
+
this.password = opts.password;
|
|
21340
|
+
this.repository = opts.repository;
|
|
21341
|
+
}
|
|
21342
|
+
/** The registry host name as used in image references (e.g. "ghcr.io"). */
|
|
21343
|
+
get host() {
|
|
21344
|
+
return this.base.host;
|
|
21345
|
+
}
|
|
21346
|
+
/** Replace every secret (and anything that looks like one) in a message. */
|
|
21347
|
+
clean(msg) {
|
|
21348
|
+
let out = msg;
|
|
21349
|
+
for (const s of [this.password, this.token]) if (s && s.length >= 4) out = out.split(s).join("[redacted]");
|
|
21350
|
+
return out.replace(/\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+/gi, "$1 [redacted]").replace(/\bgh[pousr]_[A-Za-z0-9]{20,}\b/g, "[redacted]").replace(/\bgithub_pat_[A-Za-z0-9_]{20,}\b/g, "[redacted]");
|
|
21351
|
+
}
|
|
21352
|
+
err(step, res) {
|
|
21353
|
+
let detail = "";
|
|
21354
|
+
try {
|
|
21355
|
+
const j = JSON.parse(res.body.toString("utf8"));
|
|
21356
|
+
if (Array.isArray(j.errors)) detail = j.errors.map((e) => `${String(e.code ?? "")}: ${String(e.message ?? "")}`).join("; ");
|
|
21357
|
+
} catch {
|
|
21358
|
+
detail = res.body.toString("utf8").replace(/\s+/g, " ").trim();
|
|
21359
|
+
}
|
|
21360
|
+
const hint = res.status === 401 || res.status === 403 ? " \u2014 check the ghcr credential is a classic PAT with write:packages, not expired, and allowed for this package" : "";
|
|
21361
|
+
const shown = this.clean(detail).slice(0, 300);
|
|
21362
|
+
return new RegistryError(this.clean(`ghcr: ${step} failed: HTTP ${res.status}${shown ? ` (${shown})` : ""}${hint}`), res.status);
|
|
21363
|
+
}
|
|
21364
|
+
/** Resolve a registry-provided URL and refuse anything that would leave the registry's origin. */
|
|
21365
|
+
sameOrigin(raw, step) {
|
|
21366
|
+
let u;
|
|
21367
|
+
try {
|
|
21368
|
+
u = new URL(raw, this.base);
|
|
21369
|
+
} catch {
|
|
21370
|
+
throw new RegistryError(`ghcr: ${step}: registry returned an unparseable URL`);
|
|
21371
|
+
}
|
|
21372
|
+
if (u.origin !== this.base.origin) throw new RegistryError(`ghcr: ${step}: registry pointed at another host (${u.host}) \u2014 refused`);
|
|
21373
|
+
return u;
|
|
21374
|
+
}
|
|
21375
|
+
url(path) {
|
|
21376
|
+
return new URL(path, this.base);
|
|
21377
|
+
}
|
|
21378
|
+
send(method, url2, headers, body, onBytes) {
|
|
21379
|
+
const fn = url2.protocol === "https:" ? httpsRequest : httpRequest;
|
|
21380
|
+
return new Promise((resolve, reject) => {
|
|
21381
|
+
const req2 = fn(url2, { method, headers }, (res) => {
|
|
21382
|
+
const chunks = [];
|
|
21383
|
+
let len = 0;
|
|
21384
|
+
res.on("data", (c) => {
|
|
21385
|
+
if (len < MAX_BODY) chunks.push(c);
|
|
21386
|
+
len += c.length;
|
|
21387
|
+
});
|
|
21388
|
+
res.on("end", () => resolve({ status: res.statusCode ?? 0, headers: res.headers, body: Buffer.concat(chunks).subarray(0, MAX_BODY) }));
|
|
21389
|
+
res.on("error", reject);
|
|
21390
|
+
});
|
|
21391
|
+
req2.setTimeout(IDLE_TIMEOUT_MS, () => req2.destroy(new Error(`${method} timed out`)));
|
|
21392
|
+
req2.on("error", (e) => reject(new RegistryError(this.clean(`ghcr: ${method} ${url2.pathname} failed: ${e.message}`))));
|
|
21393
|
+
if (body === void 0) req2.end();
|
|
21394
|
+
else if (Buffer.isBuffer(body)) req2.end(body);
|
|
21395
|
+
else {
|
|
21396
|
+
const src = body();
|
|
21397
|
+
src.on("error", (e) => req2.destroy(e));
|
|
21398
|
+
if (onBytes) src.on("data", (c) => onBytes(c.length));
|
|
21399
|
+
src.pipe(req2);
|
|
21400
|
+
}
|
|
21401
|
+
});
|
|
21402
|
+
}
|
|
21403
|
+
/** Exchange the PAT for a registry token scoped to this repository. */
|
|
21404
|
+
async login(actions) {
|
|
21405
|
+
const u = this.url("/token");
|
|
21406
|
+
u.searchParams.set("service", this.base.host);
|
|
21407
|
+
u.searchParams.set("scope", `repository:${this.repository}:${actions}`);
|
|
21408
|
+
const basic = Buffer.from(`${this.username}:${this.password}`).toString("base64");
|
|
21409
|
+
const res = await this.send("GET", u, { Authorization: `Basic ${basic}`, Accept: "application/json" }, void 0);
|
|
21410
|
+
if (res.status !== 200) throw this.err("token exchange", res);
|
|
21411
|
+
let token;
|
|
21412
|
+
try {
|
|
21413
|
+
const j = JSON.parse(res.body.toString("utf8"));
|
|
21414
|
+
token = j.token ?? j.access_token;
|
|
21415
|
+
} catch {
|
|
21416
|
+
}
|
|
21417
|
+
if (typeof token !== "string" || !token) throw new RegistryError("ghcr: token exchange returned no token");
|
|
21418
|
+
this.token = token;
|
|
21419
|
+
this.scope = actions;
|
|
21420
|
+
}
|
|
21421
|
+
/** One request with the registry token; on 401 re-login once and retry. */
|
|
21422
|
+
async authed(method, url2, headers, body, onBytes) {
|
|
21423
|
+
if (!this.token) await this.login("pull");
|
|
21424
|
+
const go = () => this.send(method, url2, { ...headers, Authorization: `Bearer ${this.token}` }, body, onBytes);
|
|
21425
|
+
const res = await go();
|
|
21426
|
+
if (res.status !== 401) return res;
|
|
21427
|
+
await this.login(this.scope ?? "pull");
|
|
21428
|
+
return go();
|
|
21429
|
+
}
|
|
21430
|
+
async blobExists(digest) {
|
|
21431
|
+
const res = await this.authed("HEAD", this.url(`/v2/${this.repository}/blobs/${digest}`), {}, void 0);
|
|
21432
|
+
if (res.status >= 200 && res.status < 400) return true;
|
|
21433
|
+
if (res.status === 404) return false;
|
|
21434
|
+
throw this.err(`checking blob ${digest}`, res);
|
|
21435
|
+
}
|
|
21436
|
+
/**
|
|
21437
|
+
* Upload one blob whose digest and size are known. `body` must produce a fresh stream
|
|
21438
|
+
* each call (a retry after a 401 restarts the upload). `onBytes` reports progress.
|
|
21439
|
+
*/
|
|
21440
|
+
async uploadBlob(digest, size, body, onBytes) {
|
|
21441
|
+
for (let attempt = 0; ; attempt++) {
|
|
21442
|
+
let sent = 0;
|
|
21443
|
+
const count = (n) => {
|
|
21444
|
+
sent += n;
|
|
21445
|
+
onBytes?.(n);
|
|
21446
|
+
};
|
|
21447
|
+
const res = await this.tryUpload(digest, size, body, count);
|
|
21448
|
+
if (res === "ok") return;
|
|
21449
|
+
if (attempt >= 1) throw this.err(`uploading blob ${digest}`, res);
|
|
21450
|
+
onBytes?.(-sent);
|
|
21451
|
+
await this.login("push,pull");
|
|
21452
|
+
}
|
|
21453
|
+
}
|
|
21454
|
+
async tryUpload(digest, size, body, onBytes) {
|
|
21455
|
+
const start = await this.authed("POST", this.url(`/v2/${this.repository}/blobs/uploads/`), { "Content-Length": "0" }, Buffer.alloc(0));
|
|
21456
|
+
if (start.status === 401) return start;
|
|
21457
|
+
if (start.status !== 202 || typeof start.headers.location !== "string") throw this.err("starting a blob upload", start);
|
|
21458
|
+
const patchUrl = this.sameOrigin(start.headers.location, "blob upload");
|
|
21459
|
+
const auth7 = { Authorization: `Bearer ${this.token}` };
|
|
21460
|
+
const patch = await this.send("PATCH", patchUrl, { ...auth7, "Content-Type": "application/octet-stream", "Content-Length": String(size) }, body, onBytes);
|
|
21461
|
+
if (patch.status === 401) return patch;
|
|
21462
|
+
if (patch.status !== 202 && patch.status !== 204) throw this.err(`uploading blob ${digest}`, patch);
|
|
21463
|
+
const putUrl = this.sameOrigin(typeof patch.headers.location === "string" ? patch.headers.location : patchUrl.href, "blob upload");
|
|
21464
|
+
putUrl.searchParams.set("digest", digest);
|
|
21465
|
+
const done = await this.send("PUT", putUrl, { ...auth7, "Content-Length": "0" }, Buffer.alloc(0));
|
|
21466
|
+
if (done.status === 401) return done;
|
|
21467
|
+
if (done.status !== 201) throw this.err(`finishing blob ${digest}`, done);
|
|
21468
|
+
return "ok";
|
|
21469
|
+
}
|
|
21470
|
+
/** True when `reference` (tag or digest) already names a manifest in this repository. */
|
|
21471
|
+
async manifestExists(reference) {
|
|
21472
|
+
const res = await this.authed("HEAD", this.url(`/v2/${this.repository}/manifests/${reference}`), { Accept: MANIFEST_ACCEPT }, void 0);
|
|
21473
|
+
if (res.status === 200) return true;
|
|
21474
|
+
if (res.status === 404) return false;
|
|
21475
|
+
throw this.err(`checking tag ${reference}`, res);
|
|
21476
|
+
}
|
|
21477
|
+
async putManifest(tag, mediaType, bytes) {
|
|
21478
|
+
const res = await this.authed("PUT", this.url(`/v2/${this.repository}/manifests/${tag}`), { "Content-Type": mediaType, "Content-Length": String(bytes.length) }, bytes);
|
|
21479
|
+
if (res.status !== 201 && res.status !== 200) throw this.err(`pushing manifest for tag ${tag}`, res);
|
|
21480
|
+
}
|
|
21481
|
+
/** Every tag in the repository; `undefined` when the repository doesn't exist yet. */
|
|
21482
|
+
async listTags() {
|
|
21483
|
+
const tags = [];
|
|
21484
|
+
let url2 = this.url(`/v2/${this.repository}/tags/list?n=1000`);
|
|
21485
|
+
for (let page = 0; url2 && page < MAX_TAG_PAGES; page++) {
|
|
21486
|
+
const res = await this.authed("GET", url2, { Accept: "application/json" }, void 0);
|
|
21487
|
+
if (res.status === 404) return void 0;
|
|
21488
|
+
if (res.status !== 200) throw this.err("listing tags", res);
|
|
21489
|
+
let j;
|
|
21490
|
+
try {
|
|
21491
|
+
j = JSON.parse(res.body.toString("utf8"));
|
|
21492
|
+
} catch {
|
|
21493
|
+
throw new RegistryError("ghcr: listing tags returned invalid JSON");
|
|
21494
|
+
}
|
|
21495
|
+
if (Array.isArray(j.tags)) {
|
|
21496
|
+
for (const t of j.tags) if (typeof t === "string") tags.push(t);
|
|
21497
|
+
}
|
|
21498
|
+
const link = typeof res.headers.link === "string" ? /<([^>]+)>;\s*rel="next"/.exec(res.headers.link) : null;
|
|
21499
|
+
url2 = link ? this.sameOrigin(link[1], "listing tags") : void 0;
|
|
21500
|
+
}
|
|
21501
|
+
return tags;
|
|
21502
|
+
}
|
|
21503
|
+
};
|
|
21504
|
+
|
|
21505
|
+
// src/verbs/ghcr/push.ts
|
|
21506
|
+
import { createReadStream } from "node:fs";
|
|
21507
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
21508
|
+
import { createGzip } from "node:zlib";
|
|
21509
|
+
import { PassThrough, Writable } from "node:stream";
|
|
21510
|
+
import { pipeline } from "node:stream/promises";
|
|
21511
|
+
var RELEASE_TAG = /^v?\d+\.\d+\.\d+$/;
|
|
21512
|
+
var GZIP_LEVEL = 6;
|
|
21513
|
+
var PushRefused = class extends Error {
|
|
21514
|
+
};
|
|
21515
|
+
function memberStream(fd, plan) {
|
|
21516
|
+
if (plan.member.size === 0) {
|
|
21517
|
+
const empty = new PassThrough();
|
|
21518
|
+
empty.end();
|
|
21519
|
+
return empty;
|
|
21520
|
+
}
|
|
21521
|
+
return createReadStream("", { fd, start: plan.member.offset, end: plan.member.offset + plan.member.size - 1, autoClose: false });
|
|
21522
|
+
}
|
|
21523
|
+
function gzipStream(fd, plan) {
|
|
21524
|
+
const src = memberStream(fd, plan);
|
|
21525
|
+
const gz = createGzip({ level: GZIP_LEVEL });
|
|
21526
|
+
src.on("error", (e) => gz.destroy(e));
|
|
21527
|
+
return src.pipe(gz);
|
|
21528
|
+
}
|
|
21529
|
+
async function gzipDigest(fd, plan) {
|
|
21530
|
+
const hash = createHash3("sha256");
|
|
21531
|
+
let size = 0;
|
|
21532
|
+
await pipeline(
|
|
21533
|
+
gzipStream(fd, plan),
|
|
21534
|
+
new Writable({
|
|
21535
|
+
write(chunk, _enc, cb) {
|
|
21536
|
+
hash.update(chunk);
|
|
21537
|
+
size += chunk.length;
|
|
21538
|
+
cb();
|
|
21539
|
+
}
|
|
21540
|
+
})
|
|
21541
|
+
);
|
|
21542
|
+
return { digest: `sha256:${hash.digest("hex")}`, size };
|
|
21543
|
+
}
|
|
21544
|
+
var sha2562 = (b) => `sha256:${createHash3("sha256").update(b).digest("hex")}`;
|
|
21545
|
+
async function pushImage(registry2, fd, plan, tag, opts) {
|
|
21546
|
+
const progress = { phase: "preparing", layersTotal: plan.layers.length, blobsUploaded: 0, blobsSkipped: 0, bytesTotal: 0, bytesSent: 0 };
|
|
21547
|
+
const report = () => opts.onProgress?.({ ...progress });
|
|
21548
|
+
report();
|
|
21549
|
+
const layers = [];
|
|
21550
|
+
for (const l of plan.layers) {
|
|
21551
|
+
if (l.compress) {
|
|
21552
|
+
const { digest, size } = await gzipDigest(fd, l);
|
|
21553
|
+
layers.push({ mediaType: l.mediaType, digest, size });
|
|
21554
|
+
} else {
|
|
21555
|
+
layers.push({ mediaType: l.mediaType, digest: l.sourceDigest ?? await rawDigest(fd, l), size: l.sourceSize });
|
|
21556
|
+
}
|
|
21557
|
+
}
|
|
21558
|
+
const config2 = { mediaType: plan.config.mediaType, digest: plan.config.digest, size: plan.config.sourceSize };
|
|
21559
|
+
progress.bytesTotal = config2.size + layers.reduce((n, d) => n + d.size, 0);
|
|
21560
|
+
progress.phase = "uploading";
|
|
21561
|
+
report();
|
|
21562
|
+
const blobs = [{ desc: config2, plan: plan.config }, ...layers.map((desc, i) => ({ desc, plan: plan.layers[i] }))];
|
|
21563
|
+
const seen = /* @__PURE__ */ new Set();
|
|
21564
|
+
for (const { desc, plan: p } of blobs) {
|
|
21565
|
+
if (seen.has(desc.digest) || await registry2.blobExists(desc.digest)) {
|
|
21566
|
+
seen.add(desc.digest);
|
|
21567
|
+
progress.blobsSkipped++;
|
|
21568
|
+
report();
|
|
21569
|
+
continue;
|
|
21570
|
+
}
|
|
21571
|
+
seen.add(desc.digest);
|
|
21572
|
+
await registry2.uploadBlob(desc.digest, desc.size, () => p.compress ? gzipStream(fd, p) : memberStream(fd, p), (n) => {
|
|
21573
|
+
progress.bytesSent += n;
|
|
21574
|
+
report();
|
|
21575
|
+
});
|
|
21576
|
+
progress.blobsUploaded++;
|
|
21577
|
+
report();
|
|
21578
|
+
}
|
|
21579
|
+
progress.phase = "manifest";
|
|
21580
|
+
report();
|
|
21581
|
+
let bytes;
|
|
21582
|
+
let mediaType;
|
|
21583
|
+
if (plan.verbatimManifest) {
|
|
21584
|
+
bytes = plan.verbatimManifest.bytes;
|
|
21585
|
+
mediaType = plan.verbatimManifest.mediaType;
|
|
21586
|
+
} else {
|
|
21587
|
+
mediaType = MEDIA.ociManifest;
|
|
21588
|
+
const cfg = { ...config2, mediaType: config2.mediaType === MEDIA.dockerConfig ? MEDIA.ociConfig : config2.mediaType };
|
|
21589
|
+
bytes = Buffer.from(
|
|
21590
|
+
JSON.stringify({
|
|
21591
|
+
schemaVersion: 2,
|
|
21592
|
+
mediaType,
|
|
21593
|
+
config: cfg,
|
|
21594
|
+
layers: layers.map((l) => ({ ...l, mediaType: l.mediaType === MEDIA.dockerLayerGzip ? MEDIA.ociLayerGzip : l.mediaType })),
|
|
21595
|
+
...plan.annotations ? { annotations: plan.annotations } : {}
|
|
21596
|
+
})
|
|
21597
|
+
);
|
|
21598
|
+
}
|
|
21599
|
+
if (opts.protectReleaseTags && RELEASE_TAG.test(tag) && await registry2.manifestExists(tag)) {
|
|
21600
|
+
throw new PushRefused(`ghcr_push: tag ${tag} already exists on ${registry2.repository} and release tags are protected \u2014 push a new version instead`);
|
|
21601
|
+
}
|
|
21602
|
+
await registry2.putManifest(tag, mediaType, bytes);
|
|
21603
|
+
progress.phase = "done";
|
|
21604
|
+
report();
|
|
21605
|
+
return { digest: sha2562(bytes), mediaType, size: progress.bytesTotal, blobsUploaded: progress.blobsUploaded, blobsSkipped: progress.blobsSkipped };
|
|
21606
|
+
}
|
|
21607
|
+
async function rawDigest(fd, plan) {
|
|
21608
|
+
const hash = createHash3("sha256");
|
|
21609
|
+
await pipeline(
|
|
21610
|
+
memberStream(fd, plan),
|
|
21611
|
+
new Writable({
|
|
21612
|
+
write(chunk, _enc, cb) {
|
|
21613
|
+
hash.update(chunk);
|
|
21614
|
+
cb();
|
|
21615
|
+
}
|
|
21616
|
+
})
|
|
21617
|
+
);
|
|
21618
|
+
return `sha256:${hash.digest("hex")}`;
|
|
21619
|
+
}
|
|
21620
|
+
|
|
21621
|
+
// src/verbs/ghcr/jobs.ts
|
|
21622
|
+
import { randomUUID } from "node:crypto";
|
|
21623
|
+
var QueueFull = class extends Error {
|
|
21624
|
+
};
|
|
21625
|
+
var JobQueue = class {
|
|
21626
|
+
jobs = /* @__PURE__ */ new Map();
|
|
21627
|
+
pending = [];
|
|
21628
|
+
running = false;
|
|
21629
|
+
maxPending;
|
|
21630
|
+
keepFinished;
|
|
21631
|
+
constructor({ maxPending = 8, keepFinished = 50 } = {}) {
|
|
21632
|
+
this.maxPending = maxPending;
|
|
21633
|
+
this.keepFinished = keepFinished;
|
|
21634
|
+
}
|
|
21635
|
+
/** Queued + running jobs. */
|
|
21636
|
+
activeCount() {
|
|
21637
|
+
return this.pending.length + (this.running ? 1 : 0);
|
|
21638
|
+
}
|
|
21639
|
+
submit(meta2, hooks) {
|
|
21640
|
+
if (this.activeCount() >= this.maxPending) throw new QueueFull(`ghcr_push: ${this.activeCount()} pushes are already queued or running \u2014 wait for one to finish (ghcr_push_status)`);
|
|
21641
|
+
const job = { ...meta2, job: randomUUID(), state: "queued", createdAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
21642
|
+
this.jobs.set(job.job, job);
|
|
21643
|
+
this.pending.push({ job, hooks });
|
|
21644
|
+
const snapshot = { ...job };
|
|
21645
|
+
void this.pump();
|
|
21646
|
+
return snapshot;
|
|
21647
|
+
}
|
|
21648
|
+
get(id) {
|
|
21649
|
+
const j = this.jobs.get(id);
|
|
21650
|
+
return j ? structuredClone(j) : void 0;
|
|
21651
|
+
}
|
|
21652
|
+
list(owner2) {
|
|
21653
|
+
return [...this.jobs.values()].filter((j) => j.owner === owner2).map((j) => structuredClone(j));
|
|
21654
|
+
}
|
|
21655
|
+
/** Resolves once nothing is queued or running (for tests and orderly shutdown). */
|
|
21656
|
+
async idle() {
|
|
21657
|
+
while (this.activeCount() > 0) await new Promise((r) => setTimeout(r, 5));
|
|
21658
|
+
}
|
|
21659
|
+
async pump() {
|
|
21660
|
+
if (this.running) return;
|
|
21661
|
+
const next = this.pending.shift();
|
|
21662
|
+
if (!next) return;
|
|
21663
|
+
this.running = true;
|
|
21664
|
+
const { job, hooks } = next;
|
|
21665
|
+
job.state = "running";
|
|
21666
|
+
job.startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
21667
|
+
try {
|
|
21668
|
+
job.result = await hooks.run((p) => {
|
|
21669
|
+
job.progress = p;
|
|
21670
|
+
});
|
|
21671
|
+
job.state = "succeeded";
|
|
21672
|
+
} catch (err) {
|
|
21673
|
+
job.state = "failed";
|
|
21674
|
+
job.error = hooks.describeError ? hooks.describeError(err) : "push failed";
|
|
21675
|
+
} finally {
|
|
21676
|
+
job.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
21677
|
+
try {
|
|
21678
|
+
hooks.finally?.(structuredClone(job));
|
|
21679
|
+
} catch {
|
|
21680
|
+
}
|
|
21681
|
+
this.running = false;
|
|
21682
|
+
this.prune();
|
|
21683
|
+
void this.pump();
|
|
21684
|
+
}
|
|
21685
|
+
}
|
|
21686
|
+
prune() {
|
|
21687
|
+
const finished = [...this.jobs.values()].filter((j) => j.state === "succeeded" || j.state === "failed");
|
|
21688
|
+
for (const j of finished.slice(0, Math.max(0, finished.length - this.keepFinished))) this.jobs.delete(j.job);
|
|
21689
|
+
}
|
|
21690
|
+
};
|
|
21691
|
+
|
|
21692
|
+
// src/verbs/ghcr_push.ts
|
|
21693
|
+
var IMAGE = /^[a-z0-9]+(?:(?:\.|_|__|-+)[a-z0-9]+)*(?:\/[a-z0-9]+(?:(?:\.|_|__|-+)[a-z0-9]+)*)*$/;
|
|
21694
|
+
var TAG = /^[A-Za-z0-9_][A-Za-z0-9._-]{0,127}$/;
|
|
21695
|
+
var JOB_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
21696
|
+
var PUSH_KEYS = /* @__PURE__ */ new Set(["owner", "image", "tag", "archive"]);
|
|
21697
|
+
var STATUS_KEYS = /* @__PURE__ */ new Set(["owner", "job"]);
|
|
21698
|
+
var TAGS_KEYS = /* @__PURE__ */ new Set(["owner", "image"]);
|
|
21699
|
+
function strictArgs(verb, input, keys) {
|
|
21700
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) throw new Error(`${verb}: arguments must be an object`);
|
|
21701
|
+
const a = input;
|
|
21702
|
+
const unknown2 = Object.keys(a).filter((k) => !keys.has(k));
|
|
21703
|
+
if (unknown2.length) throw new Error(`${verb}: unknown argument(s): ${unknown2.join(", ")} \u2014 expected ${[...keys].join(", ")}`);
|
|
21704
|
+
if (typeof a.owner !== "string" || !a.owner.trim()) throw new Error(`${verb}: "owner" must be a non-empty string`);
|
|
21705
|
+
return a;
|
|
21706
|
+
}
|
|
21707
|
+
var GhcrService = class {
|
|
21708
|
+
cfg;
|
|
21709
|
+
secrets;
|
|
21710
|
+
base;
|
|
21711
|
+
queue;
|
|
21712
|
+
constructor(cfg, secrets, opts = {}) {
|
|
21713
|
+
this.cfg = cfg;
|
|
21714
|
+
this.secrets = secrets;
|
|
21715
|
+
this.base = opts.base ?? GHCR_BASE;
|
|
21716
|
+
this.queue = opts.queue ?? new JobQueue();
|
|
21717
|
+
}
|
|
21718
|
+
config(verb) {
|
|
21719
|
+
if (!this.cfg) throw new Error(`${verb}: GHCR is not configured \u2014 add a "ghcr" block to the config (see docs/TOOLS.md#ghcr_push)`);
|
|
21720
|
+
return this.cfg;
|
|
21721
|
+
}
|
|
21722
|
+
ownerConfig(verb, owner2) {
|
|
21723
|
+
const o = this.config(verb).owners[owner2.toLowerCase()];
|
|
21724
|
+
if (!o) throw new Error(`${verb}: owner "${owner2}" has no GHCR configuration (config.ghcr.owners)`);
|
|
21725
|
+
if (!this.secrets.has(o.credential)) throw new Error(`${verb}: owner "${owner2}"'s GHCR credential "${o.credential.replace(/^cred:/, "")}" is not stored (re-run the installer to add it)`);
|
|
21726
|
+
return o;
|
|
21727
|
+
}
|
|
21728
|
+
image(verb, owner2, oc, raw) {
|
|
21729
|
+
if (typeof raw !== "string" || !raw) throw new Error(`${verb}: "image" must be a non-empty string`);
|
|
21730
|
+
const lower = raw.toLowerCase();
|
|
21731
|
+
if (lower.startsWith("ghcr.io/") || lower.startsWith(`${owner2}/`)) {
|
|
21732
|
+
throw new Error(`${verb}: "image" is the name under ghcr.io/${owner2}/ \u2014 pass e.g. "hoot", not "${raw}"`);
|
|
21733
|
+
}
|
|
21734
|
+
if (!IMAGE.test(raw) || raw.length + owner2.length + 1 > 255) throw new Error(`${verb}: "image" "${raw}" is not a valid image name (lowercase [a-z0-9], separators . _ __ -, "/" between segments)`);
|
|
21735
|
+
if (!imageAllowed(oc, raw)) throw new Error(`${verb}: ghcr.io/${owner2}/${raw} is not on the allowlist (config.ghcr.owners.${owner2}.images: ${oc.images.join(", ")})`);
|
|
21736
|
+
return raw;
|
|
21737
|
+
}
|
|
21738
|
+
registry(owner2, oc, image2) {
|
|
21739
|
+
return new Registry({ base: this.base, username: oc.username, password: this.secrets.resolve(oc.credential), repository: `${owner2}/${image2}` });
|
|
21740
|
+
}
|
|
21741
|
+
async push(input) {
|
|
21742
|
+
const verb = "ghcr_push";
|
|
21743
|
+
const a = strictArgs(verb, input, PUSH_KEYS);
|
|
21744
|
+
const owner2 = a.owner.toLowerCase();
|
|
21745
|
+
const cfg = this.config(verb);
|
|
21746
|
+
const oc = this.ownerConfig(verb, owner2);
|
|
21747
|
+
const image2 = this.image(verb, owner2, oc, a.image);
|
|
21748
|
+
if (typeof a.tag !== "string" || !TAG.test(a.tag)) throw new Error(`${verb}: "tag" must be a valid tag ([A-Za-z0-9_][A-Za-z0-9._-]{0,127})`);
|
|
21749
|
+
const tag = a.tag;
|
|
21750
|
+
const archive = openArchive(a.archive, cfg.dropDir, cfg.maxArchiveBytes);
|
|
21751
|
+
let handedOff = false;
|
|
21752
|
+
let registry2;
|
|
21753
|
+
try {
|
|
21754
|
+
const plan = planImage(archive.fd, archive.size);
|
|
21755
|
+
registry2 = this.registry(owner2, oc, image2);
|
|
21756
|
+
const ref = `${registry2.host}/${owner2}/${image2}`;
|
|
21757
|
+
await registry2.login("push,pull");
|
|
21758
|
+
if (cfg.protectReleaseTags && RELEASE_TAG.test(tag) && await registry2.manifestExists(tag)) {
|
|
21759
|
+
throw new Error(`${verb}: tag ${tag} already exists on ${ref} and release tags are protected \u2014 push a new version instead`);
|
|
21760
|
+
}
|
|
21761
|
+
const reg = registry2;
|
|
21762
|
+
const job = this.queue.submit(
|
|
21763
|
+
{ owner: owner2, image: ref, tag, archive: archive.path, archiveBytes: archive.size, format: plan.format },
|
|
21764
|
+
{
|
|
21765
|
+
run: (progress) => pushImage(reg, archive.fd, plan, tag, { protectReleaseTags: cfg.protectReleaseTags, onProgress: progress }),
|
|
21766
|
+
describeError: (err) => reg.clean(err?.message ?? String(err)),
|
|
21767
|
+
finally: (done) => {
|
|
21768
|
+
archive.close();
|
|
21769
|
+
audit({
|
|
21770
|
+
tool: verb,
|
|
21771
|
+
owner: owner2,
|
|
21772
|
+
mode: "write",
|
|
21773
|
+
repo: ref,
|
|
21774
|
+
decision: "custom",
|
|
21775
|
+
ok: done.state === "succeeded",
|
|
21776
|
+
detail: done.state === "succeeded" ? `job=${done.job} tag=${tag} digest=${done.result?.digest} size=${done.result?.size} uploaded=${done.result?.blobsUploaded} skipped=${done.result?.blobsSkipped} archive=${archive.path}` : `job=${done.job} tag=${tag} archive=${archive.path} error=${done.error}`
|
|
21777
|
+
});
|
|
21778
|
+
}
|
|
21779
|
+
}
|
|
21780
|
+
);
|
|
21781
|
+
handedOff = true;
|
|
21782
|
+
audit({ tool: verb, owner: owner2, mode: "write", repo: ref, decision: "custom", ok: true, detail: `job=${job.job} queued tag=${tag} archive=${archive.path} bytes=${archive.size} format=${plan.format} layers=${plan.layers.length}` });
|
|
21783
|
+
return { ...job };
|
|
21784
|
+
} catch (err) {
|
|
21785
|
+
if (!registry2) throw err;
|
|
21786
|
+
throw new Error(registry2.clean(err?.message ?? String(err)));
|
|
21787
|
+
} finally {
|
|
21788
|
+
if (!handedOff) archive.close();
|
|
21789
|
+
}
|
|
21790
|
+
}
|
|
21791
|
+
status(input) {
|
|
21792
|
+
const verb = "ghcr_push_status";
|
|
21793
|
+
const a = strictArgs(verb, input, STATUS_KEYS);
|
|
21794
|
+
const owner2 = a.owner.toLowerCase();
|
|
21795
|
+
this.ownerConfig(verb, owner2);
|
|
21796
|
+
if (a.job === void 0) return { jobs: this.queue.list(owner2) };
|
|
21797
|
+
if (typeof a.job !== "string" || !JOB_ID.test(a.job)) throw new Error(`${verb}: "job" must be a job id returned by ghcr_push`);
|
|
21798
|
+
const job = this.queue.get(a.job);
|
|
21799
|
+
if (!job || job.owner !== owner2) throw new Error(`${verb}: no job ${a.job} for owner ${owner2} (jobs are kept in memory; a broker restart forgets them)`);
|
|
21800
|
+
return job;
|
|
21801
|
+
}
|
|
21802
|
+
async tags(input) {
|
|
21803
|
+
const verb = "ghcr_tags";
|
|
21804
|
+
const a = strictArgs(verb, input, TAGS_KEYS);
|
|
21805
|
+
const owner2 = a.owner.toLowerCase();
|
|
21806
|
+
const oc = this.ownerConfig(verb, owner2);
|
|
21807
|
+
const image2 = this.image(verb, owner2, oc, a.image);
|
|
21808
|
+
const registry2 = this.registry(owner2, oc, image2);
|
|
21809
|
+
try {
|
|
21810
|
+
await registry2.login("pull");
|
|
21811
|
+
const tags = await registry2.listTags();
|
|
21812
|
+
return { image: `${registry2.host}/${owner2}/${image2}`, exists: tags !== void 0, tags: tags ?? [] };
|
|
21813
|
+
} catch (err) {
|
|
21814
|
+
throw new Error(registry2.clean(err?.message ?? String(err)));
|
|
21815
|
+
}
|
|
21816
|
+
}
|
|
21817
|
+
};
|
|
21818
|
+
var owner = { type: "string", description: "registry owner (org/user login) \u2014 must be configured in config.ghcr.owners" };
|
|
21819
|
+
var image = { type: "string", description: 'image name under ghcr.io/<owner>/, e.g. "hoot" \u2014 must match config.ghcr.owners.<owner>.images' };
|
|
21820
|
+
var GHCR_PUSH_SCHEMA = {
|
|
21821
|
+
type: "object",
|
|
21822
|
+
required: ["owner", "image", "tag", "archive"],
|
|
21823
|
+
additionalProperties: false,
|
|
21824
|
+
properties: {
|
|
21825
|
+
owner,
|
|
21826
|
+
image,
|
|
21827
|
+
tag: { type: "string", pattern: "^[A-Za-z0-9_][A-Za-z0-9._-]{0,127}$", description: 'tag to push, e.g. "1.4.0" or "latest"' },
|
|
21828
|
+
archive: {
|
|
21829
|
+
type: "string",
|
|
21830
|
+
description: "a `docker save` archive (docker-archive or OCI layout) inside the configured drop directory \u2014 absolute, or relative to it. Must be a regular, group-readable file; symlinks and hard links are refused."
|
|
21831
|
+
}
|
|
21832
|
+
}
|
|
21833
|
+
};
|
|
21834
|
+
var GHCR_PUSH_STATUS_SCHEMA = {
|
|
21835
|
+
type: "object",
|
|
21836
|
+
required: ["owner"],
|
|
21837
|
+
additionalProperties: false,
|
|
21838
|
+
properties: {
|
|
21839
|
+
owner,
|
|
21840
|
+
job: { type: "string", description: "job id returned by ghcr_push; omit to list this owner's recent jobs" }
|
|
21841
|
+
}
|
|
21842
|
+
};
|
|
21843
|
+
var GHCR_TAGS_SCHEMA = {
|
|
21844
|
+
type: "object",
|
|
21845
|
+
required: ["owner", "image"],
|
|
21846
|
+
additionalProperties: false,
|
|
21847
|
+
properties: { owner, image }
|
|
21848
|
+
};
|
|
21849
|
+
var GHCR_CATALOG = {
|
|
21850
|
+
ghcr_push: {
|
|
21851
|
+
description: "Push a container image to ghcr.io/<owner>/<image>:<tag> from a `docker save` archive in the broker's drop directory, without holding a registry token. The image must be on config.ghcr's allowlist; an existing release tag (1.2.3 / v1.2.3) is never overwritten while protectReleaseTags is on. Starts a background job and returns its id at once \u2014 poll ghcr_push_status. Runs on the owner's GHCR credential (classic PAT, write:packages).",
|
|
21852
|
+
inputSchema: GHCR_PUSH_SCHEMA
|
|
21853
|
+
},
|
|
21854
|
+
ghcr_push_status: {
|
|
21855
|
+
description: "Progress of a ghcr_push job (phase, blobs uploaded/skipped, bytes sent), then its final manifest digest or error. Omit job to list this owner's recent jobs. Jobs are kept in memory; a broker restart forgets them.",
|
|
21856
|
+
inputSchema: GHCR_PUSH_STATUS_SCHEMA
|
|
21857
|
+
},
|
|
21858
|
+
ghcr_tags: {
|
|
21859
|
+
description: "List the tags of an allowlisted image on ghcr.io/<owner>/<image>, e.g. to verify a push landed. Runs on the owner's GHCR credential (pull scope).",
|
|
21860
|
+
inputSchema: GHCR_TAGS_SCHEMA
|
|
21861
|
+
}
|
|
21862
|
+
};
|
|
21863
|
+
|
|
20471
21864
|
// src/router.ts
|
|
21865
|
+
var GHCR_TOOLS2 = /* @__PURE__ */ new Set(["ghcr_push", "ghcr_push_status", "ghcr_tags"]);
|
|
20472
21866
|
var Router = class {
|
|
20473
21867
|
config;
|
|
20474
21868
|
secrets;
|
|
20475
21869
|
upstream;
|
|
20476
21870
|
appAuth;
|
|
20477
21871
|
rules;
|
|
21872
|
+
/** GHCR verbs run on config.ghcr's own credential, not the owner's GitHub PAT (created on first use). */
|
|
21873
|
+
ghcr;
|
|
20478
21874
|
constructor(config2, secrets, upstream, appAuth) {
|
|
20479
21875
|
this.config = config2;
|
|
20480
21876
|
this.secrets = secrets;
|
|
@@ -20484,7 +21880,7 @@ var Router = class {
|
|
|
20484
21880
|
}
|
|
20485
21881
|
/** Tools advertised to the agent (used to build tools/list). */
|
|
20486
21882
|
advertisedTools() {
|
|
20487
|
-
return this.config.tools;
|
|
21883
|
+
return this.config.tools.filter((rule) => !unavailableReason(rule, this.config, this.secrets));
|
|
20488
21884
|
}
|
|
20489
21885
|
async call(tool, args) {
|
|
20490
21886
|
const rule = this.rules.get(tool);
|
|
@@ -20492,32 +21888,52 @@ var Router = class {
|
|
|
20492
21888
|
audit({ tool, decision: "deny", detail: "not in allowlist" });
|
|
20493
21889
|
throw new Error(`tool "${tool}" is not allowed`);
|
|
20494
21890
|
}
|
|
20495
|
-
|
|
20496
|
-
if (
|
|
20497
|
-
|
|
21891
|
+
const unavailable = unavailableReason(rule, this.config, this.secrets);
|
|
21892
|
+
if (unavailable) {
|
|
21893
|
+
audit({ tool, decision: "deny", detail: `unavailable: ${unavailable}` });
|
|
21894
|
+
throw new Error(`tool "${tool}" is not available on this broker: ${unavailable}`);
|
|
21895
|
+
}
|
|
21896
|
+
if (GHCR_TOOLS2.has(tool)) return this.callGhcr(tool, args, rule);
|
|
21897
|
+
let owner2 = extractOwner(args);
|
|
21898
|
+
if (!owner2 && rule.mode === "read" && this.config.defaultOwner && this.config.owners[this.config.defaultOwner]) {
|
|
21899
|
+
owner2 = this.config.defaultOwner.toLowerCase();
|
|
20498
21900
|
}
|
|
20499
|
-
if (!
|
|
21901
|
+
if (!owner2) {
|
|
20500
21902
|
audit({ tool, decision: "error", detail: "no owner in args" });
|
|
20501
21903
|
throw new Error(`tool "${tool}": could not determine repo owner from arguments`);
|
|
20502
21904
|
}
|
|
20503
|
-
const creds = this.config.owners[
|
|
21905
|
+
const creds = this.config.owners[owner2];
|
|
20504
21906
|
if (!creds) {
|
|
20505
|
-
audit({ tool, owner, decision: "error", detail: "no creds for owner" });
|
|
20506
|
-
throw new Error(`no credentials configured for owner "${
|
|
21907
|
+
audit({ tool, owner: owner2, decision: "error", detail: "no creds for owner" });
|
|
21908
|
+
throw new Error(`no credentials configured for owner "${owner2}" \u2014 add it to config.owners + creds`);
|
|
20507
21909
|
}
|
|
20508
21910
|
const token = this.secrets.resolve(rule.mode === "write" ? creds.write : creds.read);
|
|
20509
|
-
const repo = typeof args.repo === "string" ? `${
|
|
21911
|
+
const repo = typeof args.repo === "string" ? `${owner2}/${args.repo}` : owner2;
|
|
20510
21912
|
const decision = rule.custom ? "custom" : rule.augment ? "augment" : "forward";
|
|
20511
21913
|
try {
|
|
20512
|
-
const out = rule.custom ? await this.dispatchCustom(tool, args,
|
|
20513
|
-
audit({ tool, owner, mode: rule.mode, repo, decision, ok: true });
|
|
21914
|
+
const out = rule.custom ? await this.dispatchCustom(tool, args, owner2, token) : rule.augment ? await this.dispatchAugmented(tool, args, owner2, token) : await this.upstream.forward(tool, args, token);
|
|
21915
|
+
audit({ tool, owner: owner2, mode: rule.mode, repo, decision, ok: true });
|
|
20514
21916
|
return out;
|
|
20515
21917
|
} catch (err) {
|
|
20516
|
-
audit({ tool, owner, mode: rule.mode, repo, decision, ok: false, detail: err.message });
|
|
21918
|
+
audit({ tool, owner: owner2, mode: rule.mode, repo, decision, ok: false, detail: err.message });
|
|
20517
21919
|
throw err;
|
|
20518
21920
|
}
|
|
20519
21921
|
}
|
|
20520
|
-
|
|
21922
|
+
/** GHCR verbs: config.ghcr checks the owner and image itself; no GitHub PAT is resolved. */
|
|
21923
|
+
async callGhcr(tool, args, rule) {
|
|
21924
|
+
const owner2 = extractOwner(args) ?? "";
|
|
21925
|
+
const repo = typeof args.image === "string" ? `${owner2}/${args.image}` : owner2;
|
|
21926
|
+
try {
|
|
21927
|
+
this.ghcr ??= new GhcrService(this.config.ghcr, this.secrets);
|
|
21928
|
+
const out = tool === "ghcr_push" ? await this.ghcr.push(args) : tool === "ghcr_tags" ? await this.ghcr.tags(args) : this.ghcr.status(args);
|
|
21929
|
+
audit({ tool, owner: owner2, mode: rule.mode, repo, decision: "custom", ok: true });
|
|
21930
|
+
return { content: [{ type: "text", text: JSON.stringify(out) }] };
|
|
21931
|
+
} catch (err) {
|
|
21932
|
+
audit({ tool, owner: owner2, mode: rule.mode, repo, decision: "custom", ok: false, detail: err.message });
|
|
21933
|
+
throw err;
|
|
21934
|
+
}
|
|
21935
|
+
}
|
|
21936
|
+
async dispatchCustom(tool, args, owner2, token) {
|
|
20521
21937
|
switch (tool) {
|
|
20522
21938
|
// DEFERRED: link_issue (App-token-only addClosingIssueReference) is not wired in — see docs.
|
|
20523
21939
|
// Re-enable by restoring this case + the import above + the catalog block, adding a
|
|
@@ -20563,7 +21979,14 @@ var Router = class {
|
|
|
20563
21979
|
case "report_ci_status": {
|
|
20564
21980
|
const octokit = new Octokit({ auth: token });
|
|
20565
21981
|
const reportArgs = args;
|
|
20566
|
-
const out = await reportCiStatus(octokit, reportArgs, this.makeJobLogsFetcher(
|
|
21982
|
+
const out = await reportCiStatus(octokit, reportArgs, this.makeJobLogsFetcher(owner2, reportArgs.repo, token));
|
|
21983
|
+
return { content: [{ type: "text", text: JSON.stringify(out) }] };
|
|
21984
|
+
}
|
|
21985
|
+
case "npm_scaffold": {
|
|
21986
|
+
const npm = this.config.npm;
|
|
21987
|
+
const octokit = new Octokit({ auth: this.secrets.resolve(this.config.owners[owner2].read) });
|
|
21988
|
+
const registry2 = new NpmRegistry({ token: this.secrets.resolve(npm.token), ...npm.registry ? { registry: npm.registry } : {} });
|
|
21989
|
+
const out = await npmScaffold(args, { octokit, registry: registry2, scopes: npm.scopes });
|
|
20567
21990
|
return { content: [{ type: "text", text: JSON.stringify(out) }] };
|
|
20568
21991
|
}
|
|
20569
21992
|
default:
|
|
@@ -20578,11 +22001,11 @@ var Router = class {
|
|
|
20578
22001
|
* just yields an empty map — report_ci_status treats a lookup miss as "no
|
|
20579
22002
|
* logTail for this job", never a hard failure of the whole report.
|
|
20580
22003
|
*/
|
|
20581
|
-
makeJobLogsFetcher(
|
|
22004
|
+
makeJobLogsFetcher(owner2, repo, token) {
|
|
20582
22005
|
return async (runId, tailLines) => {
|
|
20583
22006
|
const map = /* @__PURE__ */ new Map();
|
|
20584
22007
|
try {
|
|
20585
|
-
const result = await this.upstream.forward("get_job_logs", { owner, repo, run_id: runId, failed_only: true, return_content: true, tail_lines: tailLines }, token);
|
|
22008
|
+
const result = await this.upstream.forward("get_job_logs", { owner: owner2, repo, run_id: runId, failed_only: true, return_content: true, tail_lines: tailLines }, token);
|
|
20586
22009
|
const textBlock = result.content?.find((c) => c.type === "text");
|
|
20587
22010
|
if (!textBlock?.text) return map;
|
|
20588
22011
|
const parsed = JSON.parse(textBlock.text);
|
|
@@ -20610,7 +22033,7 @@ var Router = class {
|
|
|
20610
22033
|
* rather than thrown — a shape mismatch should degrade to "no CI data added," not
|
|
20611
22034
|
* break an otherwise-successful PR read.
|
|
20612
22035
|
*/
|
|
20613
|
-
async dispatchAugmented(tool, args,
|
|
22036
|
+
async dispatchAugmented(tool, args, owner2, token) {
|
|
20614
22037
|
switch (tool) {
|
|
20615
22038
|
case "pull_request_read": {
|
|
20616
22039
|
const { ciVerbosity, logLines, ...upstreamArgs } = args;
|
|
@@ -20629,10 +22052,10 @@ var Router = class {
|
|
|
20629
22052
|
if (typeof p.number !== "number" || typeof p.head?.sha !== "string") return result;
|
|
20630
22053
|
const repo = String(upstreamArgs.repo ?? "");
|
|
20631
22054
|
const octokit = new Octokit({ auth: token });
|
|
20632
|
-
const ci = await ciStatusForHeadSha(octokit,
|
|
22055
|
+
const ci = await ciStatusForHeadSha(octokit, owner2, repo, p.number, p.head.sha, {
|
|
20633
22056
|
verbosity: Number(ciVerbosity),
|
|
20634
22057
|
logLines: logLines !== void 0 ? Number(logLines) : void 0,
|
|
20635
|
-
fetchFailedJobLogs: this.makeJobLogsFetcher(
|
|
22058
|
+
fetchFailedJobLogs: this.makeJobLogsFetcher(owner2, repo, token)
|
|
20636
22059
|
});
|
|
20637
22060
|
textBlock.text = JSON.stringify({ ...pr, ci });
|
|
20638
22061
|
return result;
|
|
@@ -21291,8 +22714,8 @@ function getDERfromPEM(pem) {
|
|
|
21291
22714
|
const decoded = atob(pemB64);
|
|
21292
22715
|
return string2ArrayBuffer(decoded);
|
|
21293
22716
|
}
|
|
21294
|
-
function getEncodedMessage(
|
|
21295
|
-
return `${base64encodeJSON(
|
|
22717
|
+
function getEncodedMessage(header2, payload) {
|
|
22718
|
+
return `${base64encodeJSON(header2)}.${base64encodeJSON(payload)}`;
|
|
21296
22719
|
}
|
|
21297
22720
|
function base64encode(buffer) {
|
|
21298
22721
|
var binary = "";
|
|
@@ -21338,7 +22761,7 @@ async function getToken({ privateKey, payload }) {
|
|
|
21338
22761
|
name: "RSASSA-PKCS1-v1_5",
|
|
21339
22762
|
hash: { name: "SHA-256" }
|
|
21340
22763
|
};
|
|
21341
|
-
const
|
|
22764
|
+
const header2 = { alg: "RS256", typ: "JWT" };
|
|
21342
22765
|
const privateKeyDER = getDERfromPEM(convertedPrivateKey);
|
|
21343
22766
|
const importedKey = await subtle.importKey(
|
|
21344
22767
|
"pkcs8",
|
|
@@ -21347,7 +22770,7 @@ async function getToken({ privateKey, payload }) {
|
|
|
21347
22770
|
false,
|
|
21348
22771
|
["sign"]
|
|
21349
22772
|
);
|
|
21350
|
-
const encodedMessage = getEncodedMessage(
|
|
22773
|
+
const encodedMessage = getEncodedMessage(header2, payload);
|
|
21351
22774
|
const encodedMessageArrBuf = string2ArrayBuffer(encodedMessage);
|
|
21352
22775
|
const signatureArrBuf = await subtle.sign(
|
|
21353
22776
|
algorithm.name,
|
|
@@ -21994,8 +23417,8 @@ var AppAuth = class {
|
|
|
21994
23417
|
this.privateKey = secrets.resolve(config2.app.privateKey);
|
|
21995
23418
|
}
|
|
21996
23419
|
/** App-authed Octokit for this owner's installation, or null if none configured. */
|
|
21997
|
-
octokitFor(
|
|
21998
|
-
const installationId = this.config.owners[
|
|
23420
|
+
octokitFor(owner2) {
|
|
23421
|
+
const installationId = this.config.owners[owner2]?.installationId;
|
|
21999
23422
|
if (installationId === void 0) return null;
|
|
22000
23423
|
let ok = this.cache.get(installationId);
|
|
22001
23424
|
if (!ok) {
|
|
@@ -22030,6 +23453,11 @@ async function buildCatalog(config2, secrets, upstream) {
|
|
|
22030
23453
|
}
|
|
22031
23454
|
const tools = [];
|
|
22032
23455
|
for (const rule of config2.tools) {
|
|
23456
|
+
const unavailable = unavailableReason(rule, config2, secrets);
|
|
23457
|
+
if (unavailable) {
|
|
23458
|
+
console.error(`catalog: "${rule.name}" is not exposed: ${unavailable}`);
|
|
23459
|
+
continue;
|
|
23460
|
+
}
|
|
22033
23461
|
if (rule.custom) {
|
|
22034
23462
|
if (rule.name === "resolve_review_thread") {
|
|
22035
23463
|
tools.push({
|
|
@@ -22049,6 +23477,12 @@ async function buildCatalog(config2, secrets, upstream) {
|
|
|
22049
23477
|
description: 'Edit the labels on one issue or PR without touching any other label on it. add / remove take one label or several (combinable in one call); swap { prefix, to } removes every label starting with prefix and adds to (e.g. prefix "status: ", to "status: verified"); set replaces the whole set and only as an explicit choice. Labels already present (add) or absent (remove) are no-ops. A label name to add that does not exist in the repo refuses the call and changes nothing, unless create_missing is true. Returns the resulting label set plus what was added, removed and created. Runs on the write PAT (Issues: write).',
|
|
22050
23478
|
inputSchema: ISSUE_LABELS_SCHEMA
|
|
22051
23479
|
});
|
|
23480
|
+
} else if (rule.name === "npm_scaffold") {
|
|
23481
|
+
tools.push({
|
|
23482
|
+
name: "npm_scaffold",
|
|
23483
|
+
description: "Create a new npm package and register its GitHub Actions trusted publisher, so the repo's publish workflow can release it through OIDC from then on. Publishes a broker-built placeholder (0.0.0-stage, no code) and never publishes a version of an existing package. Every npm write needs its own one-time password from the user, so it takes two calls: the first publishes the placeholder and returns status needs_otp; ask the user for the next code and call again to add the trusted publisher. Ask for each code right before the call. A refused or expired code also returns needs_otp with the remaining steps. Checks first that the repo's package.json names the package and that the workflow file exists. Uses the broker's npm token (no 2FA bypass) and the read PAT.",
|
|
23484
|
+
inputSchema: NPM_SCAFFOLD_SCHEMA
|
|
23485
|
+
});
|
|
22052
23486
|
} else if (rule.name === "report_unlabeled") {
|
|
22053
23487
|
tools.push({
|
|
22054
23488
|
name: "report_unlabeled",
|
|
@@ -22073,6 +23507,8 @@ async function buildCatalog(config2, secrets, upstream) {
|
|
|
22073
23507
|
description: "Report CI health for a PR, or every open PR in a repo, at a chosen verbosity: 0 counts only, 1 adds which run/job failed, 2 adds which step failed, 3 adds a tail of the failing job's log. Detail beyond tier 0 is only fetched for runs that are actually failing. Runs on the read PAT.",
|
|
22074
23508
|
inputSchema: REPORT_CI_STATUS_SCHEMA
|
|
22075
23509
|
});
|
|
23510
|
+
} else if (GHCR_CATALOG[rule.name]) {
|
|
23511
|
+
tools.push({ name: rule.name, ...GHCR_CATALOG[rule.name] });
|
|
22076
23512
|
}
|
|
22077
23513
|
continue;
|
|
22078
23514
|
}
|