canopycms-cdk 0.0.67-int.90 → 0.0.67

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 (48) hide show
  1. package/dist/constructs/asset-support.d.ts +137 -269
  2. package/dist/constructs/asset-support.js +275 -455
  3. package/dist/constructs/cms-distribution.d.ts +23 -40
  4. package/dist/constructs/cms-distribution.js +61 -88
  5. package/dist/constructs/cms-service.d.ts +38 -60
  6. package/dist/constructs/cms-service.js +246 -390
  7. package/dist/constructs/lambda-execution-role.d.ts +23 -39
  8. package/dist/constructs/lambda-execution-role.js +37 -60
  9. package/dist/index.js +0 -2
  10. package/dist/worker.d.ts +0 -2
  11. package/dist/worker.js +0 -2
  12. package/lambda/asset-transform/dist/handler.js +33 -49
  13. package/lambda/asset-transform/dist/node_modules/.package-lock.json +38 -38
  14. package/lambda/asset-transform/dist/node_modules/@img/sharp-libvips-linux-arm64/README.md +1 -1
  15. package/lambda/asset-transform/dist/node_modules/@img/sharp-libvips-linux-arm64/lib/glib-2.0/include/glibconfig.h +2 -2
  16. package/lambda/asset-transform/dist/node_modules/@img/sharp-libvips-linux-arm64/lib/{libvips-cpp.so.8.18.6 → libvips-cpp.so.8.18.7} +0 -0
  17. package/lambda/asset-transform/dist/node_modules/@img/sharp-libvips-linux-arm64/package.json +2 -2
  18. package/lambda/asset-transform/dist/node_modules/@img/sharp-libvips-linux-arm64/versions.json +11 -11
  19. package/lambda/asset-transform/dist/node_modules/@img/sharp-linux-arm64/index.cjs +1 -1
  20. package/lambda/asset-transform/dist/node_modules/@img/sharp-linux-arm64/lib/{sharp-linux-arm64-0.35.4.node → sharp-linux-arm64-0.35.5.node} +0 -0
  21. package/lambda/asset-transform/dist/node_modules/@img/sharp-linux-arm64/package.json +2 -2
  22. package/lambda/asset-transform/dist/node_modules/@img/sharp-wasm32/index.cjs +1 -1
  23. package/lambda/asset-transform/dist/node_modules/@img/sharp-wasm32/lib/sharp-wasm32-0.35.5.node.js +1 -0
  24. package/lambda/asset-transform/dist/node_modules/@img/sharp-wasm32/lib/{sharp-wasm32-0.35.4.node.wasm → sharp-wasm32-0.35.5.node.wasm} +0 -0
  25. package/lambda/asset-transform/dist/node_modules/@img/sharp-wasm32/package.json +1 -1
  26. package/lambda/asset-transform/dist/node_modules/@img/sharp-wasm32/versions.json +7 -7
  27. package/lambda/asset-transform/dist/node_modules/sharp/dist/index.d.cts +2 -2
  28. package/lambda/asset-transform/dist/node_modules/sharp/dist/index.d.mts +3 -3
  29. package/lambda/asset-transform/dist/node_modules/sharp/dist/is.cjs +8 -2
  30. package/lambda/asset-transform/dist/node_modules/sharp/dist/is.mjs +8 -2
  31. package/lambda/asset-transform/dist/node_modules/sharp/dist/operation.cjs +2 -2
  32. package/lambda/asset-transform/dist/node_modules/sharp/dist/operation.mjs +2 -2
  33. package/lambda/asset-transform/dist/node_modules/sharp/dist/output.cjs +1 -0
  34. package/lambda/asset-transform/dist/node_modules/sharp/dist/output.mjs +1 -0
  35. package/lambda/asset-transform/dist/node_modules/sharp/dist/resize.cjs +14 -14
  36. package/lambda/asset-transform/dist/node_modules/sharp/dist/resize.mjs +14 -14
  37. package/lambda/asset-transform/dist/node_modules/sharp/dist/sharp.cjs +3 -3
  38. package/lambda/asset-transform/dist/node_modules/sharp/dist/sharp.mjs +3 -3
  39. package/lambda/asset-transform/dist/node_modules/sharp/lib/index.d.ts +2 -2
  40. package/lambda/asset-transform/dist/node_modules/sharp/package.json +35 -35
  41. package/lambda/asset-transform/dist/node_modules/sharp/src/binding.gyp +1 -1
  42. package/lambda/asset-transform/dist/node_modules/sharp/src/common.cc +4 -4
  43. package/lambda/asset-transform/dist/node_modules/sharp/src/common.h +2 -2
  44. package/lambda/asset-transform/dist/node_modules/sharp/src/pipeline.cc +31 -6
  45. package/lambda/asset-transform/dist/node_modules/sharp/src/utilities.cc +1 -1
  46. package/package.json +4 -4
  47. package/worker/dist/index.js +251 -364
  48. package/lambda/asset-transform/dist/node_modules/@img/sharp-wasm32/lib/sharp-wasm32-0.35.4.node.js +0 -1
@@ -12515,18 +12515,18 @@ var init_config2 = __esm({
12515
12515
  onAccountClick: external_exports.function().returns(external_exports.void()).optional(),
12516
12516
  onLogoutClick: external_exports.function().returns(external_exports.void()).optional(),
12517
12517
  // Optional: custom account component (e.g., Clerk's UserButton)
12518
- AccountComponent: external_exports.custom().optional()
12518
+ AccountComponent: external_exports.custom().optional(),
12519
+ // Optional: provider sign-in UI for signed-out users (see EditorSignInProps)
12520
+ SignInComponent: external_exports.custom().optional()
12519
12521
  });
12520
12522
  CanopyConfigSchema = external_exports.object({
12521
12523
  media: mediaSchema.optional(),
12522
- // No outer .optional() here: defaultBranchAccessSchema/defaultPathAccessSchema already
12523
- // have .default('deny'), so the field is optional on input but always resolves to
12524
- // 'allow'/'deny' (never undefined) on output. An outer .optional() would short-circuit
12525
- // before the inner default runs, defeating the fail-closed default (SCH-M1).
12526
- // defaultPathAccessSchema's union keeps the same rule: the .default('deny') sits on the
12527
- // OUTER union, not inside defaultPathAccessLevelsSchema, so an omitted top-level field
12528
- // still resolves to 'deny' and an omitted level inside the object form stays undefined
12529
- // (resolved to 'deny' at read time by resolveDefaultPathAccess).
12524
+ // No outer .optional(): both schemas already .default('deny'), so the field is optional on
12525
+ // input but always resolves to 'allow'/'deny' (never undefined) on output — an outer
12526
+ // .optional() would short-circuit before the inner default runs, defeating the fail-closed
12527
+ // default (SCH-M1). defaultPathAccessSchema's .default('deny') sits on the OUTER union, not
12528
+ // inside defaultPathAccessLevelsSchema, so an omitted level in the object form stays
12529
+ // undefined (resolved to 'deny' at read time by resolveDefaultPathAccess).
12530
12530
  defaultBranchAccess: defaultBranchAccessSchema,
12531
12531
  defaultPathAccess: defaultPathAccessSchema,
12532
12532
  // .optional() deliberately defeats defaultBaseBranchSchema's .default('main'):
@@ -12550,16 +12550,11 @@ var init_config2 = __esm({
12550
12550
  allowNetworkRemoteInProd: external_exports.boolean().optional(),
12551
12551
  settingsBranch: external_exports.string().optional(),
12552
12552
  autoCreateSettingsPR: external_exports.boolean().optional(),
12553
- // Deliberately `deploymentNameSchema.optional()`, NOT `deploymentNameSchema` alone.
12554
- // deploymentNameSchema.default('prod') would make `parse(undefined)` resolve to
12555
- // the literal 'prod' instead of staying `undefined` — collapsing the
12556
- // env > config > modeDefault precedence chain that resolveDeploymentName
12557
- // (operating-mode/deployment-name.ts) implements: config would then always
12558
- // "win" over modeDefault (masking dev's real default of 'local'), and the env
12559
- // var would just be racing config's baked-in 'prod' instead of a true absence.
12560
- // `.optional()` here is what keeps an omitted deploymentName reaching
12561
- // resolveDeploymentName as `undefined`, so it can fall through to modeDefault.
12562
- // Do not "fix" this by removing `.optional()`.
12553
+ // `.optional()`, NOT bare `deploymentNameSchema`: its own `.default('prod')` would make
12554
+ // `parse(undefined)` resolve to 'prod' instead of staying `undefined`, collapsing the
12555
+ // env > config > modeDefault precedence chain `resolveDeploymentName`
12556
+ // (operating-mode/deployment-name.ts) implements — config would always "win" over
12557
+ // modeDefault, masking dev's real default of 'local'. Do not remove `.optional()`.
12563
12558
  deploymentName: deploymentNameSchema.optional(),
12564
12559
  contentRoot: contentRootSchema.default("content"),
12565
12560
  sourceRoot: sourceRootSchema.optional(),
@@ -48211,15 +48206,11 @@ var DebugLogger = class {
48211
48206
  return LOG_LEVELS[level] >= LOG_LEVELS[minLevel];
48212
48207
  }
48213
48208
  /**
48214
- * The ISO-8601 timestamp leads the line BARE - not wrapped in brackets as it
48215
- * once was. `CANOPYCMS_DEBUG=true` can be set on the EC2 worker, whose stdout
48216
- * appends to /var/log/canopy-worker/worker.log; the CloudWatch agent's
48217
- * `multi_line_start_pattern` matches the timestamp at the START of a line
48218
- * (see worker/log.ts's INVARIANT and cms-service.ts's agent config), so a
48219
- * leading `[` meant every debug line was silently folded into the previous
48220
- * event instead of starting its own. The bracketed `[prefix:category]` and
48221
- * `[level]` fields still follow, so the human-readable shape is unchanged
48222
- * apart from those two characters.
48209
+ * The ISO-8601 timestamp leads the line BARE, never bracketed. `CANOPYCMS_DEBUG=true` can
48210
+ * be set on the EC2 worker, whose stdout appends to /var/log/canopy-worker/worker.log, and
48211
+ * the CloudWatch agent's `multi_line_start_pattern` matches a timestamp at the START of a
48212
+ * line (see worker/log.ts's INVARIANT and cms-service.ts's agent config): any leading
48213
+ * character folds the line into the previous event instead of starting its own.
48223
48214
  */
48224
48215
  formatMessage(level, category, message) {
48225
48216
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString();
@@ -48252,15 +48243,9 @@ var DebugLogger = class {
48252
48243
  throw new Error(errorMsg);
48253
48244
  }
48254
48245
  }
48255
- /**
48256
- * Start timing an operation
48257
- */
48258
48246
  time(label) {
48259
48247
  this.timers.set(label, Date.now());
48260
48248
  }
48261
- /**
48262
- * End timing an operation and log the duration
48263
- */
48264
48249
  timeEnd(category, label) {
48265
48250
  const start = this.timers.get(label);
48266
48251
  if (start === void 0) {
@@ -48272,9 +48257,6 @@ var DebugLogger = class {
48272
48257
  this.debug(category, `${label} completed`, { durationMs: duration });
48273
48258
  return duration;
48274
48259
  }
48275
- /**
48276
- * Wrap an async function with automatic timing
48277
- */
48278
48260
  async timed(category, label, fn) {
48279
48261
  this.time(label);
48280
48262
  try {
@@ -48614,7 +48596,7 @@ async function moveToCorrupt(taskDir, sourceDir, fileName, reason, logger3) {
48614
48596
  }
48615
48597
  }
48616
48598
 
48617
- // ../canopycms/src/worker/task-queue.ts
48599
+ // ../canopycms/src/task-queue/cms-task-queue.ts
48618
48600
  var debugLogger = createDebugLogger({ prefix: "TaskQueue" });
48619
48601
  var cmsTaskQueueLogger = {
48620
48602
  debug(message, data2) {
@@ -48915,7 +48897,7 @@ var RESERVED_SETTINGS_BRANCH_PREFIX = "canopycms-settings-";
48915
48897
  // ../canopycms/src/worker/cms-worker.ts
48916
48898
  init_deployment_name();
48917
48899
 
48918
- // ../canopycms/src/worker/worker-status.ts
48900
+ // ../canopycms/src/task-queue/worker-status.ts
48919
48901
  import path4 from "node:path";
48920
48902
  var WORKER_STATUS_FILE = "worker-status.json";
48921
48903
  async function writeWorkerStatus(taskDir, report) {
@@ -49068,15 +49050,12 @@ var BranchRegistry = class {
49068
49050
  return await this.regenerate();
49069
49051
  }
49070
49052
  /**
49071
- * Returns a single branch by name. Uses cache if available.
49053
+ * Returns a single branch by name, from the cached snapshot when it is fresh.
49072
49054
  *
49073
- * Suspicious-miss backstop: if `name` isn't in the (possibly cached) list,
49074
- * that's a signal the snapshot may be stale in the specific direction where
49075
- * a branch that now exists is missing from it - force one fresh
49076
- * regeneration and re-search before giving up, throttled per instance so a
49077
- * genuinely-absent branch costs at most one extra scan per throttle window.
49078
- * This bounds durable window-E staleness for the "branch exists but
49079
- * snapshot predates it" direction; see the class doc comment.
49055
+ * Suspicious-miss backstop: a `name` missing from the list is the signal that
49056
+ * the snapshot may predate a branch that now exists, so force one fresh
49057
+ * regeneration and re-search before giving up. Throttled per instance, so a
49058
+ * genuinely-absent branch costs at most one extra scan per window.
49080
49059
  */
49081
49060
  async get(name) {
49082
49061
  const branches = await this.list();
@@ -49093,14 +49072,11 @@ var BranchRegistry = class {
49093
49072
  return true;
49094
49073
  }
49095
49074
  /**
49096
- * Marks the cache as stale for every process sharing this root by bumping
49097
- * the cross-process generation marker, then eager-regenerates on this host
49098
- * (see class doc comment for why). The bump must succeed: a swallowed
49099
- * failure here would leave the registry stale indefinitely with no
49100
- * bounding backstop, unlike a failed eager regen below, which only forgoes
49101
- * closing window (E) early - the bump alone already restored correctness
49102
- * for every future reader. A failed eager regen therefore must not fail the
49103
- * caller's save/delete.
49075
+ * Marks the cache stale for every process sharing this root by bumping the
49076
+ * marker, then eager-regenerates on this host. The bump must succeed —
49077
+ * swallowing that failure leaves the registry stale indefinitely with no
49078
+ * backstop. A failed eager regen must NOT fail the caller's save/delete: the
49079
+ * bump alone already restored correctness for every future reader.
49104
49080
  */
49105
49081
  async invalidate() {
49106
49082
  await bumpResourceGeneration(this.root, RESOURCE, { mustSucceed: true });
@@ -49117,9 +49093,8 @@ var BranchRegistry = class {
49117
49093
  /**
49118
49094
  * Scans branch directories and rebuilds the cache, deduping concurrent
49119
49095
  * callers on this instance to a single underlying scan. Never loops waiting
49120
- * for the snapshot's embedded token to match the marker - under a bump
49121
- * storm that could livelock; a caller that wants the very latest state
49122
- * after a concurrent bump should call list() again after this resolves.
49096
+ * for the embedded token to match the marker — that livelocks under a bump
49097
+ * storm; a caller wanting the very latest state calls list() again.
49123
49098
  */
49124
49099
  async regenerate() {
49125
49100
  if (this.regenInFlight) return this.regenInFlight;
@@ -49371,16 +49346,13 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
49371
49346
  this.baseRoot = baseRoot;
49372
49347
  this.settleMs = options?.settleMs;
49373
49348
  }
49374
- /**
49375
- * Load branch metadata without requiring baseRoot.
49376
- * Use this for read-only access (e.g., in registry scanning or loadBranchContext).
49377
- */
49349
+ /** Read-only load, needing no baseRoot: registry scanning, loadBranchContext. */
49378
49350
  static async loadOnly(branchRoot) {
49379
49351
  return readBranchMetadataFile(branchRoot);
49380
49352
  }
49381
49353
  /**
49382
- * Get a BranchMetadataFileManager instance configured for registry invalidation.
49383
- * Use this in API handlers to ensure registry cache is invalidated on updates.
49354
+ * An instance wired for registry invalidation. API handlers use this, so the
49355
+ * registry cache is invalidated on update.
49384
49356
  */
49385
49357
  static get(branchRoot, baseRoot, options) {
49386
49358
  return new _BranchMetadataFileManager(branchRoot, baseRoot, options);
@@ -49399,18 +49371,16 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
49399
49371
  }
49400
49372
  }
49401
49373
  /**
49402
- * Write branch.json via the shared OCC helper, with the schemaVersion
49403
- * default applied here (payload shaping stays branch-metadata's concern).
49374
+ * Write branch.json via the shared OCC helper, applying the schemaVersion
49375
+ * default here since payload shaping is branch-metadata's concern.
49404
49376
  *
49405
- * Throws the helper's raw {@link OccWriteConflictError} so the surrounding
49406
- * {@link withOccRetry} in save() recognizes and retries it; translation to
49407
- * the public `BranchMetadataConflictError` contract happens at the save()
49408
- * boundary, after retries are exhausted (translating earlier would make
49409
- * withOccRetry's default predicate miss it, since it only recognizes the
49410
- * raw error type).
49377
+ * Throws the helper's raw {@link OccWriteConflictError}, which is what the
49378
+ * surrounding {@link withOccRetry} in save() recognizes and retries;
49379
+ * translating to the public `BranchMetadataConflictError` earlier than the
49380
+ * save() boundary would make withOccRetry's predicate miss it.
49411
49381
  *
49412
- * branch-metadata historically writes WITH a trailing newline, unlike
49413
- * comment-store; `trailingNewline: true` preserves that.
49382
+ * branch.json is written WITH a trailing newline (`trailingNewline: true`),
49383
+ * unlike comments.json.
49414
49384
  */
49415
49385
  async write(meta, expectedVersion) {
49416
49386
  const payload2 = {
@@ -49424,28 +49394,22 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
49424
49394
  });
49425
49395
  }
49426
49396
  /**
49427
- * Run a save cycle under the full lock + OCC-retry stack described in the
49428
- * class doc comment. A conflict that survives every retry surfaces as the
49429
- * public `BranchMetadataConflictError`.
49397
+ * Run a save cycle under the full lock + OCC-retry stack (class doc). A
49398
+ * conflict surviving every retry surfaces as `BranchMetadataConflictError`.
49430
49399
  *
49431
- * Guards against a phantom-resurrection race with branch deletion: a
49432
- * caller's branchContext can be resolved BEFORE a concurrent
49433
- * deleteBranchHandler removes the branch directory, but this save() call
49434
- * only reaches here (and its own mkdir({recursive:true}) inside write())
49435
- * AFTER the removal. Without this check, that mkdir would silently
49436
- * recreate `.canopy-meta/` (and this save would recreate branch.json from
49437
- * defaults) inside a directory tree that no longer exists anywhere else --
49438
- * a registry entry with no clone behind it. Checking BEFORE the lock stack
49439
- * (rather than after) fails fast without paying for a lock acquisition on
49440
- * a doomed save.
49400
+ * The stat guards a phantom-resurrection race with branch deletion: a
49401
+ * caller's branchContext can resolve BEFORE a concurrent deleteBranchHandler
49402
+ * removes the branch directory, while the save reaches here after it, and
49403
+ * write()'s `mkdir({recursive:true})` would then recreate `.canopy-meta/` and
49404
+ * branch.json from defaults inside a tree nothing else refers to — a registry
49405
+ * entry with no clone behind it. It runs BEFORE the lock stack so a doomed
49406
+ * save fails fast instead of paying for a lock.
49441
49407
  *
49442
- * Residual window (accepted): a save that passes this check can still
49443
- * race a `rm` that starts moments later and is still mid-flight when this
49444
- * save's write lands, resurrecting the tree. Closing that fully would
49445
- * need a tombstone OUTSIDE the tree being removed -- the lockfile
49446
- * (`withOccFileLock`) this save takes next lives INSIDE `branchRoot`, so
49447
- * it cannot itself provide a wider guarantee than "the directory existed
49448
- * a moment ago."
49408
+ * Accepted residual window: a save that passes the check can still race a
49409
+ * `rm` that starts moments later and is mid-flight when the write lands.
49410
+ * Closing that needs a tombstone OUTSIDE the tree being removed, and the
49411
+ * lockfile taken next lives INSIDE `branchRoot`, so it can promise no more
49412
+ * than "the directory existed a moment ago".
49449
49413
  */
49450
49414
  async save(incoming) {
49451
49415
  try {
@@ -49489,9 +49453,9 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
49489
49453
  createdAt: existing?.branch.createdAt ?? defaults.createdAt,
49490
49454
  // Fork point is recorded once at creation; later saves must not move it
49491
49455
  baseBranch: existing?.branch.baseBranch ?? incoming.branch?.baseBranch,
49492
- // Always stamped fresh — the spreads above would otherwise let
49493
- // the existing (creation-time) value win forever, freezing the
49494
- // timestamp the editor's Branches panel sorts and displays by
49456
+ // Always stamped fresh; the spreads above would otherwise let
49457
+ // the creation-time value win forever, freezing the timestamp
49458
+ // the editor's Branches panel sorts and displays by
49495
49459
  updatedAt: now
49496
49460
  }
49497
49461
  };
@@ -49511,9 +49475,7 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
49511
49475
  await this.invalidateRegistry();
49512
49476
  return saved;
49513
49477
  }
49514
- /**
49515
- * Invalidates the registry cache so next list() call regenerates from branch.json files.
49516
- */
49478
+ /** Invalidate the registry cache so the next list() regenerates. */
49517
49479
  async invalidateRegistry() {
49518
49480
  const registry2 = new BranchRegistry(this.baseRoot);
49519
49481
  await registry2.invalidate();
@@ -49695,12 +49657,11 @@ function provisioningLockOptions(lockPath, retries, onCompromised) {
49695
49657
  realpath: false,
49696
49658
  retries,
49697
49659
  stale: 3e4,
49698
- // proper-lockfile invokes this from inside its refresh timer, so ANY throw
49699
- // escaping here is an uncaught exception that kills the process -- the very
49700
- // failure the handler exists to prevent. Call sites are told not to throw
49701
- // (see OnLockCompromised); this makes it structural rather than a
49702
- // convention, and also covers the LOGGER throwing: under `CI=true`,
49703
- // vitest's `onConsoleLog` turns any console write into a throw.
49660
+ // proper-lockfile invokes this from inside its refresh timer, so ANY throw escaping here is
49661
+ // an uncaught exception that kills the process. Call sites are told not to throw (see
49662
+ // OnLockCompromised); this makes it structural rather than a convention, and also covers the
49663
+ // LOGGER throwing -- under `CI=true`, vitest's `onConsoleLog` turns a console write into a
49664
+ // throw.
49704
49665
  onCompromised: (err) => {
49705
49666
  try {
49706
49667
  if (onCompromised) {
@@ -50104,11 +50065,9 @@ async function updateBranchMetadata(ctx, task, result) {
50104
50065
  const updates = {
50105
50066
  name: branch,
50106
50067
  syncStatus: "synced",
50107
- // save()'s merge only overwrites keys present in this update (see
50108
- // BranchMetadataFileManager.save()'s spread order) -- a prior
50109
- // syncFailureReason from an earlier failed attempt would otherwise
50110
- // survive forever past this successful sync. Explicitly clear it,
50111
- // same pattern as rebaseFailure's PR-W2 M2 clearing elsewhere.
50068
+ // save()'s merge only overwrites keys present in this update, so a prior
50069
+ // syncFailureReason would survive this successful sync forever unless
50070
+ // cleared explicitly (same pattern as rebaseFailure elsewhere).
50112
50071
  syncFailureReason: void 0
50113
50072
  };
50114
50073
  if (result.prUrl) updates.pullRequestUrl = result.prUrl;
@@ -50152,9 +50111,9 @@ async function updateBranchMetadataOnFailure(ctx, task, error2) {
50152
50111
  async function pushBranchToGitHub(ctx, branch) {
50153
50112
  const git = simpleGit({
50154
50113
  baseDir: ctx.remoteGitPath,
50155
- // DEP-H1: kill the git process if it produces no output for
50156
- // taskTimeoutMs (network stall, credential prompt) instead of letting
50157
- // it hang past the task timeout.
50114
+ // DEP-H1: kill the git process if it produces no output for taskTimeoutMs
50115
+ // (network stall, credential prompt) instead of letting it hang past the
50116
+ // task timeout.
50158
50117
  timeout: { block: ctx.taskTimeoutMs }
50159
50118
  });
50160
50119
  git.env(gitNetworkChildEnv());
@@ -50441,10 +50400,10 @@ async function rebaseOneBranch(ctx, branchDir, branchPath) {
50441
50400
  }
50442
50401
  const branchGit = simpleGit({
50443
50402
  baseDir: branchPath,
50444
- // Keep git non-interactive during rebase/merge so it never blocks on an editor.
50445
- // simple-git >=3.32 blocks setting core.editor unless explicitly opted in; the
50446
- // value here is a hardcoded literal ("true", the shell no-op), not user input,
50447
- // so enabling allowUnsafeEditor carries no injection risk.
50403
+ // Keep git non-interactive during rebase/merge so it never blocks on an
50404
+ // editor. simple-git >=3.32 requires opting in to set core.editor; the
50405
+ // value is a hardcoded literal ("true", the shell no-op), not user input,
50406
+ // so allowUnsafeEditor carries no injection risk here.
50448
50407
  config: ["core.editor=true"],
50449
50408
  unsafe: { allowUnsafeEditor: true }
50450
50409
  });
@@ -50575,8 +50534,8 @@ async function rebaseOneBranch(ctx, branchDir, branchPath) {
50575
50534
  name: branchDir,
50576
50535
  conflictStatus: hadConflicts ? "conflicts-detected" : "clean",
50577
50536
  conflictFiles: conflictIdsDeduped,
50578
- // PR-W2: the cycle completed successfully -- clear any prior
50579
- // failure record regardless of conflict outcome.
50537
+ // The cycle completed, so clear any prior failure record regardless
50538
+ // of the conflict outcome.
50580
50539
  rebaseFailure: void 0
50581
50540
  }
50582
50541
  });
@@ -50873,18 +50832,14 @@ async function refreshBaseBranchWorkspace(ctx) {
50873
50832
  const baseGit = simpleGit({
50874
50833
  baseDir: basePath37,
50875
50834
  // Keep git non-interactive during the merge so it never blocks on an
50876
- // editor. simple-git >=3.32 blocks setting core.editor unless
50877
- // explicitly opted in; the value here is a hardcoded literal
50878
- // ("true", the shell no-op), not user input, so enabling
50879
- // allowUnsafeEditor carries no injection risk (same as the rebase
50880
- // loop's git config below).
50835
+ // editor. simple-git >=3.32 requires opting in to set core.editor; the
50836
+ // value is a hardcoded literal ("true", the shell no-op), not user input,
50837
+ // so allowUnsafeEditor carries no injection risk here.
50881
50838
  config: ["core.editor=true"],
50882
50839
  unsafe: { allowUnsafeEditor: true },
50883
- // DEP-H1: a hung fetch/merge against this EFS-backed clone would
50884
- // stall the sync loop forever (scheduleLoop only reschedules after
50885
- // completion). The block timeout is inactivity-based, so a
50886
- // slow-but-flowing transfer is unaffected -- same as syncGit()'s
50887
- // remote handle.
50840
+ // DEP-H1: a hung fetch/merge against this EFS-backed clone would stall
50841
+ // the sync loop forever (scheduleLoop only reschedules after completion).
50842
+ // Inactivity-based, so a slow-but-flowing transfer is unaffected.
50888
50843
  timeout: { block: ctx.taskTimeoutMs }
50889
50844
  });
50890
50845
  const status = await baseGit.status();
@@ -50957,12 +50912,10 @@ var CmsWorker = class {
50957
50912
  this.contentRoot = config.contentRoot ?? "content";
50958
50913
  }
50959
50914
  /**
50960
- * Lazily get (and initialize if necessary) this worker's self-reported
50961
- * status object (PR-W1). Normally set once, up front, at the top of
50962
- * start(). The lazy fallback here covers two cases: (1) something in
50963
- * start() reaching a status-write point before that normal init runs
50964
- * (defensive -- see start()'s catch), and (2) unit tests that exercise
50965
- * syncGit()/processTaskQueue() directly without calling start() first.
50915
+ * This worker's self-reported status object, initialized on first use.
50916
+ * Normally set at the top of start(); the lazy fallback covers something in
50917
+ * start() reaching a status-write point before that (see start()'s catch) and
50918
+ * unit tests driving syncGit()/processTaskQueue() without calling start().
50966
50919
  */
50967
50920
  ensureStatusReport() {
50968
50921
  if (!this.statusReport) {
@@ -50975,28 +50928,23 @@ var CmsWorker = class {
50975
50928
  * Resolve this deployment's settings branch, throwing if the infra-stamped
50976
50929
  * deployment name is not a valid git ref component.
50977
50930
  *
50978
- * Routed through the shared resolver, not a local `?? 'prod'`: it is the
50931
+ * Routed through the shared resolver, not a local `?? 'prod'`: that is the
50979
50932
  * single definition of the env > config > mode-default precedence the Lambda
50980
- * already follows, and the only place the resolved value is validated.
50981
- * Without it the worker could silently own a different settings branch than
50982
- * the Lambda writing to the same workspace -- and pushSettingsBranches would
50983
- * then report the real branch as foreign and never push it.
50933
+ * follows, and the only place the resolved value is validated. Without it the
50934
+ * worker can silently own a different settings branch than the Lambda writing
50935
+ * to the same workspace, and pushSettingsBranches then reports the real branch
50936
+ * as foreign and never pushes it.
50984
50937
  *
50985
- * DEFERRED out of the constructor deliberately, and this is the whole point
50986
- * of the method existing. Resolving there made the throw land during `new
50987
- * CmsWorker(...)` (canopycms-cdk/worker/index.ts constructs before it calls
50988
- * start()), which is BEFORE the only code that writes `lastFatalError` --
50989
- * start()'s catch. The result was billed as "a loud startup exit" but was
50990
- * nothing of the kind: with systemd `Type=simple` + `Restart=always` and no
50991
- * cfn-signal, an invalid deployment name produced an invisible ~5s
50992
- * crash-loop that `cdk deploy` reported as success while the admin panel
50993
- * showed the worker as 'absent' with no fatal error to explain it. Resolving
50994
- * inside start()'s try block instead means the same throw is recorded to
50995
- * worker-status.json and surfaces in the admin panel.
50938
+ * DEFERRED out of the constructor deliberately, which is the whole point of
50939
+ * the method existing: canopycms-cdk/worker/index.ts constructs the worker
50940
+ * before calling start(), so a throw during `new CmsWorker(...)` lands BEFORE
50941
+ * the only code that writes `lastFatalError` -- start()'s catch. Under
50942
+ * systemd `Type=simple` + `Restart=always` with no cfn-signal, that is an
50943
+ * invisible ~5s crash-loop that `cdk deploy` reports as success while the
50944
+ * admin panel shows the worker 'absent' with no fatal error to explain it.
50996
50945
  *
50997
- * Lazy rather than start()-only so the value is still available to unit tests
50998
- * that drive pushSettingsBranches() directly without calling start() -- the
50999
- * same reason ensureStatusReport() above is shaped this way. Idempotent: the
50946
+ * Lazy rather than start()-only so unit tests driving pushSettingsBranches()
50947
+ * still see the value, exactly as ensureStatusReport() above. Idempotent: the
51000
50948
  * resolver is pure, so a later call returns the identical string.
51001
50949
  */
51002
50950
  ensureSettingsBranch() {
@@ -51009,15 +50957,11 @@ var CmsWorker = class {
51009
50957
  * Build the {@link WorkerContext} handed to the extracted clusters
51010
50958
  * (task-runner.ts, git-sync.ts, rebase.ts, history-rewrite.ts).
51011
50959
  *
51012
- * Built FRESH on every call rather than once in the constructor, and the
51013
- * instance-backed members are functions rather than copied values. Both
51014
- * choices exist for the same reason: the test suite drives this class by
51015
- * replacing `octokit` and `buildGitHubUrl` ON THE INSTANCE, by setting
51016
- * `running` directly, and by subclassing to override the two rebase test
51017
- * hooks. A context that captured any of those at construction time would hand
51018
- * the extracted code the pre-test value -- which for `buildGitHubUrl` means a
51019
- * test's push going to github.com for real instead of its local fixture repo.
51020
- * See WorkerContext's doc comment for the full list.
50960
+ * Built FRESH on every call, with instance-backed members as functions rather
50961
+ * than copied values. See WorkerContext's INVARIANT: a context that captured
50962
+ * any of them at construction time would hand the extracted code the pre-test
50963
+ * value, which for `buildGitHubUrl` means a test's push going to github.com
50964
+ * for real instead of its local fixture repo.
51021
50965
  */
51022
50966
  ctx() {
51023
50967
  return {
@@ -51072,9 +51016,9 @@ var CmsWorker = class {
51072
51016
  } catch (err) {
51073
51017
  const report = this.ensureStatusReport();
51074
51018
  report.lastFatalError = {
51075
- // [REDACT] Persisted to worker-status.json and served to the
51076
- // browser by the admin panel -- must never carry the bot token
51077
- // that a poisoned/failed git URL (buildGitHubUrl()) can embed.
51019
+ // [REDACT] Persisted to worker-status.json and served to the browser by
51020
+ // the admin panel -- must never carry the bot token a poisoned or
51021
+ // failed git URL (buildGitHubUrl()) can embed.
51078
51022
  message: redactCredentials(getErrorMessage2(err)),
51079
51023
  at: (/* @__PURE__ */ new Date()).toISOString(),
51080
51024
  phase: "startup"
@@ -51124,26 +51068,23 @@ var CmsWorker = class {
51124
51068
  * Acquire the cross-host worker lock (DEP-C2).
51125
51069
  *
51126
51070
  * The task queue is single-consumer (see task-queue/task-queue.ts): two
51127
- * concurrent workers would double-process tasks (duplicate pushes, duplicate
51128
- * PRs). The workspace lives on a shared filesystem (EFS), so mutual
51129
- * exclusion must be sound ACROSS HOSTS — a PID liveness probe
51130
- * (`process.kill(pid, 0)`) only means something on the holder's own machine
51131
- * and must never participate in staleness decisions.
51071
+ * concurrent workers would double-process tasks, duplicating pushes and PRs.
51072
+ * The workspace lives on EFS, so mutual exclusion must be sound ACROSS HOSTS
51073
+ * — a PID liveness probe (`process.kill(pid, 0)`) means something only on the
51074
+ * holder's own machine and must NEVER participate in staleness decisions.
51132
51075
  *
51133
51076
  * proper-lockfile provides a heartbeat lease with no PID involved: the lock
51134
51077
  * is a directory created atomically (mkdir — atomic on NFS/EFS), the holder
51135
- * refreshes its mtime every lockStaleMs/2, and the lock is considered
51136
- * abandoned — and taken over — only when that heartbeat is older than
51137
- * lockStaleMs. Liveness is judged purely by heartbeat freshness.
51078
+ * refreshes its mtime every lockStaleMs/2, and the lock is abandoned, and
51079
+ * taken over, only once that heartbeat is older than lockStaleMs.
51138
51080
  *
51139
51081
  * No acquire retries: a second worker exits immediately, matching daemon
51140
- * semantics (the supervisor restarts it later). After a crash, the dead
51141
- * holder's heartbeat expires within lockStaleMs and the next start succeeds.
51082
+ * semantics. After a crash the dead holder's heartbeat expires within
51083
+ * lockStaleMs and the next start succeeds.
51142
51084
  *
51143
- * Staleness is judged by comparing the lock's mtime against the local
51144
- * clock, so correct cross-host takeover assumes reasonable clock agreement
51145
- * between hosts (e.g. NTP); with the default TTL, ordinary clock skew is
51146
- * negligible, but a host with a badly wrong clock could misjudge liveness.
51085
+ * Staleness compares the lock's mtime against the LOCAL clock, so correct
51086
+ * cross-host takeover assumes reasonable clock agreement (NTP). Ordinary skew
51087
+ * is negligible at the default TTL; a badly wrong clock misjudges liveness.
51147
51088
  */
51148
51089
  async acquireLock() {
51149
51090
  await fs13.mkdir(this.taskDir, { recursive: true });
@@ -51176,9 +51117,9 @@ var CmsWorker = class {
51176
51117
  }
51177
51118
  }
51178
51119
  /**
51179
- * Schedule a function to run repeatedly with setTimeout chaining.
51180
- * The next invocation starts `interval` ms after the previous one completes,
51181
- * preventing overlapping executions.
51120
+ * Run `fn` repeatedly, the next invocation starting `interval` ms after the
51121
+ * previous one COMPLETES. setTimeout chaining rather than setInterval, so
51122
+ * executions cannot overlap when one runs longer than the interval.
51182
51123
  */
51183
51124
  scheduleLoop(fn, interval) {
51184
51125
  const run = () => {
@@ -51200,18 +51141,16 @@ var CmsWorker = class {
51200
51141
  /**
51201
51142
  * Whether the bare repo at `gitDir` has a local `refs/heads/<baseBranch>`.
51202
51143
  *
51203
- * Uses an explicit `--git-dir` invocation rather than `simpleGit({ baseDir
51204
- * })` so this also works in sandboxed/CI git environments that set
51205
- * `safe.bareRepository=explicit` (which refuses cwd-based discovery of
51206
- * bare repos but expressly allows `--git-dir` — see
51207
- * GitManager.bareRemoteHasBranch for the same pattern).
51144
+ * Explicit `--git-dir` rather than `simpleGit({ baseDir })`, so this also
51145
+ * works where `safe.bareRepository=explicit` refuses cwd-based discovery of
51146
+ * bare repos but expressly allows `--git-dir` (same pattern as
51147
+ * GitManager.bareRemoteHasBranch).
51208
51148
  *
51209
- * Deliberately omits `--quiet`: simple-git only treats a task as failed
51210
- * when the process both exits non-zero AND writes to stderr
51211
- * (isTaskError), so a quiet, silent-on-failure `--verify` would leave a
51212
- * missing branch indistinguishable from success. Without `--quiet`,
51213
- * `rev-parse --verify` writes its "fatal: ..." to stderr on failure, which
51214
- * is what makes simple-git reject the promise here.
51149
+ * Deliberately omits `--quiet`: simple-git treats a task as failed only when
51150
+ * the process exits non-zero AND writes to stderr, so a silent-on-failure
51151
+ * `--verify` would leave a missing branch indistinguishable from success.
51152
+ * Without `--quiet`, `rev-parse --verify` writes "fatal: ..." to stderr,
51153
+ * which is what makes simple-git reject the promise here.
51215
51154
  */
51216
51155
  async verifyBaseBranchExists(gitDir) {
51217
51156
  await simpleGit().raw([
@@ -51228,20 +51167,17 @@ var CmsWorker = class {
51228
51167
  *
51229
51168
  * `git clone https://x-access-token:<token>@github.com/...` records that URL
51230
51169
  * verbatim as `remote.origin.url`, and for `remote.git` that config lives on
51231
- * shared EFS. The security model in docs/deploying-to-aws.md -- "If Lambda is
51232
- * compromised, an attacker can read/write content on EFS but cannot push to
51233
- * GitHub", "Secrets stay on the worker" -- is false while that string is
51234
- * there: a compromised Lambda could read the token off EFS and, despite
51235
- * having no egress of its own, exfiltrate it by writing it into branch
51236
- * content the worker then pushes to GitHub.
51170
+ * shared EFS. The security model in docs/deploying-to-aws.md -- a compromised
51171
+ * Lambda can read/write EFS content but cannot push to GitHub, secrets stay
51172
+ * on the worker -- is false while that string is there: the Lambda could read
51173
+ * the token off EFS and, with no egress of its own, exfiltrate it by writing
51174
+ * it into branch content the worker then pushes to GitHub.
51237
51175
  *
51238
51176
  * Nothing needs the remote: every push passes the URL explicitly as an
51239
- * argument (see the `git.push(this.buildGitHubUrl(), ...)` call sites), and
51240
- * `verifyBaseBranchExists` reads local refs.
51177
+ * argument, and `verifyBaseBranchExists` reads local refs.
51241
51178
  *
51242
51179
  * VERIFIES rather than assuming: it re-reads the config and throws if the URL
51243
- * survives, because the previous code's `.catch(() => {})` meant a failed
51244
- * scrub was indistinguishable from a successful one.
51180
+ * survives, so a failed scrub is never indistinguishable from a clean one.
51245
51181
  */
51246
51182
  async scrubPersistedRemote(gitDir) {
51247
51183
  const git = simpleGit({ baseDir: gitDir });
@@ -51275,19 +51211,17 @@ var CmsWorker = class {
51275
51211
  }
51276
51212
  }
51277
51213
  /**
51278
- * Ensure remote.git bare repo exists.
51279
- * On first run, clone from GitHub as a bare repo.
51214
+ * Ensure the remote.git bare repo exists, cloning it from GitHub on first
51215
+ * run.
51280
51216
  *
51281
51217
  * Empty-remote guard: simple-git's bare clone of an EMPTY GitHub repo (no
51282
- * commits, or a base branch that's never been pushed) exits 0 and produces
51283
- * a refs-less bare repo — HEAD points at an unborn branch. `fs.stat`
51284
- * cannot distinguish this from a healthy clone, so left unchecked it
51285
- * silently poisons remote.git: every later branch operation (Lambda-side
51286
- * clone provisioning, worker pushes) breaks, and the fs.stat short-circuit
51287
- * means it never heals on its own. We verify the base branch exists right
51288
- * after cloning and again on the already-exists fast path, since a
51289
- * previous run could have left a poisoned remote.git behind before this
51290
- * guard existed.
51218
+ * commits, or a base branch never pushed) exits 0 and produces a refs-less
51219
+ * bare repo whose HEAD points at an unborn branch. `fs.stat` cannot tell that
51220
+ * from a healthy clone, so left unchecked it silently poisons remote.git —
51221
+ * every later branch operation breaks and the fs.stat short-circuit means it
51222
+ * never heals. The base branch is therefore verified right after cloning AND
51223
+ * on the already-exists fast path, since a previous run can have left a
51224
+ * poisoned remote.git behind.
51291
51225
  */
51292
51226
  async ensureRemoteGit() {
51293
51227
  let exists2;
@@ -51329,24 +51263,18 @@ var CmsWorker = class {
51329
51263
  }
51330
51264
  // --- Task-queue cluster (worker/task-runner.ts) ------------------------
51331
51265
  //
51332
- // READ THIS BEFORE STUBBING ANY DELEGATOR BELOW. They are not all the same,
51333
- // and the difference is invisible from here.
51266
+ // READ THIS BEFORE STUBBING ANY DELEGATOR BELOW: they are not all the same,
51267
+ // and the difference is invisible from here. `processTaskQueue` is the public
51268
+ // loop entry `scheduleLoop` drives, so it IS on the production path; the
51269
+ // other three exist ONLY so test files can reach the implementations through
51270
+ // the instance, since the extracted modules call each other at module level.
51334
51271
  //
51335
- // `processTaskQueue` is the public loop entry `scheduleLoop` drives, so it IS
51336
- // on the production path. The other three exist ONLY so the existing test
51337
- // files can reach the implementations through the instance; production never
51338
- // dispatches through them, because the extracted modules call each other
51339
- // directly at module level.
51340
- //
51341
- // The consequence: replacing one of these on an instance only affects
51342
- // production behaviour if the context also routes it. Two do --
51343
- // `executeTask` and `pushBranchToGitHub` are on `WorkerContext` precisely
51344
- // because cms-worker.test.ts assigns over them and then asserts on the
51345
- // replacement. `updateBranchMetadata` is NOT: the test calls it directly and
51346
- // never stubs it, so a stub installed there today would be a silent no-op.
51347
- // If you need to stub a method that isn't on the context, add it to
51348
- // WorkerContext and route the internal caller through `ctx` -- do not assume
51349
- // the delegator's existence means the stub takes effect.
51272
+ // So replacing one of these on an instance affects production behaviour only
51273
+ // if the context also routes it. `executeTask` and `pushBranchToGitHub` are
51274
+ // on `WorkerContext` for exactly that reason; `updateBranchMetadata` is NOT,
51275
+ // so a stub installed there is a silent no-op. To stub a method that is not
51276
+ // on the context, add it to WorkerContext and route the internal caller
51277
+ // through `ctx` -- a delegator's existence does not make a stub take effect.
51350
51278
  async processTaskQueue() {
51351
51279
  return processTaskQueue(this.ctx());
51352
51280
  }
@@ -51364,16 +51292,11 @@ var CmsWorker = class {
51364
51292
  * Octokit client from it.
51365
51293
  *
51366
51294
  * DEFERRED out of the constructor deliberately, exactly as
51367
- * `ensureSettingsBranch()` is, and for the same reason that method records:
51295
+ * `ensureSettingsBranch()` is and for the reason that method records:
51368
51296
  * `resolveWorkerGitHubAuth` throws for a half-configured credential (both
51369
- * set, neither set, an unusable mint timeout or refresh interval), and a throw during
51370
- * `new CmsWorker(...)` lands BEFORE the only code that writes
51371
- * `lastFatalError` — start()'s catch. The AWS entrypoint constructs the
51372
- * worker and calls start() separately, and its `main().catch()` only logs
51373
- * and exits, so a constructor throw is an invisible ~5s systemd crash-loop
51374
- * that `cdk deploy` reports as success while the admin panel shows the
51375
- * worker 'absent' with no fatal error to explain it. That is a shipped
51376
- * regression this codebase has already paid for once (#198).
51297
+ * set, neither set, an unusable mint timeout or refresh interval), and a
51298
+ * throw during `new CmsWorker(...)` lands BEFORE the only code that writes
51299
+ * `lastFatalError` — start()'s catch.
51377
51300
  *
51378
51301
  * Idempotent, and it does NOT replace an `octokit` a test has already
51379
51302
  * assigned onto the instance — see the field's comment.
@@ -51388,34 +51311,29 @@ var CmsWorker = class {
51388
51311
  return this.githubAuth;
51389
51312
  }
51390
51313
  /**
51391
- * The Octokit client, built on first use.
51392
- *
51393
- * Every read goes through here rather than touching the field, because the
51394
- * field is no longer populated by the constructor: a method reached without
51395
- * start() would otherwise see `undefined`. `rebaseActiveBranches()` is
51396
- * exactly that case — apps/test-app's e2e route calls it directly, and its
51397
- * `pollMergeState` dispatch reads `ctx.octokit()`.
51314
+ * The Octokit client, built on first use. Every read goes through here rather
51315
+ * than touching the field, which the constructor does not populate: a method
51316
+ * reached without start() would otherwise see `undefined`.
51317
+ * `rebaseActiveBranches()` is exactly that case — apps/test-app's e2e route
51318
+ * calls it directly, and its `pollMergeState` dispatch reads `ctx.octokit()`.
51398
51319
  */
51399
51320
  octokitClient() {
51400
51321
  this.ensureGitHubAuth();
51401
51322
  return this.octokit;
51402
51323
  }
51403
51324
  /**
51404
- * The single seam through which every git-over-HTTPS credential reaches a
51405
- * git command. The only other consumer of the credential is Octokit, built
51406
- * from the same resolution by `ensureGitHubAuth()` above.
51325
+ * The single seam through which every git-over-HTTPS credential reaches a git
51326
+ * command. The only other consumer is Octokit, built from the same resolution
51327
+ * by `ensureGitHubAuth()` above.
51407
51328
  *
51408
- * Async because the credential need not be a value the worker already
51409
- * holds: under GitHub App auth `resolveGitToken` mints an installation
51410
- * token, which lasts about an hour. Nothing may cache what this returns —
51411
- * a URL built from an installation token goes stale with it. Resolving per
51412
- * use is cheap: `@octokit/auth-app` answers from its own cache until the
51413
- * token is near expiry, so the usual cost is a resolved microtask, and the
51414
- * token path is a bare `async` return.
51329
+ * Async because under GitHub App auth `resolveGitToken` mints an installation
51330
+ * token lasting about an hour. NOTHING may cache what this returns — a URL
51331
+ * built from an installation token goes stale with it — and resolving per use
51332
+ * is cheap, since `@octokit/auth-app` answers from its own cache until the
51333
+ * token is near expiry.
51415
51334
  *
51416
51335
  * A mint failure propagates AS THROWN, carrying the `.status` that
51417
- * `isPermanentTaskFailure` (task-runner.ts:92) classifies on — see
51418
- * github-auth.ts.
51336
+ * `isPermanentTaskFailure` classifies on — see github-auth.ts.
51419
51337
  *
51420
51338
  * Do NOT add a parallel token accessor alongside it. Every instance-backed
51421
51339
  * WorkerContext member stays a function precisely so tests can replace it
@@ -51429,36 +51347,31 @@ var CmsWorker = class {
51429
51347
  /**
51430
51348
  * Prove the GitHub App credential works before anything depends on it.
51431
51349
  *
51432
- * Without this the first failure comes out of `ensureRemoteGit`'s bare
51433
- * clone below, whose catch reads "the GitHub repository may be empty, or
51434
- * the base branch may not exist" — which would send an operator holding a
51435
- * bad private key to go looking for a repository problem that does not
51436
- * exist. Called from start()'s try, so the failure is also recorded as
51437
- * `lastFatalError` in worker-status.json and reaches the admin panel.
51350
+ * Without this the first failure comes out of `ensureRemoteGit`'s bare clone
51351
+ * below, whose catch blames the repository ("may be empty, or the base branch
51352
+ * may not exist") and sends an operator holding a bad private key looking for
51353
+ * a problem that does not exist. Called from start()'s try, so the failure is
51354
+ * also recorded as `lastFatalError` and reaches the admin panel. No-op on the
51355
+ * token path: a PAT is a literal, so the first real request checks everything
51356
+ * this could.
51438
51357
  *
51439
- * No-op on the token path: a PAT is a literal, so there is nothing to
51440
- * check that the first real request would not check anyway.
51441
- *
51442
- * FATAL UNLESS THE FAILURE POSITIVELY LOOKS TRANSIENT. Both halves of that
51443
- * are load-bearing.
51444
- *
51445
- * Not always fatal, because the two credential paths must degrade alike: on
51446
- * the token path a GitHub 502 during boot is absorbed (a warm `remote.git`
51447
- * short-circuits `ensureRemoteGit`, and `Promise.allSettled` swallows the
51448
- * initial `syncGit`), so the worker starts and its loops retry. Rethrowing
51449
- * every error class would make the App path exit instead, and systemd
51450
- * (Restart=always) would crash-loop it until GitHub recovered — each
51451
- * iteration telling the operator to go and check their private key.
51358
+ * FATAL UNLESS THE FAILURE POSITIVELY LOOKS TRANSIENT. Both halves are
51359
+ * load-bearing. Not always fatal, because the two credential paths must
51360
+ * degrade alike: on the token path a GitHub 502 during boot is absorbed (a
51361
+ * warm `remote.git` short-circuits `ensureRemoteGit`, `Promise.allSettled`
51362
+ * swallows the initial `syncGit`) so the worker starts and its loops retry,
51363
+ * while rethrowing every error class would make the App path exit and systemd
51364
+ * crash-loop it until GitHub recovered — each iteration telling the operator
51365
+ * to check their private key.
51452
51366
  *
51453
51367
  * But fail CLOSED, via `isTransientAuthFailure` rather than the inverse of
51454
- * `isPermanentTaskFailure`. That classifier defaults an error with no HTTP
51455
- * status to transient, which is right on the task path (bounded by
51456
- * `maxRetries`) and wrong here (bounded by nothing): a key that never
51457
- * reaches GitHub at all — the wrong key type, or one too mangled to sign
51458
- * with — fails locally and status-lessly, so "default to transient" would
51459
- * boot a worker with a dead credential, record no `lastFatalError`, and show
51460
- * healthy in the admin panel while every task and every sync failed. See
51461
- * isTransientAuthFailure in github-auth.ts.
51368
+ * `isPermanentTaskFailure`: that classifier defaults a status-less error to
51369
+ * transient, which is right on the task path (bounded by `maxRetries`) and
51370
+ * wrong here (bounded by nothing). A key that never reaches GitHub at all —
51371
+ * the wrong type, or too mangled to sign with — fails locally and
51372
+ * status-lessly, so defaulting to transient would boot a worker with a dead
51373
+ * credential, record no `lastFatalError`, and show healthy in the admin panel
51374
+ * while every task and every sync failed.
51462
51375
  */
51463
51376
  async preflightGitHubAppAuth() {
51464
51377
  if (!this.config.githubAppAuth) return;
@@ -51479,20 +51392,19 @@ var CmsWorker = class {
51479
51392
  workerLog("GitHub App authentication verified");
51480
51393
  }
51481
51394
  /**
51482
- * The workspace directory for a branch named by its GIT REF name -- the
51483
- * form task payloads carry (`context.branch.name`), not the directory form.
51395
+ * The workspace directory for a branch named by its GIT REF name -- the form
51396
+ * task payloads carry (`context.branch.name`), not the directory form.
51484
51397
  *
51485
51398
  * These differ for any name outside `[A-Za-z0-9._-]`, `/` being the obvious
51486
51399
  * one: workspaces are provisioned under `sanitizeBranchName(...)` (see
51487
51400
  * paths/branch.ts's `resolveBranchPaths`), so `feature/x` lives in
51488
- * `feature-x`. Joining the raw name instead silently addresses a directory
51489
- * that does not exist -- which for the metadata writers below meant the
51490
- * update was quietly dropped, and for the leased push meant the
51491
- * history-rewrite marker read as absent and the push went out unleased,
51492
- * wedging exactly the branch this workstream exists to unwedge.
51401
+ * `feature-x`. Joining the RAW name instead silently addresses a directory
51402
+ * that does not exist -- which drops the metadata writers' updates, and makes
51403
+ * the leased push read the history-rewrite marker as absent and go out
51404
+ * unleased, wedging the branch.
51493
51405
  *
51494
- * `name` inside the metadata itself stays the raw ref name; only the path
51495
- * is sanitized.
51406
+ * `name` inside the metadata itself stays the raw ref name; only the path is
51407
+ * sanitized.
51496
51408
  */
51497
51409
  branchWorkspacePath(branchRefName) {
51498
51410
  return path17.join(this.contentBranchesPath, sanitizeBranchName(branchRefName));
@@ -51503,32 +51415,28 @@ var CmsWorker = class {
51503
51415
  * `checkout --theirs` resolution loop overwrites them. No-op in production.
51504
51416
  *
51505
51417
  * A test subclass overrides this to land a real `ContentStore` write at
51506
- * exactly the instant the rebase is mid-flight -- the window the old TOCTOU
51507
- * comment above the dirty check wrongly called safe -- without any sleeps or
51508
- * shell rendezvous. See the "Deterministic interleavings" testing pattern in
51418
+ * exactly the instant the rebase is mid-flight, with no sleeps or shell
51419
+ * rendezvous. See the "Deterministic interleavings" pattern in
51509
51420
  * docs/concurrency.md, and `ContentStore.afterPrePassForTesting()` for the
51510
51421
  * same idiom on the write side.
51511
51422
  */
51512
51423
  async afterConflictDetectedForTesting() {
51513
51424
  }
51514
51425
  /**
51515
- * Test seam, sibling of {@link afterConflictDetectedForTesting}: runs at the
51426
+ * Test seam, sibling of {@link afterConflictDetectedForTesting}: runs the
51516
51427
  * instant a rebase round has succeeded, before the completion path (cache
51517
- * invalidation, conflict metadata, [SYNC-H1] marker) executes. Exists so a
51518
- * test can lose the content-write lock at exactly the point where bailing out
51519
- * would strand a rewritten history.
51428
+ * invalidation, conflict metadata, [SYNC-H1] marker) executes, so a test can
51429
+ * lose the content-write lock at exactly the point where bailing out would
51430
+ * strand a rewritten history.
51520
51431
  */
51521
51432
  async afterRebaseCompletedForTesting() {
51522
51433
  }
51523
51434
  // --- Rebase loop (worker/rebase.ts) ------------------------------------
51524
51435
  //
51525
51436
  // Both are TEST-ONLY entry points -- see the task-queue block above. Nothing
51526
- // in production dispatches through either; `syncGit` calls `runRebaseCycle`
51527
- // directly and that calls `pollMergeState` directly. Four test files call
51528
- // `rebaseActiveBranches` (it is the single entry point the whole rebase suite
51529
- // drives), and cms-worker-merge-poll.test.ts calls `pollMergeState` with a
51530
- // mocked `octokit`. Neither is on WorkerContext, so a stub installed on
51531
- // either is a no-op as far as a production code path is concerned.
51437
+ // in production dispatches through either: `syncGit` calls `runRebaseCycle`
51438
+ // directly and that calls `pollMergeState` directly. Neither is on
51439
+ // WorkerContext, so a stub installed on either is a no-op for production.
51532
51440
  //
51533
51441
  // `rebaseActiveBranches` also has a consumer outside the test files:
51534
51442
  // apps/test-app/app/api/e2e-test/rebase/route.ts drives it from an e2e
@@ -51540,14 +51448,11 @@ var CmsWorker = class {
51540
51448
  //
51541
51449
  // `syncGit` is the public loop entry `scheduleLoop` drives. The three private
51542
51450
  // ones below it are TEST-ONLY entry points -- see the task-queue block above
51543
- // -- each called directly by one test file (cms-worker-sync-reconcile for the
51544
- // settings push, cms-worker-base-refresh for the base workspace,
51545
- // cms-worker.test.ts for the trash sweep). `syncGit` reaches all three as
51451
+ // -- each called directly by one test file. `syncGit` reaches all three as
51546
51452
  // module-level calls, so a stub installed on one of these is a no-op.
51547
51453
  //
51548
51454
  // `reconcileTrackedBranches` deliberately has NO delegator: no test reaches
51549
- // it through the instance. Comments elsewhere that point at it as living in
51550
- // this file are stale by definition -- it is in git-sync.ts.
51455
+ // it through the instance.
51551
51456
  async syncGit() {
51552
51457
  return syncGit(this.ctx());
51553
51458
  }
@@ -51556,10 +51461,10 @@ var CmsWorker = class {
51556
51461
  *
51557
51462
  * One of `refreshGitHubCredential`'s two call sites, and the one that works
51558
51463
  * when nobody is publishing: it fetches from GitHub every `gitSyncInterval`
51559
- * (default 5 minutes) whether or not anyone is editing, so a credential that
51560
- * has stopped working surfaces here even with no push queued for days.
51464
+ * whether or not anyone is editing, so a credential that has stopped working
51465
+ * surfaces here even with no push queued for days.
51561
51466
  *
51562
- * The sync failure is what propagates to `scheduleLoop`'s catch.
51467
+ * The SYNC failure is what propagates to `scheduleLoop`'s catch;
51563
51468
  * `refreshGitHubCredential` never throws, so nothing it does can replace it.
51564
51469
  */
51565
51470
  async syncGitWithCredentialRefresh() {
@@ -51576,42 +51481,29 @@ var CmsWorker = class {
51576
51481
  * **Two call sites, each covering what the other cannot.** The git-sync loop
51577
51482
  * (`syncGitWithCredentialRefresh`) notices a dead credential when nobody is
51578
51483
  * publishing. `processTaskQueue`'s per-task catch is what saves a publish: a
51579
- * push task spends its retry budget on a 5s/10s/20s backoff
51580
- * (task-queue/task-queue.ts), well inside one 5-minute sync interval, so with
51581
- * the sync loop as the only trigger a publish that met a rotated token failed
51582
- * permanently while the working one was already in the secret store.
51583
- *
51584
- * Every consumer reaches the credential through `ensureGitHubAuth()`, which
51585
- * reads it per use — the sync fetch, `pushBranchToGitHub`,
51586
- * `pushSettingsBranches`, `ensureRemoteGit`'s clone, and every Octokit call —
51587
- * so a refresh from either site repairs all of them for their NEXT use, with
51588
- * nothing to invalidate. It cannot rescue an attempt that has already failed,
51589
- * so a task `isPermanentTaskFailure` fails fast is not saved; the task after
51590
- * it is.
51591
- *
51592
- * NOT gated on the error looking auth-shaped, at either site. There is nothing
51593
- * to classify on: a `git fetch` or `git push` rejected for a dead token throws
51594
- * a plain simple-git error — exit 128, no HTTP `.status` — which
51595
- * `isPermanentTaskFailure` reads as transient, so a gate keyed on it would
51596
- * never fire. Two floors bound the cost instead: core's own
51597
- * `refreshGitHubTokenMinIntervalMs` (default 60s, enforced by
51598
- * `refreshCredential` in github-auth.ts), and whatever floor the provider
51599
- * keeps — the AWS one reads at most once per five minutes and returns
51600
- * `undefined` for an unchanged value (canopycms-cdk/worker/credential-refresh.ts).
51601
- * On the GitHub App path the refresh is a no-op. Both call sites share both
51602
- * floors, so a read issued by one throttles the other.
51484
+ * push task spends its retry budget on a 5s/10s/20s backoff, well inside one
51485
+ * 5-minute sync interval, so with the sync loop as the only trigger a publish
51486
+ * meeting a rotated token fails permanently while the working one is already
51487
+ * in the secret store. Every consumer reaches the credential through
51488
+ * `ensureGitHubAuth()`, which reads it per use, so a refresh from either site
51489
+ * repairs all of them for their NEXT use — but not an attempt already failed.
51603
51490
  *
51604
- * **Never throws.** Both callers are already reporting a failure, and that
51605
- * failure is the one that must reach the log.
51491
+ * NOT gated on the error looking auth-shaped, at either site: a `git
51492
+ * fetch`/`push` rejected for a dead token throws a plain simple-git error
51493
+ * (exit 128, no HTTP `.status`) that `isPermanentTaskFailure` reads as
51494
+ * transient, so a gate keyed on it would never fire. Two floors bound the
51495
+ * cost instead — core's `refreshGitHubTokenMinIntervalMs` (default 60s,
51496
+ * enforced by `refreshCredential` in github-auth.ts) and whatever floor the
51497
+ * provider keeps (the AWS one reads at most once per five minutes) — and both
51498
+ * call sites share both, so a read issued by one throttles the other. On the
51499
+ * GitHub App path the refresh is a no-op.
51606
51500
  *
51607
- * **Bounded by `taskTimeoutMs`**, because the task loop awaits it, and a read
51608
- * that never settled would stop every publish queued behind it. An adopter's
51609
- * provider may have no bound at all, and the AWS one, bounded as it is
51610
- * (canopycms-cdk/worker/secrets.ts), can still take up to 87s for one
51611
- * `getSecret` — longer than the 60s default here. A read that loses the race
51612
- * is not cancelled, and
51613
- * may still land later; `refreshCredential` discards a result older than one
51614
- * it has already applied, so a late landing cannot put a stale token back.
51501
+ * **Never throws**: both callers are already reporting the failure that must
51502
+ * reach the log. **Bounded by `taskTimeoutMs`**, because the task loop awaits
51503
+ * it and a read that never settled would stop every publish queued behind it
51504
+ * (an adopter's provider may have no bound at all; the AWS one can take 87s
51505
+ * for one `getSecret`). A losing read is not cancelled and may land later, but
51506
+ * `refreshCredential` discards a result older than one already applied.
51615
51507
  */
51616
51508
  async refreshGitHubCredential() {
51617
51509
  let timer;
@@ -61321,12 +61213,9 @@ async function main() {
61321
61213
  const githubAppAuth = githubAppId && githubAppInstallationId && githubAppPrivateKeySecretArn ? buildGitHubAppAuth({
61322
61214
  appId: githubAppId,
61323
61215
  installationId: githubAppInstallationId,
61324
- // An App private key is the credential most likely to live inside a
61325
- // JSON document rather than alone in a secret -- which is why the
61326
- // JSON-field option exists at all. Same `|| undefined` as the other
61327
- // two `getSecret` call sites in this file (the GitHub token above and
61328
- // the Clerk key below), and for the same reason: a blank var means
61329
- // "not configured", not "read the field named ''".
61216
+ // Same `|| undefined` as the other two `getSecret` call sites in this
61217
+ // file, for the same reason: a blank var means "not configured", not
61218
+ // "read the field named ''".
61330
61219
  privateKey: await getSecret(githubAppPrivateKeySecretArn, {
61331
61220
  jsonField: process.env.CANOPYCMS_GITHUB_APP_PRIVATE_KEY_SECRET_JSON_FIELD || void 0,
61332
61221
  jsonFieldEnvVar: "CANOPYCMS_GITHUB_APP_PRIVATE_KEY_SECRET_JSON_FIELD"
@@ -61363,12 +61252,10 @@ async function main() {
61363
61252
  refreshAuthCache,
61364
61253
  baseBranch: process.env.CANOPYCMS_BASE_BRANCH ?? "main",
61365
61254
  // deploymentName is deliberately NOT passed: CmsWorker resolves it through
61366
- // resolveDeploymentName, which reads CANOPYCMS_DEPLOYMENT_NAME itself
61367
- // (CanopyCmsService stamps that env var from
61368
- // CanopyCmsServiceProps.deploymentName), applies the same env > config >
61369
- // 'prod' precedence as the Lambda, and validates the result as a git ref
61370
- // component. Reading the env var here instead would re-implement the
61371
- // outer half of that chain and skip the validation.
61255
+ // resolveDeploymentName, which reads CANOPYCMS_DEPLOYMENT_NAME itself,
61256
+ // applies the same env > config > 'prod' precedence as the Lambda, and
61257
+ // validates the result as a git ref component. Reading the env var here
61258
+ // would re-implement the outer half of that chain and skip the validation.
61372
61259
  // Explicit override, matching the strategy's own precedence: an adopter who
61373
61260
  // sets `settingsBranch` in canopycms.config.ts must set this too, or the
61374
61261
  // worker would own a branch name the Lambda never writes to.