@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.
Files changed (90) hide show
  1. package/README.md +134 -40
  2. package/config.example.json +21 -8
  3. package/config.sample.json +45 -0
  4. package/deploy/README.md +73 -15
  5. package/deploy/install.mjs +166 -101
  6. package/deploy/install.sh +0 -0
  7. package/deploy/lib/config-merge.mjs +174 -16
  8. package/deploy/lib/credentials.mjs +143 -0
  9. package/deploy/lib/install-args.mjs +144 -0
  10. package/deploy/lib/npm-setup.mjs +164 -0
  11. package/deploy/lib/secret-store.mjs +20 -2
  12. package/deploy/lib/system.mjs +26 -2
  13. package/dist/index.mjs +1638 -202
  14. package/dist/index.mjs.map +4 -4
  15. package/docs/TOOLS.md +192 -1
  16. package/docs/changelog/v0/v0.8.0.md +115 -0
  17. package/docs/deploy-architecture.md +13 -11
  18. package/package.json +7 -7
  19. package/src/availability.ts +42 -0
  20. package/src/catalog.ts +18 -1
  21. package/src/config.ts +29 -2
  22. package/src/router.ts +43 -2
  23. package/src/secrets.ts +34 -4
  24. package/src/types.ts +25 -1
  25. package/src/verbs/ghcr/archive.ts +314 -0
  26. package/src/verbs/ghcr/config.ts +142 -0
  27. package/src/verbs/ghcr/jobs.ts +141 -0
  28. package/src/verbs/ghcr/push.ts +192 -0
  29. package/src/verbs/ghcr/registry.ts +275 -0
  30. package/src/verbs/ghcr/tar.ts +203 -0
  31. package/src/verbs/ghcr_push.ts +264 -0
  32. package/src/verbs/npm-registry.ts +168 -0
  33. package/src/verbs/npm-tarball.ts +76 -0
  34. package/src/verbs/npm_scaffold.ts +325 -0
  35. package/src/verbs/release-analysis.ts +3 -1
  36. package/tests/catalog.test.vitest.mts +55 -1
  37. package/tests/config-merge.test.vitest.mts +275 -8
  38. package/tests/config.test.vitest.mts +75 -1
  39. package/tests/credentials.test.vitest.mts +127 -0
  40. package/tests/ghcr-archive.test.vitest.mts +439 -0
  41. package/tests/ghcr-config.test.vitest.mts +96 -0
  42. package/tests/ghcr-push.test.vitest.mts +526 -0
  43. package/tests/ghcr-registry.test.vitest.mts +206 -0
  44. package/tests/helpers/ghcr-fixtures.mts +373 -0
  45. package/tests/install-args.test.vitest.mts +152 -0
  46. package/tests/issue-labels.test.vitest.mts +15 -0
  47. package/tests/npm-scaffold.test.vitest.mts +479 -0
  48. package/tests/npm-setup.test.vitest.mts +196 -0
  49. package/tests/paths.test.vitest.mts +5 -0
  50. package/tests/prompt.test.vitest.mts +128 -0
  51. package/tests/release-prs.test.vitest.mts +10 -0
  52. package/tests/router.test.vitest.mts +108 -2
  53. package/tests/secret-store.test.vitest.mts +64 -3
  54. package/tests/secrets.test.vitest.mts +53 -0
  55. package/tests/security-alerts.test.vitest.mts +10 -0
  56. package/tests/server.test.vitest.mts +187 -7
  57. package/tests/smoke.test.vitest.mts +177 -0
  58. package/tests/system.test.vitest.mts +355 -0
  59. package/tests/verb-helpers.test.vitest.mts +46 -0
  60. package/.configs/fix-headers.json +0 -3
  61. package/.configs/vitest.config.mjs +0 -44
  62. package/.github/dependabot.yml +0 -111
  63. package/.github/workflows/branch-retention.yml +0 -44
  64. package/.github/workflows/bundle-size.yml +0 -52
  65. package/.github/workflows/ci.yml +0 -389
  66. package/.github/workflows/cla.yml +0 -66
  67. package/.github/workflows/codeql.yml +0 -87
  68. package/.github/workflows/dependabot-auto-merge.yml +0 -62
  69. package/.github/workflows/dependabot-recreate.yml +0 -67
  70. package/.github/workflows/dependency-review.yml +0 -46
  71. package/.github/workflows/feature-pr.yml +0 -75
  72. package/.github/workflows/hotfix-redirector.yml +0 -67
  73. package/.github/workflows/hotfixes-release.yml +0 -70
  74. package/.github/workflows/labeler.yml +0 -48
  75. package/.github/workflows/master-commit-audit.yml +0 -56
  76. package/.github/workflows/member-auto-merge.yml +0 -75
  77. package/.github/workflows/next-release.yml +0 -76
  78. package/.github/workflows/next-reset.yml +0 -50
  79. package/.github/workflows/pr-notify.yml +0 -44
  80. package/.github/workflows/pr-title-normalizer.yml +0 -52
  81. package/.github/workflows/provenance.yml +0 -51
  82. package/.github/workflows/publish.yml +0 -146
  83. package/.github/workflows/release-merge.yml +0 -104
  84. package/.github/workflows/release-notify.yml +0 -41
  85. package/.github/workflows/scorecard.yml +0 -64
  86. package/.github/workflows/stale.yml +0 -50
  87. package/.github/workflows/tag-health.yml +0 -70
  88. package/.github/workflows/update-major-version-tags.yml +0 -94
  89. package/.github/workflows/v4-bootstrap.yml +0 -115
  90. 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, constants) {
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, constants);
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, constants) {
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, constants);
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, constants) {
543
- this.code = optimizeExpr(this.code, names2, constants);
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, constants) {
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, constants))
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, constants) {
630
+ optimizeNames(names2, constants2) {
631
631
  var _a3;
632
- this.else = (_a3 = this.else) === null || _a3 === void 0 ? void 0 : _a3.optimizeNames(names2, constants);
633
- if (!(super.optimizeNames(names2, constants) || this.else))
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, constants);
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, constants) {
659
- if (!super.optimizeNames(names2, constants))
658
+ optimizeNames(names2, constants2) {
659
+ if (!super.optimizeNames(names2, constants2))
660
660
  return;
661
- this.iteration = optimizeExpr(this.iteration, names2, constants);
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, constants) {
698
- if (!super.optimizeNames(names2, constants))
697
+ optimizeNames(names2, constants2) {
698
+ if (!super.optimizeNames(names2, constants2))
699
699
  return;
700
- this.iterable = optimizeExpr(this.iterable, names2, constants);
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, constants) {
742
+ optimizeNames(names2, constants2) {
743
743
  var _a3, _b;
744
- super.optimizeNames(names2, constants);
745
- (_a3 = this.catch) === null || _a3 === void 0 ? void 0 : _a3.optimizeNames(names2, constants);
746
- (_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNames(names2, constants);
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, constants) {
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 = constants[n.str];
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 && constants[c.str] !== void 0);
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 validate2 = !$data && def.compile ? def.compile.call(it.self, schema, parentSchema, it) : def.validate;
1918
- const validateRef = useKeyword(gen, keyword, validate2);
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 = "", normalize) {
2283
- if (normalize !== false)
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 validate2 = makeValidate(this, this.scope.get());
2992
- this.scope.value(validateName, { ref: validate2 });
2993
- validate2.errors = null;
2994
- validate2.schema = sch.schema;
2995
- validate2.schemaEnv = sch;
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
- validate2.$async = true;
2997
+ validate3.$async = true;
2998
2998
  if (this.opts.code.source === true) {
2999
- validate2.source = { validateName, validateCode, scopeValues: gen._values };
2999
+ validate3.source = { validateName, validateCode, scopeValues: gen._values };
3000
3000
  }
3001
3001
  if (this.opts.unevaluated) {
3002
3002
  const { props, items } = schemaCxt;
3003
- validate2.evaluated = {
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 (validate2.source)
3010
- validate2.source.evaluated = (0, codegen_1.stringify)(validate2.evaluated);
3009
+ if (validate3.source)
3010
+ validate3.source.evaluated = (0, codegen_1.stringify)(validate3.evaluated);
3011
3011
  }
3012
- sch.validate = validate2;
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 normalize(uri, options) {
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(validate2, compare) {
6936
- return { validate: validate2, compare };
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 header = typeof string4 === "object" ? getcontenttype(string4) : string4;
7286
- if (typeof header !== "string") {
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 = header.indexOf(";");
7290
- var type = index !== -1 ? header.slice(0, index).trim() : header.trim();
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(header)) {
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 !== header.length) {
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 header;
7322
+ var header2;
7323
7323
  if (typeof obj.getHeader === "function") {
7324
- header = obj.getHeader("content-type");
7324
+ header2 = obj.getHeader("content-type");
7325
7325
  } else if (typeof obj.headers === "object") {
7326
- header = obj.headers && obj.headers["content-type"];
7326
+ header2 = obj.headers && obj.headers["content-type"];
7327
7327
  }
7328
- if (typeof header !== "string") {
7328
+ if (typeof header2 !== "string") {
7329
7329
  throw new TypeError("content-type header is missing from object");
7330
7330
  }
7331
- return header;
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 [owner, v] of Object.entries(c.owners)) {
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.${owner}: needs "read" and "write" credential refs`);
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.${owner}.installationId: must be a number`);
7460
+ throw new Error(`config.owners.${owner2}.installationId: must be a number`);
7392
7461
  }
7393
- owners[owner.toLowerCase()] = {
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 [owner, creds] of Object.entries(config2.owners)) {
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 "${owner}" ref must be "cred:<name>", got "${ref}"`);
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 { resolve: (ref) => read(credName(ref)) };
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 { resolve: (ref) => read(credName(ref)) };
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: () => 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 isObject(data) {
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 (isObject(o) === false)
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 (isObject(prot) === false)
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 [header] = tokensParts;
9336
- if (!header)
9448
+ const [header2] = tokensParts;
9449
+ if (!header2)
9337
9450
  return false;
9338
- const parsedHeader = JSON.parse(atob(header));
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 isObject2 = isObject;
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 (!isObject2(input)) {
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 isObject2 = isObject;
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 (!isObject2(input)) {
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 (!isObject(input)) {
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(header) {
16299
- if (!header) {
16411
+ function mediaTypeEssence(header2) {
16412
+ if (!header2) {
16300
16413
  return void 0;
16301
16414
  }
16302
16415
  try {
16303
- return import_content_type.default.parse(header).type;
16416
+ return import_content_type.default.parse(header2).type;
16304
16417
  } catch {
16305
- const essence = (header.split(";", 1)[0] ?? "").trim().toLowerCase();
16306
- if (essence === "" || header.slice(essence.length).includes(",")) {
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(header, options) {
18518
+ function parse4(header2, options) {
18406
18519
  const stopChar = options?.comma === true ? COMMA : 65536;
18407
- const len = header.length;
18408
- let index = skipOWS(header, options?.start ?? 0, len);
18520
+ const len = header2.length;
18521
+ let index = skipOWS(header2, options?.start ?? 0, len);
18409
18522
  const valueStart = index;
18410
- index = skipValue(header, index, len, stopChar);
18411
- const valueEnd = trailingOWS(header, valueStart, index);
18412
- const type = header.slice(valueStart, valueEnd).toLowerCase();
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(header, type, index, len, stopChar);
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(header, type, index, len, stopChar) {
18538
+ function parseParameters(header2, type, index, len, stopChar) {
18426
18539
  const parameters = new NullObject();
18427
18540
  parameter: while (index < len) {
18428
- if (header.charCodeAt(index) === stopChar)
18541
+ if (header2.charCodeAt(index) === stopChar)
18429
18542
  break;
18430
- index = skipOWS(header, index + 1, len);
18543
+ index = skipOWS(header2, index + 1, len);
18431
18544
  const keyStart = index;
18432
18545
  while (index < len) {
18433
- const code = header.charCodeAt(index);
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(header, keyStart, index);
18440
- const key2 = header.slice(keyStart, keyEnd).toLowerCase();
18441
- index = skipOWS(header, index + 1, len);
18442
- if (index < len && header.charCodeAt(index) === DQUOTE) {
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 = header.charCodeAt(index++);
18559
+ const code2 = header2.charCodeAt(index++);
18447
18560
  if (code2 === DQUOTE) {
18448
- index = skipValue(header, index, len, stopChar);
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 += header[index++];
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(header, index, len, stopChar);
18575
+ index = skipValue(header2, index, len, stopChar);
18463
18576
  if (parameters[key2] === void 0) {
18464
- const valueEnd = trailingOWS(header, valueStart, index);
18465
- parameters[key2] = header.slice(valueStart, valueEnd);
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(header, index, len) {
18596
+ function skipOWS(header2, index, len) {
18484
18597
  while (index < len) {
18485
- const char = header.charCodeAt(index);
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(header, start, end) {
18605
+ function trailingOWS(header2, start, end) {
18493
18606
  while (end > start) {
18494
- const char = header.charCodeAt(end - 1);
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 isObject2 = val !== null && typeof val === "object";
18523
- const hasToJSON = isObject2 && typeof val.toJSON === "function";
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, owner, maxRepos) {
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: owner, type: "all" }, maxRepos);
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, owner, repo, state, limit) {
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(owner, repo, "issue", it));
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, owner, repo, state, limit, includeDrafts) {
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(owner, repo, "pull", it));
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(owner, repo, type, it) {
19539
- return { repo: `${owner}/${repo}`, type, number: it.number, title: it.title, url: it.html_url, createdAt: it.created_at };
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, owner, repo) {
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(owner, repo, a) {
19776
+ function normalizeDependabot(owner2, repo, a) {
19647
19777
  const vuln = a.security_vulnerability;
19648
19778
  const out = {
19649
- repo: a.repository?.full_name ?? `${owner}/${repo}`,
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(owner, repo, a) {
19797
+ function normalizeCodeql(owner2, repo, a) {
19668
19798
  const out = {
19669
- repo: a.repository?.full_name ?? `${owner}/${repo}`,
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(owner, repo, a) {
19813
+ function normalizeSecret(owner2, repo, a) {
19684
19814
  const out = {
19685
- repo: a.repository?.full_name ?? `${owner}/${repo}`,
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, owner, repo, raws) => {
19870
+ const addAll = (type, owner2, repo, raws) => {
19741
19871
  const spec = SPECS[type];
19742
- for (const raw of raws) alerts.push(spec.normalize(owner, repo, raw));
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 [owner, repo] = row.repo.includes("/") ? row.repo.split("/") : [input.owner, row.repo];
19831
- const t = await classifyRepo(octokit, budget, owner, repo);
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, owner, repo, path, ref) {
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, owner, repo, args.changelogPath, args.head) ?? "";
19980
- const readme = await getText(octokit, budget, owner, repo, "README.md", args.head);
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, owner, repo, ref, paths) {
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: `${owner}/${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, owner, repo, pull.head.ref, paths);
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 fail = (msg) => {
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) fail(`"${field}" is empty \u2014 pass at least one label name`);
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()) fail(`"${field}" must be a label name or a list of label names`);
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) fail(`"${field}" has ${list.length} labels \u2014 at most ${MAX_LABELS} per call`);
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)) fail("arguments must be an object");
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) fail(`unknown argument(s): ${unknown2.join(", ")} \u2014 expected ${[...INPUT_KEYS].join(", ")}`);
20141
- if (typeof a.owner !== "string" || !a.owner.trim()) fail('"owner" must be a non-empty string');
20142
- if (typeof a.repo !== "string" || !a.repo.trim()) fail('"repo" must be a non-empty string');
20143
- if (typeof a.issue_number !== "number" || !Number.isInteger(a.issue_number) || a.issue_number <= 0) fail('"issue_number" must be a positive integer');
20144
- if (a.create_missing !== void 0 && typeof a.create_missing !== "boolean") fail('"create_missing" must be a boolean');
20145
- if (a.color !== void 0 && (typeof a.color !== "string" || !/^#?[0-9a-f]{6}$/i.test(a.color))) fail('"color" must be a 6-digit hex color, e.g. "ededed"');
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)) fail('"swap" must be an object { prefix, to }');
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) fail(`unknown "swap" key(s): ${extra.join(", ")} \u2014 expected prefix, to`);
20156
- if (typeof sw.prefix !== "string" || !sw.prefix) fail('"swap.prefix" is required');
20157
- if (sw.to === void 0) fail('"swap.to" needs at least one label');
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
- fail('"set" replaces the whole label set and cannot be combined with add/remove/swap');
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) fail("nothing to do \u2014 pass add, remove, swap or set");
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) fail(`${addSide.length} labels to add \u2014 at most ${MAX_LABELS} per call`);
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) fail(`label(s) both added and removed: ${clash.join(", ")}`);
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 = `${owner}/${repo}#${issue_number}`;
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
- fail(`label(s) not found in ${owner}/${repo}: ${missing.join(", ")} \u2014 nothing was changed. Check the spelling (list_label), or pass create_missing: true to create them.`);
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, owner, repo, pullNumber, headSha, verbosity, logLines, fetchFailedJobLogs) {
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, owner, repo, pullNumber, headSha, opts = {}) {
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
- owner,
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
- let owner = extractOwner(args);
20496
- if (!owner && rule.mode === "read" && this.config.defaultOwner && this.config.owners[this.config.defaultOwner]) {
20497
- owner = this.config.defaultOwner.toLowerCase();
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 (!owner) {
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[owner];
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 "${owner}" \u2014 add it to config.owners + creds`);
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" ? `${owner}/${args.repo}` : owner;
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, owner, token) : rule.augment ? await this.dispatchAugmented(tool, args, owner, token) : await this.upstream.forward(tool, args, token);
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
- async dispatchCustom(tool, args, owner, token) {
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(owner, reportArgs.repo, token));
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(owner, repo, token) {
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, owner, token) {
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, owner, repo, p.number, p.head.sha, {
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(owner, repo, token)
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(header, payload) {
21295
- return `${base64encodeJSON(header)}.${base64encodeJSON(payload)}`;
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 header = { alg: "RS256", typ: "JWT" };
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(header, payload);
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(owner) {
21998
- const installationId = this.config.owners[owner]?.installationId;
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
  }