@aglyn/aglyn 1.0.0-beta.220 → 1.0.0-beta.221

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 (26) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/docs-help.generated.d.ts +2 -2
  3. package/src/lib/app-utils/docs-help.generated.js +3 -0
  4. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  5. package/src/lib/app-utils/docs-index.generated.js +9 -2
  6. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  7. package/src/lib/app-utils/health-report.js +22 -0
  8. package/src/lib/app-utils/health-report.js.map +1 -1
  9. package/src/lib/app-utils/leaving-notice.d.ts +96 -0
  10. package/src/lib/app-utils/leaving-notice.js +157 -0
  11. package/src/lib/app-utils/leaving-notice.js.map +1 -0
  12. package/src/lib/app-utils/operator-alerts.js +17 -0
  13. package/src/lib/app-utils/operator-alerts.js.map +1 -1
  14. package/src/lib/app-utils/screen-link-context-value.d.ts +7 -0
  15. package/src/lib/app-utils/screen-link-context-value.js.map +1 -1
  16. package/src/lib/app-utils/screen-link-context.js +14 -2
  17. package/src/lib/app-utils/screen-link-context.js.map +1 -1
  18. package/src/lib/app-utils/screen-route.d.ts +3 -0
  19. package/src/lib/app-utils/screen-route.js +6 -1
  20. package/src/lib/app-utils/screen-route.js.map +1 -1
  21. package/src/lib/plugin-manager/first-party-plugins.generated.d.ts +6 -0
  22. package/src/lib/plugin-manager/first-party-plugins.generated.js +4 -0
  23. package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
  24. package/src/lib/plugin-manager/plugin-server-steps.d.ts +170 -0
  25. package/src/lib/plugin-manager/plugin-server-steps.js +116 -0
  26. package/src/lib/plugin-manager/plugin-server-steps.js.map +1 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/health-report.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Shared shape for the `/api/health` endpoints (AGL-1102).\n *\n * Both the console and the tenant runtime answer this, and they must answer it\n * IDENTICALLY — an uptime series is only comparable across services if the\n * status codes and the field names agree. Two near-identical route files would\n * have drifted the first time one of them was touched.\n *\n * The rules encoded here, each of which is a way health checks routinely lie:\n *\n * **The status code is the contract.** Most uptime monitors read nothing else,\n * so a degraded dependency has to be a 503. A 200 whose body says \"degraded\"\n * is a signal nobody receives.\n *\n * **Never cacheable.** A cached health check returns 200 from the edge while\n * the origin is on fire, and the graph stays green straight through the\n * outage. Three separate caches have faked a green check in this repo already,\n * so this sets `no-store` on every response including the failures — an error\n * response is the one you least want served from a cache.\n *\n * **No internals in the body.** It is public and unauthenticated, so a failing\n * check reports a CODE and never the underlying message, which can carry\n * project ids, paths or credentials fragments.\n */\n\nexport interface HealthCheck {\n ok: boolean\n /** How long the dependency took. The number that turns into a latency graph. */\n ms: number\n /** A stable code on failure — never a raw error message. */\n code?: string\n}\n\nexport interface HealthReport {\n service: string\n checks: Record<string, HealthCheck>\n commit?: string | null\n /**\n * Which VERSION of the platform answered (AGL-2091).\n *\n * `commit` alone could only ever be answered on Aglyn's own cloud, so a\n * self-host operator had nothing to quote in a bug report and no way to ask\n * whether a fix had reached them. This is the field that answers it, and\n * unlike the commit it needs no operator configuration —\n * `platformVersion()` reads the number the build inlined from package.json.\n */\n version?: string | null\n environment?: string | null\n region?: string | null\n /** Injected so the body is deterministic in tests. */\n at?: string\n}\n\nexport const HEALTH_NO_STORE =\n 'no-store, no-cache, must-revalidate, max-age=0'\n\n/** Degraded if ANY check failed. There is no partial credit for uptime. */\nexport function healthStatus(checks: Record<string, HealthCheck>): 'ok' | 'degraded' {\n return Object.values(checks).every((check) => check.ok) ? 'ok' : 'degraded'\n}\n\nexport function healthBody(report: HealthReport): Record<string, unknown> {\n return {\n status: healthStatus(report.checks),\n service: report.service,\n checks: report.checks,\n commit: report.commit ?? null,\n version: report.version ?? null,\n environment: report.environment ?? null,\n region: report.region ?? null,\n at: report.at ?? new Date().toISOString(),\n }\n}\n\nexport function healthHeaders(status: 'ok' | 'degraded'): Record<string, string> {\n return {\n 'Cache-Control': HEALTH_NO_STORE,\n // Readable from any origin, deliberately.\n //\n // A status page must not live on the thing it reports on, so it is served\n // from a DIFFERENT deployment and reads these cross-origin. Without this\n // header the browser blocks the read and the page renders every service as\n // down during a perfectly healthy day — a status page that cries outage is\n // worse than none.\n //\n // Safe to open: the body is already public and unauthenticated, carries no\n // secrets, and a failing check reports a code rather than a message.\n 'Access-Control-Allow-Origin': '*',\n // Tells a well-behaved monitor the failure is transient rather than a\n // permanent 5xx worth escalating on the first sample.\n ...(status === 'ok' ? {} : { 'Retry-After': '30' }),\n }\n}\n\nexport function healthHttpStatus(status: 'ok' | 'degraded'): number {\n return status === 'ok' ? 200 : 503\n}\n\n/**\n * A HEAD response for a health endpoint: the SAME status and headers its GET\n * would return, with no body (AGL-1148).\n *\n * ## Why this exists\n *\n * Every health route used to answer HEAD with a hardcoded `200` and the\n * comment \"cheap liveness for monitors that only issue HEAD. Touches\n * nothing.\" Touching nothing is precisely the problem: **a HEAD that knows\n * nothing is a health check that cannot go red.** A monitor configured with\n * HEAD — which several uptime providers use by default — would have reported\n * green through any outage that left the function able to boot, which is most\n * of them. `/api/health/crons` sat at 503 for fifty-one hours; a HEAD monitor\n * pointed at it would have agreed with the green board the whole time.\n *\n * It also violated the contract this file opens with. \"The status code is the\n * contract\" cannot hold for one method and not another, and HTTP requires a\n * HEAD response to carry the same status and headers its GET would.\n *\n * ## The cost argument, which is why this is safe\n *\n * Delegating to GET means HEAD runs the dependency probe. Every route\n * memoizes that probe (`memoizeWithTtl`, 15s on the root and 5 minutes on the\n * subsystems) explicitly so a public unauthenticated endpoint cannot be made\n * to read in a loop — the same bound that already lets a fifteen-minute probe\n * hit seven endpoints for nearly nothing. HEAD now costs exactly what GET\n * costs, which is a memo lookup between refreshes, and it serializes no body.\n *\n * Headers are carried over rather than rebuilt so `Cache-Control: no-store`,\n * the open CORS origin and `Retry-After` reach a HEAD client identically — a\n * HEAD response that was cacheable while its GET was not would be the same\n * lie one layer down.\n */\nexport async function healthHeadOf(\n get: () => Promise<Response>,\n): Promise<Response> {\n const response = await get()\n return new Response(null, {\n status: response.status,\n headers: response.headers,\n })\n}\n\n/**\n * Firestore backup state, reduced to a health verdict (AGL-1490, AGL-1502).\n *\n * The 2026-08-02 backup sat at `NOT_AVAILABLE` for eleven days and nothing\n * noticed — the schedule ran, the API reported the failure, and no one was\n * listening. This turns \"is the newest backup real and recent\" into the same\n * 200/503 contract the other health checks speak, so the existing uptime\n * probe + alert path is the listener.\n *\n * Pure on purpose: the route fetches, this decides, the spec exercises every\n * branch without a network.\n */\nexport interface BackupSnapshot {\n /** `READY` | `CREATING` | `NOT_AVAILABLE` (or whatever the API adds next). */\n state?: string\n /** ISO timestamp of the data the backup captured. */\n snapshotTime?: string\n}\n\nexport interface BackupsCheck extends HealthCheck {\n /**\n * Histogram by state — `{ READY: 1, NOT_AVAILABLE: 1 }`. Counts only: the\n * endpoint is public, and backup ids or resource paths have no business in\n * it. Counts are enough to know WHAT is wrong before opening gcloud.\n */\n states: Record<string, number>\n /** Age of the newest READY backup, in days. Null when there is none. */\n newestReadyAgeDays: number | null\n /**\n * Present, and always `false`, ONLY in the third state (AGL-1843): the\n * check could not determine whether backups are healthy. Omitted entirely\n * from a real verdict, exactly as `code` is omitted from a healthy one, so\n * `ok: true` with no `determinate` key means \"measured, and fine\".\n *\n * An indeterminate check reports `ok: true` and therefore does not page.\n * That is deliberate and it is bounded — see the escalation note in\n * `backupsHealth`.\n */\n determinate?: false\n}\n\n/**\n * Weekly Sunday schedule + one day of slack. A healthy cadence never exceeds\n * 7; day 8 means the last run produced nothing usable.\n */\nexport const MAX_BACKUP_AGE_DAYS = 8\n\n/**\n * States in which a backup has not FAILED — it is simply not usable yet.\n *\n * `STATE_UNSPECIFIED` is a real member of the API's `Backup.State` enum (\"The\n * state is unspecified\"), and proto3 JSON omits a default-valued field, so a\n * backup in it arrives with **no `state` key at all**. Treating that absence\n * as a failure is the `strictNullChecks`-off trap this file is otherwise\n * careful about: a missing field must never fold into a red.\n */\nconst BACKUP_IN_FLIGHT = new Set(['CREATING', 'STATE_UNSPECIFIED'])\n\n/**\n * `backups` is the listing, or `null` when it could not be read at all.\n *\n * `options.unreachable` is `ListBackupsResponse.unreachable` — the locations\n * the API could not reach. Non-empty means the listing is a PARTIAL result\n * set. `options.code` is the code to report when `backups` is `null`.\n */\nexport function backupsHealth(\n backups: BackupSnapshot[] | null,\n ms: number,\n now: number = Date.now(),\n options: { unreachable?: string[]; code?: string } = {},\n): BackupsCheck {\n // ## THE THREE STATES (AGL-1843, second pass)\n //\n // This check used to have two: healthy, and failed. Everything it could not\n // establish — an upstream that errored, a partial listing, a run that had\n // not finished — collapsed into \"backup failed\", which is the loudest thing\n // it can say and was, in every one of those cases, false. It reported 503\n // `backup-failed` continuously for four and a half days while the backups\n // were fine, against a monitor that emails on a five-minute interval.\n //\n // ### Why `NOT_AVAILABLE` is not a failure — measured, not assumed\n //\n // The API documents the state as \"The backup is not available **at this\n // moment**\", and that temporal wording is literal. The same three backup\n // ids, read from `gcloud firestore backups list` on three dates:\n //\n // id 2026-08-13 2026-08-17/18 2026-08-24\n // 3b5238df… NOT_AVAILABLE NOT_AVAILABLE READY\n // eb4d21e3… READY NOT_AVAILABLE READY\n // d14ce827… — READY READY\n //\n // `NOT_AVAILABLE` flips in BOTH directions, on backups whose `expireTime`\n // is months away, independent of age. The 2026-08-02 backup that AGL-1490\n // was filed about — \"half the Firestore backups are NOT_AVAILABLE\" — is\n // READY today, twenty-two days old. It was never broken. Neither was the\n // 2026-08-09 one, which went READY → NOT_AVAILABLE → READY. And the\n // predicted permanent day-7 degradation did not happen: d14ce827 is READY\n // at day 8. So every rule that reads a `NOT_AVAILABLE` count as damage is\n // reading a maintenance window as an outage.\n //\n // ### What the check actually needs to answer\n //\n // \"Can we restore, and is the restore point recent?\" That question is\n // answered ENTIRELY by the age of the newest READY backup. No other row in\n // the listing can make a fresh READY backup un-restorable. So a fresh READY\n // backup is unconditionally green, and everything below it is the taxonomy\n // of why we could not find one.\n //\n // ### Why indeterminate answers 200, and why that is not fail-open\n //\n // Three things bound it, because a backups check that cannot go red is\n // worse than no backups check:\n // 1. It is time-bounded. Indeterminacy is only tolerated while a backup\n // younger than `MAX_BACKUP_AGE_DAYS` exists. A schedule that stops, or\n // that never produces anything usable, crosses the budget and goes red\n // within eight days whatever the states say.\n // 2. It never overrides staleness. If a READY backup exists and is over\n // budget, that is `backup-stale` — a newer CREATING or NOT_AVAILABLE\n // row earns no freshness credit, so a run that hangs every week cannot\n // hold the check green.\n // 3. The route classifies PERMANENT upstream failures (no credential,\n // 401/403/404) as hard red rather than indeterminate, so a revoked IAM\n // role cannot silence this forever. And `checks.exports` in the same\n // endpoint is an independent, still-hard-red probe of the GCS copy:\n // \"we cannot read the managed backups\" answers 200 only while we can\n // still prove a fresh independent copy exists.\n //\n // State 3a — the listing could not be read AT ALL. That is not a verdict;\n // it is the absence of one, and reporting it as `backup-failed` is the\n // defect this pass exists to remove.\n if (backups === null) {\n return {\n ok: true,\n ms,\n code: options.code ?? 'backups-unreadable',\n states: {},\n newestReadyAgeDays: null,\n determinate: false,\n }\n }\n\n const states: Record<string, number> = {}\n for (const backup of backups) {\n const state = backup.state ?? 'STATE_UNSPECIFIED'\n states[state] = (states[state] ?? 0) + 1\n }\n\n const snapshotMsOf = (backup: BackupSnapshot): number =>\n backup.snapshotTime ? Date.parse(backup.snapshotTime) : Number.NaN\n const newestOf = (keep: (backup: BackupSnapshot) => boolean): number | null =>\n backups\n .filter(keep)\n .map(snapshotMsOf)\n .filter((time) => Number.isFinite(time))\n .reduce<number | null>((newest, time) => Math.max(newest ?? time, time), null)\n const ageDaysOf = (time: number): number =>\n Math.round(((now - time) / 86_400_000) * 10) / 10\n\n const newestReadyMs = newestOf((backup) => backup.state === 'READY')\n const newestAnyMs = newestOf(() => true)\n const newestReadyAgeDays = newestReadyMs === null ? null : ageDaysOf(newestReadyMs)\n const newestAnyAgeDays = newestAnyMs === null ? null : ageDaysOf(newestAnyMs)\n\n // State 1 — HEALTHY. A restore point exists and it is recent. Nothing else\n // in the listing can falsify that, so nothing else is consulted.\n if (newestReadyAgeDays !== null && newestReadyAgeDays <= MAX_BACKUP_AGE_DAYS) {\n return { ok: true, ms, states, newestReadyAgeDays }\n }\n\n // State 2a — FAILED. A backup that is neither READY nor in flight and that\n // carries no usable `snapshotTime` cannot be aged at all, so we cannot even\n // say when the last run was. That is positive evidence of something wrong,\n // and \"assume it aged out\" is the assumption that hides a broken run.\n const undateable = backups.some(\n (backup) =>\n backup.state !== 'READY' &&\n !BACKUP_IN_FLIGHT.has(backup.state ?? 'STATE_UNSPECIFIED') &&\n !Number.isFinite(snapshotMsOf(backup)),\n )\n if (undateable) {\n return { ok: false, ms, code: 'backup-failed', states, newestReadyAgeDays }\n }\n\n // State 3b — the listing is a PARTIAL result set. `ListBackupsResponse`\n // carries `unreachable`: rather than failing the request when one location\n // cannot be reached, the API returns the backups it could see and names the\n // ones it could not. Positive evidence survives that (state 1 above already\n // returned); negative evidence does not — \"we saw no fresh READY backup\" is\n // not a finding when we did not see everything.\n if ((options.unreachable ?? []).length > 0) {\n return {\n ok: true,\n ms,\n code: 'backups-partial',\n states,\n newestReadyAgeDays,\n determinate: false,\n }\n }\n\n // State 3c — a recent run exists, it simply is not usable AT THIS MOMENT:\n // mid-creation on a Sunday, or inside one of the `NOT_AVAILABLE` windows\n // measured above. Only reachable when NOTHING is READY — a stale READY\n // backup falls through to `backup-stale` below, so this can never buy a\n // hanging schedule more than the eight-day budget.\n if (\n newestReadyAgeDays === null &&\n newestAnyAgeDays !== null &&\n newestAnyAgeDays <= MAX_BACKUP_AGE_DAYS\n ) {\n return {\n ok: true,\n ms,\n code: 'backups-not-ready-yet',\n states,\n newestReadyAgeDays,\n determinate: false,\n }\n }\n\n // State 2b — FAILED on freshness. Either nothing has ever been READY and\n // nothing recent exists to be waiting on, or the newest READY backup is\n // past the budget. Both mean the same operational thing: there is no recent\n // restore point, which is the only fact this check exists to protect.\n return {\n ok: false,\n ms,\n code: newestReadyAgeDays === null ? 'no-ready-backup' : 'backup-stale',\n states,\n newestReadyAgeDays,\n }\n}\n\n/**\n * GCS export freshness, reduced to a health verdict (AGL-1843).\n *\n * Exports are NOT managed backups — they are the independent copy that exists\n * BECAUSE the managed backups proved unreliable (every backup this project\n * took flipped `READY` → `NOT_AVAILABLE` at roughly one week old, AGL-1843).\n * A weekly `exportDocuments` run writes portable snapshot files to a GCS\n * bucket, and this check watches that the newest one is real and recent, so\n * a managed-backup flip no longer means \"one copy only\". The two are kept as\n * SEPARATE checks in the same endpoint precisely so the body says which layer\n * is degraded rather than blending them into one dishonest verdict.\n *\n * The input is the list of `*.overall_export_metadata` completion markers —\n * Firestore writes exactly one per FINISHED export, so the marker's creation\n * time is the moment the export completed, not the moment it started. An\n * export that hangs forever never produces a marker and fails the age rule,\n * the same trick `backupsHealth` plays with `CREATING`.\n *\n * Pure on purpose, like its siblings: the route lists, this decides, the\n * spec exercises every branch without a network.\n */\nexport interface ExportMarker {\n /** ISO timestamp the completion marker was written — when the export FINISHED. */\n timeCreated?: string\n}\n\nexport interface ExportsCheck extends HealthCheck {\n /**\n * Completed exports currently retained. A COUNT only — the endpoint is\n * public, and bucket names or object paths have no business in it. Null\n * when the listing itself failed.\n */\n exportCount: number | null\n /** Age of the newest completed export, in days. Null when there is none. */\n newestExportAgeDays: number | null\n}\n\n/**\n * Weekly cadence (Mondays, `scheduled-crons.yml`) + one day of slack — the\n * same budget as `MAX_BACKUP_AGE_DAYS`, for the same reason: a schedule that\n * stops producing is as broken as one that fails.\n */\nexport const MAX_EXPORT_AGE_DAYS = 8\n\nexport function exportsHealth(\n markers: ExportMarker[] | null,\n ms: number,\n now: number = Date.now(),\n): ExportsCheck {\n // A failed listing is degraded, not ok — same rule as `signupsHealth` and\n // `rateLimitsHealth`: an alarm that cannot see the thing it watches must\n // say so rather than report calm.\n if (markers === null) {\n return {\n ok: false,\n ms,\n code: 'exports-unavailable',\n exportCount: null,\n newestExportAgeDays: null,\n }\n }\n\n const newestMs = markers\n .map((marker) => Date.parse(marker.timeCreated ?? ''))\n .filter((time) => Number.isFinite(time))\n .reduce<number | null>((newest, time) => Math.max(newest ?? time, time), null)\n const newestExportAgeDays =\n newestMs === null ? null : Math.round(((now - newestMs) / 86_400_000) * 10) / 10\n\n // The cron exists, so \"no exports\" is a failure, not a fresh start — the\n // first export ran 2026-08-17, before this check first deployed.\n const code =\n newestExportAgeDays === null\n ? 'no-export'\n : newestExportAgeDays > MAX_EXPORT_AGE_DAYS\n ? 'export-stale'\n : undefined\n\n return {\n ok: code === undefined,\n ms,\n ...(code === undefined ? {} : { code }),\n exportCount: markers.length,\n newestExportAgeDays,\n }\n}\n\n/**\n * Org-creation volume, reduced to a health verdict (AGL-1536).\n *\n * The detection layer over the AGL-1534 rate limit: the limiter bounds each\n * uid (3/h) and each IP (10/h), so a distributed farm holding every actor\n * under both caps is invisible to it — but visible in aggregate. This turns\n * \"how many orgs appeared in the trailing hour\" into the same 200/503\n * contract the other health checks speak, so the existing uptime check +\n * alert + email path is the listener, and a wave becomes an email instead of\n * a surprise. The manual response lever is the AGL-1510 signups feature-lock.\n *\n * Pure on purpose, like `backupsHealth`: the route counts, this decides, the\n * spec exercises every branch without a network.\n */\nexport interface SignupsCheck extends HealthCheck {\n /**\n * Orgs created inside the trailing window. A COUNT only — the endpoint is\n * public, and org names, slugs or owners have no business in it. Null when\n * the count query itself failed.\n */\n recentOrgCreations: number | null\n /** The trailing window the count covers, so the body is self-describing. */\n windowMinutes: number\n /** The count above which this reports degraded. */\n threshold: number\n}\n\n/** Trailing window the org-creation count covers. */\nexport const ORG_CREATION_WINDOW_MINUTES = 60\n\n/**\n * Degraded above 10 creations/hour. Calibrated against two facts: production\n * holds 4 orgs TOTAL (2026-08), so the organic baseline is ~zero per hour;\n * and the AGL-1534 per-IP cap is 10/h, so a single maxed-out address can\n * never trip this — 11+/h necessarily means multiple addresses, which is\n * exactly the distributed signature the rate limiter cannot see. If a launch\n * day legitimately beats this, the alert asks a human to glance at the orgs\n * list and enjoy the view; that is the correct outcome, not a false positive.\n */\nexport const MAX_ORG_CREATIONS_PER_WINDOW = 10\n\nexport function signupsHealth(\n recentOrgCreations: number | null,\n ms: number,\n threshold: number = MAX_ORG_CREATIONS_PER_WINDOW,\n): SignupsCheck {\n // A failed count is degraded, not ok — an alarm that cannot see the thing\n // it watches must say so rather than report calm.\n const code =\n recentOrgCreations === null\n ? 'count-unavailable'\n : recentOrgCreations > threshold\n ? 'signup-wave'\n : undefined\n return {\n ok: code === undefined,\n ms,\n ...(code === undefined ? {} : { code }),\n recentOrgCreations,\n windowMinutes: ORG_CREATION_WINDOW_MINUTES,\n threshold,\n }\n}\n\n/**\n * Refused org creations, reduced to a health verdict (AGL-1907).\n *\n * `signupsHealth` above counts the signups that SUCCEEDED. That is the right\n * alarm for a wave that gets through, and the wrong one for a wave that is\n * being contained: when the AGL-1534 limiter starts refusing, org-creation\n * volume goes *down*, so the check that watches volume reads calmer at exactly\n * the moment scripted pressure arrives. The 429s are the signature. Before the\n * doors open on Sep 1 this is also the only thing that can answer \"has the\n * limiter ever actually fired in production\" — a control nobody has seen\n * refuse is a control nobody has tested.\n *\n * Counted from `rateLimits/signupRefused_{minuteBucket}` markers, which carry\n * per-reason counts and no identifiers. This is a separate check rather than\n * more fields on `SignupsCheck` so the AGL-1536 verdict keeps its meaning\n * exactly: a creation wave and a refusal wave are different events with\n * different responses (feature-lock the signups door vs. leave the limiter to\n * do its job and go read the refusal reasons).\n *\n * Pure on purpose, like its siblings: the route queries, this decides.\n */\nexport interface SignupRefusalMarker {\n /** Refused attempts merged into this minute bucket. */\n refusals?: number\n /**\n * Split by what refused: `{ uid?: n, ip?: n, locked?: n, held?: n }`.\n *\n * `uid` and `ip` are the AGL-1534 rate limiter's two caps. `locked` and\n * `held` come from the `beforeUserCreated` blocking function (AGL-2583) — a\n * door the limiter never sees, because it decides account creation on the\n * Identity Platform path before any Aglyn route runs.\n */\n byReason?: Record<string, number>\n /**\n * Account creations the blocking function admitted BLIND: the signups lock\n * could not be read and nothing this instance remembered said it was pulled\n * (AGL-2583).\n *\n * Deliberately its own field rather than another `byReason` entry. Nobody\n * was refused, so folding it into the refusal split would make both counts\n * lie — and it is graded on its own terms below.\n */\n unreadable?: number\n /** When the most recent refusal in this bucket happened. */\n refusedAtMs?: number\n}\n\nexport interface SignupRefusalsCheck extends HealthCheck {\n /**\n * Refused org creations inside the trailing window. A COUNT only — the\n * endpoint is public, and the limiter's keys are a uid and a hashed client\n * IP. Null when the query itself failed.\n */\n refusedSignups: number | null\n /**\n * Which cap did the refusing. A run that is almost all `ip` is one address\n * hammering; a run that is almost all `uid` is many accounts each pushing\n * past three, which is the distributed shape. Null when the query failed.\n */\n refusedByReason: Record<string, number> | null\n /**\n * Account creations decided WITHOUT the signups lock being readable\n * (AGL-2583). Graded on its own, at zero tolerance, because it is\n * categorically different from every entry in `refusedByReason`: those are\n * controls working as designed, this is the platform answering the signups\n * question without being able to consult it, and one of those is one too\n * many. Null when the query itself failed.\n */\n lockUnreadable: number | null\n /** Minutes since the most recent refusal, so the body dates itself. */\n minutesSinceLast: number | null\n /** The trailing window the count covers, so the body is self-describing. */\n windowMinutes: number\n /** The count above which this reports degraded. */\n threshold: number\n}\n\n/**\n * Trailing window the refusal count covers. One hour, matching the limiter's\n * own window (`org-create` is 3/h and 10/h) so the number is readable against\n * the caps that produced it.\n */\nexport const SIGNUP_REFUSAL_WINDOW_MINUTES = 60\n\n/**\n * Degraded above 50 refusals/hour.\n *\n * Not zero, unlike the limiter-degradation alarm: a refusal is the system\n * working, and a real person who fumbles a workspace slug four times in an\n * hour produces one. The number that means something is *sustained* refusal.\n * Calibrated the same way AGL-1536's was: production holds single-digit orgs\n * (2026-08), so organic refusals are ~zero per hour, and 50 is far above any\n * plausible human while being reached in under a minute by a script. One\n * determined address can generate this alone — that is intended, because \"one\n * address is hammering the signup door\" is worth an email even though the\n * limiter is containing it.\n */\nexport const MAX_SIGNUP_REFUSALS_PER_WINDOW = 50\n\n/**\n * The marker field the blocking function counts blind decisions under\n * (AGL-2583).\n *\n * `signupsCreationVerdict` answers `locked` when a completed read found the\n * lever pulled, `held` when a failed read fell back on an earlier one that\n * had, and — the case this names — admits the account when the read failed\n * and nothing remembered said the lever was pulled. That last one refuses\n * nobody, so it is not a `byReason` entry; it is the platform deciding the\n * signups question without being able to consult it, which is the visible\n * edge of a Firestore read outage on the account-creation path. AGL-2581 was\n * three days of that same read failing, and its only trace was a\n * `logger.warn` nothing alerts on.\n */\nexport const SIGNUP_LOCK_UNREADABLE_FIELD = 'unreadable'\n\n/**\n * Blind signups-lock decisions tolerated per window: NONE.\n *\n * Deliberately not calibrated like `MAX_SIGNUP_REFUSALS_PER_WINDOW` above,\n * which is a volume threshold over refusals that mean the system is working.\n * A blind decision is never routine — a single one says the signups lock was\n * unreadable at least once, and the lock is unenforceable for as long as that\n * lasts. False positives here cost a glance at one endpoint.\n */\nexport const MAX_UNREADABLE_SIGNUP_DECISIONS_PER_WINDOW = 0\n\nexport function signupRefusalsHealth(\n markers: SignupRefusalMarker[] | null,\n ms: number,\n now: number = Date.now(),\n threshold: number = MAX_SIGNUP_REFUSALS_PER_WINDOW,\n windowMinutes: number = SIGNUP_REFUSAL_WINDOW_MINUTES,\n unreadableThreshold: number = MAX_UNREADABLE_SIGNUP_DECISIONS_PER_WINDOW,\n): SignupRefusalsCheck {\n // A failed query is degraded, not ok — the same rule `signupsHealth` and\n // `rateLimitsHealth` follow. An alarm that cannot see the thing it watches\n // must not report calm.\n if (markers === null) {\n return {\n ok: false,\n ms,\n code: 'refusals-unavailable',\n refusedSignups: null,\n refusedByReason: null,\n lockUnreadable: null,\n minutesSinceLast: null,\n windowMinutes,\n threshold,\n }\n }\n\n let refusedSignups = 0\n let lockUnreadable = 0\n let newestAtMs: number | null = null\n const refusedByReason: Record<string, number> = {}\n for (const marker of markers) {\n refusedSignups += marker.refusals ?? 0\n // `Number(...) || 0` for the reason the split below gives: a corrupt\n // Firestore field must not turn this into NaN, which compares false\n // against every threshold and would report calm forever.\n lockUnreadable += Number(marker.unreadable) || 0\n for (const [reason, count] of Object.entries(marker.byReason ?? {})) {\n // `Number(...) || 0` rather than a bare add: these come out of Firestore\n // and a corrupt field must not turn the total into NaN, which compares\n // false against the threshold and would report calm forever.\n refusedByReason[reason] =\n (refusedByReason[reason] ?? 0) + (Number(count) || 0)\n }\n const at = marker.refusedAtMs\n if (typeof at === 'number' && (newestAtMs === null || at > newestAtMs)) {\n newestAtMs = at\n }\n }\n\n const over = refusedSignups > threshold\n // The blind count is graded FIRST and reported over the volume code. A wave\n // and an unreadable lock call for opposite responses — leave the limiter\n // alone and go read the refusal reasons, versus go find out why Firestore\n // is unreadable from the account-creation path — so the code has to name\n // the one that is actually happening.\n const blind = lockUnreadable > unreadableThreshold\n const code = blind\n ? 'signups-lock-unreadable'\n : over\n ? 'signup-refusal-wave'\n : undefined\n return {\n ok: code === undefined,\n ms,\n ...(code === undefined ? {} : { code }),\n refusedSignups,\n refusedByReason,\n lockUnreadable,\n minutesSinceLast:\n newestAtMs === null\n ? null\n : Math.max(0, Math.floor((now - newestAtMs) / 60_000)),\n windowMinutes,\n threshold,\n }\n}\n\n/**\n * NOBODY GOT AN ACCOUNT WHILE PEOPLE WERE TRYING (AGL-2583).\n *\n * `signupsHealth` watches for a FLOOD. Its healthiest possible reading —\n * zero orgs created in the trailing hour — is also what total failure looks\n * like, so the monitor named \"signups\" reports green precisely when nobody\n * can sign up. That is not a hypothetical: account creation was refused for\n * every visitor from Sep 1 to Sep 4 (AGL-2581) and the signup check was\n * green through all of it, because zero is comfortably under ten.\n *\n * ## Zero is only a signal next to a denominator\n *\n * Alone, \"no accounts this hour\" describes a quiet Tuesday night as loudly as\n * an outage, and an alarm that fires on quiet nights is an alarm everyone\n * mutes. What makes zero mean something is TRAFFIC: people arrived at the\n * signup page and none of them ended up with an account.\n *\n * ## The denominator is ATTEMPTS, and was once SERVES (AGL-2714)\n *\n * It began as signup-page serves — `signupServed_` markers written when the\n * page asks `/api/lockdown-status?feature=signups`, the one server request\n * every rendering makes. That counts LOOKERS: crawlers, link previews, a\n * person reading the plan names and closing the tab. On 2026-09-09 it paged\n * \"Signups is down\" on an hour with six such hits and no account, while the\n * signup page rendered clean, the lockdown was off, all seven auth doors were\n * green and not one signup had been refused. One of the six was the\n * diagnostic load made while investigating the alert.\n *\n * At this platform's conversion rate a quiet hour and a broken door produce\n * the same reading, so no threshold on serves could separate them. The\n * quantity was wrong, not the number.\n *\n * `signupAttempted_` markers are the right one: written at the top of\n * `/api/orgs/create` before any outcome is known, so an attempt means\n * somebody authenticated, filled the form and asked for an org. Arrivals with\n * no account is then a statement about the door rather than about demand.\n *\n * Writing the marker BEFORE the outcome is what makes this stronger than the\n * refusal check beside it: `signupRefusals` counts what the route chose to\n * turn away, so a request that 500s, hangs, or dies between the limiter and\n * the write leaves no refusal at all — and leaves an attempt with no org,\n * which is exactly this verdict.\n *\n * Not GA4, for the reason it was never GA4: its API needs a service account\n * and an OAuth round trip from a public health route, its data lands with\n * hours of latency, and a monitor that depends on an analytics vendor is a\n * monitor that reports an outage every time that vendor has one.\n *\n * The denominator UNDER-counts on purpose and that is the safe direction: a\n * marker write is fire-and-forget and a lost one is simply not counted.\n * Under-counting can only make this check quieter, never louder — it cannot\n * invent attempts that did not happen, so it cannot invent an outage.\n *\n * Serves are still read and still reported, as CONTEXT. They cost nothing\n * extra and they answer the second question an on-call person asks: was\n * anybody even looking.\n *\n * Pure on purpose, like its siblings: the route counts, this decides.\n */\nexport interface SignupServedMarker {\n /** Signup-page serves merged into this minute bucket. */\n serves?: number\n /** When the most recent serve in this bucket happened. */\n servedAtMs?: number\n}\n\nexport interface SignupAttemptMarker {\n /** Org-creation attempts merged into this minute bucket. */\n attempts?: number\n /** When the most recent attempt in this bucket happened. */\n attemptedAtMs?: number\n}\n\nexport interface SignupDroughtCheck extends HealthCheck {\n /**\n * Signup pages served inside the trailing window — the denominator. A\n * COUNT only; the markers carry no visitor, no IP and no referrer. Null\n * when the query itself failed.\n */\n signupPagesServed: number | null\n /**\n * Org-creation attempts inside the same window — the denominator the\n * verdict is actually taken on (AGL-2714). Null when that query failed.\n */\n signupAttempts: number | null\n /** Accounts (orgs) actually created in the same window. The numerator. */\n orgCreations: number | null\n /** The trailing window all three numbers cover, so the body is self-describing. */\n windowMinutes: number\n /**\n * Attempts below which zero creations is treated as a quiet hour rather\n * than an outage. Named `minimumTraffic` still, because it is the same\n * question — how much has to have happened before zero means something —\n * and renaming it would break every monitor body that reads it.\n */\n minimumTraffic: number\n}\n\n/**\n * Trailing window the drought verdict covers. One hour, matching the sibling\n * checks so the three numbers on this endpoint are all read against the same\n * clock — and long enough that a single slow afternoon does not page anyone.\n */\nexport const SIGNUP_DROUGHT_WINDOW_MINUTES = 60\n\n/**\n * Attempts in the window below which zero accounts means nothing.\n *\n * Three, against attempts rather than the five it once meant against serves\n * (AGL-2714). The unit changed, so the number had to be re-argued rather than\n * carried over.\n *\n * Not one: a browser can lose a request, and a person can start the form and\n * abandon it between authenticating and the org write. Not ten either — at a\n * handful of signups a week, waiting for ten attempts to fail would mean\n * waiting days to notice a door that stopped opening, which is the failure\n * this check exists to catch inside an hour.\n *\n * Three distinct requests to create an org, none of which produced one and\n * none of which was refused, is a door that does not open. That reading has\n * no quiet-hour interpretation: nobody attempts three times by browsing.\n */\nexport const MIN_SIGNUP_TRAFFIC_FOR_DROUGHT = 3\n\nexport function signupDroughtHealth(\n markers: SignupServedMarker[] | null,\n attemptMarkers: SignupAttemptMarker[] | null,\n orgCreations: number | null,\n ms: number,\n minimumTraffic: number = MIN_SIGNUP_TRAFFIC_FOR_DROUGHT,\n windowMinutes: number = SIGNUP_DROUGHT_WINDOW_MINUTES,\n): SignupDroughtCheck {\n // Serves are context, not the verdict, so they are summed wherever they are\n // readable and reported as null when they are not — a missing context line\n // must not decide anything on its own.\n let signupPagesServed: number | null = null\n if (markers !== null) {\n signupPagesServed = 0\n for (const marker of markers) {\n // `Number(...) || 0` for the reason the sibling gives: a corrupt\n // Firestore field must not turn the total into NaN, which compares\n // false against every threshold and would report calm forever.\n signupPagesServed += Number(marker.serves) || 0\n }\n }\n // A failed ATTEMPT query is degraded, not ok — the same rule the siblings\n // follow. Without the denominator this check has no opinion at all, and a\n // check with no opinion must not spend it saying everything is fine.\n if (attemptMarkers === null) {\n return {\n ok: false,\n ms,\n code: 'traffic-unavailable',\n signupPagesServed,\n signupAttempts: null,\n orgCreations,\n windowMinutes,\n minimumTraffic,\n }\n }\n let signupAttempts = 0\n for (const marker of attemptMarkers) {\n signupAttempts += Number(marker.attempts) || 0\n }\n if (orgCreations === null) {\n return {\n ok: false,\n ms,\n code: 'count-unavailable',\n signupPagesServed,\n signupAttempts,\n orgCreations: null,\n windowMinutes,\n minimumTraffic,\n }\n }\n // The whole verdict: people ASKED for an org and NOT ONE of them got it.\n const drought = signupAttempts >= minimumTraffic && orgCreations === 0\n return {\n ok: !drought,\n ms,\n ...(drought ? { code: 'signup-drought' } : {}),\n signupPagesServed,\n signupAttempts,\n orgCreations,\n windowMinutes,\n minimumTraffic,\n }\n}\n\n/**\n * Rate-limiter degradation, reduced to a health verdict (AGL-1693).\n *\n * `consumeRateLimit` fails SOFT: when the Firestore counter is unreachable it\n * answers from a per-instance `Map` and flags `degraded`. AGL-1679 made that\n * findable after the fact — on recovery the instance writes one summary\n * document `rateLimits/degraded_{minuteBucket}` — but nothing reads it, so a\n * degraded window during the beta still passes unnoticed in real time. This\n * turns \"did any limiter fall back recently\" into the same 200/503 contract\n * the sibling checks speak, so the AGL-1502 uptime check + alert + email path\n * is the listener.\n *\n * **The window is the whole design.** Markers carry a 30-day `expiresAt`, so\n * \"does a marker exist\" would leave the endpoint red for a month after a\n * thirty-second blip — a recovered incident permanently paging is how a check\n * gets muted, and muted is worse than absent. That is the opposite of the\n * backups check, which stays red BY DESIGN because a missing restore point is\n * a condition that persists until someone fixes it (DISASTER_RECOVERY.md gap\n * 2). A degraded limiter window is an EVENT: it is over, and what is owed is\n * a notification, not a standing alarm. So the verdict covers a trailing\n * window and clears itself.\n *\n * Filtering on `lastAtMs` rather than the marker's id: the id is bucketed on\n * `firstAtMs`, so a long episode's document is stamped with the minute the\n * outage STARTED. An id-range window would miss exactly the longest episodes,\n * which are the ones worth waking up for.\n *\n * Pure on purpose, like `backupsHealth` and `signupsHealth`: the route\n * queries, this decides, the spec exercises every branch without a network.\n */\nexport interface RateLimitDegradationMarker {\n /** Calls that fell back to the per-instance cap. */\n calls?: number\n /** Instance-episodes merged into this minute bucket. */\n episodes?: number\n /** When the episode's last fallback happened. */\n lastAtMs?: number\n /** The failure code the episode ended on. */\n code?: string\n}\n\nexport interface RateLimitsCheck extends HealthCheck {\n /**\n * Fallback calls inside the trailing window. A COUNT only — the endpoint is\n * public, and limiter keys are hashed client IPs. Null when the query\n * itself failed.\n */\n degradedCalls: number | null\n /** Instance-episodes inside the window. 2+ means several instances saw it. */\n degradedEpisodes: number | null\n /** Minutes since the most recent fallback, so the body dates itself. */\n minutesSinceLast: number | null\n /** The trailing window the counts cover, so the body is self-describing. */\n windowMinutes: number\n /** The count above which this reports degraded. */\n threshold: number\n}\n\n/**\n * Trailing window the degradation counts cover.\n *\n * Bounded from below by the alert path, not by taste. The probe memoizes for\n * `HEALTH_PROBE_TTL_MS` (2 minutes), the slowest check on this door runs\n * every 15, and the alert policy wants a minute of sustained failure — so a\n * window shorter than ~18 minutes can go red and green again before that\n * check tells anyone, which is a check that reports nothing while looking\n * like it works. 30 leaves margin and still clears itself inside half an hour.\n */\nexport const RATE_LIMIT_DEGRADED_WINDOW_MINUTES = 30\n\n/**\n * Any fallback at all is worth an email. Unlike the signup alarm there is no\n * organic baseline to clear: a healthy deployment produces exactly zero of\n * these, and one is already the statement \"for some requests, the global cap\n * was not the cap\". Firestore retries transactions internally, so a failure\n * that reaches this code is not a single unlucky contention.\n */\nexport const MAX_DEGRADED_CALLS_PER_WINDOW = 0\n\nexport function rateLimitsHealth(\n markers: RateLimitDegradationMarker[] | null,\n ms: number,\n now: number = Date.now(),\n threshold: number = MAX_DEGRADED_CALLS_PER_WINDOW,\n windowMinutes: number = RATE_LIMIT_DEGRADED_WINDOW_MINUTES,\n): RateLimitsCheck {\n // A failed query is degraded, not ok — same rule as `signupsHealth`. An\n // alarm that cannot see the thing it watches must not report calm, and this\n // one reads the collection that just failed for the limiter.\n if (markers === null) {\n return {\n ok: false,\n ms,\n code: 'markers-unavailable',\n degradedCalls: null,\n degradedEpisodes: null,\n minutesSinceLast: null,\n windowMinutes,\n threshold,\n }\n }\n\n const cutoff = now - windowMinutes * 60_000\n // The window is re-applied here rather than trusted from the query, so the\n // rule lives in one spec-covered place and a marker whose `lastAtMs` is\n // missing cannot silently count as recent.\n const recent = markers.filter(\n (marker) =>\n typeof marker.lastAtMs === 'number' && marker.lastAtMs >= cutoff,\n )\n\n const degradedCalls = recent.reduce(\n (total, marker) => total + (Number(marker.calls) || 0),\n 0,\n )\n const degradedEpisodes = recent.reduce(\n (total, marker) => total + (Number(marker.episodes) || 0),\n 0,\n )\n const lastAtMs = recent.reduce<number | null>(\n (latest, marker) => Math.max(latest ?? -Infinity, marker.lastAtMs as number),\n null,\n )\n\n return {\n ok: degradedCalls <= threshold,\n ms,\n ...(degradedCalls <= threshold ? {} : { code: 'rate-limiter-degraded' }),\n degradedCalls,\n degradedEpisodes,\n minutesSinceLast:\n lastAtMs === null\n ? null\n : Math.round(((now - lastAtMs) / 60_000) * 10) / 10,\n windowMinutes,\n threshold,\n }\n}\n\n/**\n * Uncaught server-side errors, reduced to a health verdict (AGL-1921).\n *\n * ## The gap this closes\n *\n * Of the eleven alert policies in `aglyn-main`, not one could see a server\n * error rate. Every check is a LIVENESS probe on one URL: `/api/health` can be\n * green while `/api/billing/checkout` 500s for every paying customer, which is\n * the single most likely shape of a launch-day incident and the one nothing\n * could page on. Server errors lived only in the Vercel runtime log, which\n * retains ~60 minutes and drains nowhere (AGL-1799).\n *\n * The first pass gave both apps an `onRequestError` hook that forwards each\n * error to a `server-errors` log in `aglyn-main`. That closed the CAPTURE gap\n * and left the reading gap wide open, and the reading gap is the one this repo\n * keeps losing to — `/api/health/crons` was correct about a broken job for\n * fifty-one hours because nothing asked it. Measured 2026-08-24: the\n * production credential is refused `entries:list` on that log\n * (`403 Permission denied for all log views`), so no probe here can read what\n * the hook writes. The only reader that log can have is a GCP alert policy\n * created by hand.\n *\n * So the hook ALSO counts into a `rateLimits/serverError_{minute}` marker,\n * and this grades those counts into the same 200/503 contract the sibling\n * checks speak — which hands the signal to readers that already exist and\n * already run: the 15-minute GitHub uptime probe, the external keyword\n * monitors, and `docs.aglyn.com/status`.\n *\n * ## What it can and cannot see, exactly\n *\n * It counts what `onRequestError` counts: an uncaught throw in a render or a\n * route handler, in the nodejs runtime. It does NOT count an error that kills\n * the process before the hook runs, a platform-level 5xx that never reaches\n * our code (function timeout, OOM, cold-start 502), or anything thrown in the\n * edge runtime. A Vercel log drain sees all of those and remains the real fix.\n * This is the arm that ships without a purchase, and its blind spots are\n * written down rather than glossed — `docs/UPTIME_AND_SLA.md`.\n *\n * ## The third state is the one that matters here\n *\n * A monitoring path that cannot measure MUST NOT report a zero. `null` markers\n * — the query failed — is `errors-unavailable` and degraded, never \"0 errors\".\n * That is the same rule `rateLimitsHealth` and `billingWebhookHealth` follow,\n * and on this check it is the difference between \"nothing is wrong\" and \"we\n * have no idea\", which during a 5xx spike are the two readings furthest apart.\n *\n * ## It must be able to go GREEN\n *\n * A spike is an EVENT, not a condition (the AGL-1843 rule). Markers live seven\n * days, so \"does a marker exist\" would hold this red for a week after a\n * one-minute blip, and a check that stays red after recovery is a check that\n * gets muted. The verdict covers a trailing window and clears itself.\n *\n * Pure on purpose, like its siblings: the route queries, this decides, the\n * spec exercises every branch without a network.\n */\nexport interface ServerErrorMarker {\n /** Uncaught server errors merged into this minute bucket. */\n errors?: number\n /** Split by deployment — `{ 'console-web': n, 'tenant-web': n }`. */\n byService?: Record<string, number>\n /** When the most recent error in this bucket happened. */\n erroredAtMs?: number\n}\n\nexport interface ServerErrorsCheck extends HealthCheck {\n /**\n * Uncaught server errors inside the trailing window. A COUNT only — the\n * endpoint is public, and messages, stacks and route patterns have no\n * business in it (they are in the Logging entry, which is not public). Null\n * when the query itself failed, and null is NEVER zero.\n */\n serverErrors: number | null\n /**\n * Which deployment produced them. The first thing an incident needs, and it\n * is a deployment name rather than anything about a request. Null when the\n * query failed.\n */\n byService: Record<string, number> | null\n /** Minutes since the most recent error, so the body dates itself. */\n minutesSinceLast: number | null\n /** The trailing window the count covers, so the body is self-describing. */\n windowMinutes: number\n /** The count above which this reports degraded. */\n threshold: number\n}\n\n/**\n * Trailing window the error count covers.\n *\n * Bounded from below by the alert path, not by taste — the same arithmetic as\n * `RATE_LIMIT_DEGRADED_WINDOW_MINUTES`: the probe memoizes for 2 minutes, the\n * slowest external check runs every 15, and an alert policy wants a minute of\n * sustained failure, so a window under ~18 minutes can go red and green again\n * before that check tells anyone. 30 leaves margin and still clears itself\n * inside half an hour of the last error.\n */\nexport const SERVER_ERROR_WINDOW_MINUTES = 30\n\n/**\n * Errors tolerated in the window.\n *\n * NOT zero, unlike the rate-limiter and billing alarms, and the difference is\n * real rather than a matter of nerve. Those two watch controls that a healthy\n * deployment exercises exactly never. This one watches a count that a single\n * cold-start Firestore deadline, or one visitor's malformed request reaching a\n * route that throws instead of 400ing, can make non-zero on a perfectly good\n * day — and an alarm that pages on the first of those gets muted before the\n * one that matters arrives.\n *\n * Five over thirty minutes is chosen against the only two facts available.\n * Production serves single-digit orgs (2026-08), so the organic rate of\n * UNCAUGHT server errors is ~zero — the codebase answers its expected failures\n * with 4xx, and reaching this hook means nothing handled it. And the shape\n * being caught is \"a route is failing for real users\", which does not produce\n * six errors an hour; it produces six in a minute. There is deliberately no\n * pretence of calibration: the beta window exists to produce a baseline, and\n * `SERVER_ERROR_ALARM_MAX_ERRORS` moves this without a deploy when it does.\n */\nexport const MAX_SERVER_ERRORS_PER_WINDOW = 5\n\nexport function serverErrorsHealth(\n markers: ServerErrorMarker[] | null,\n ms: number,\n now: number = Date.now(),\n threshold: number = MAX_SERVER_ERRORS_PER_WINDOW,\n windowMinutes: number = SERVER_ERROR_WINDOW_MINUTES,\n): ServerErrorsCheck {\n // ⚠️ THE MEASURED ZERO. A failed query is degraded, never calm, and on this\n // check that is not a convention borrowed from its siblings — it is the\n // whole reason the check is trustworthy. \"We could not count the errors\"\n // and \"there were no errors\" are the same JSON if this branch is missing,\n // and the first is most likely precisely when the second is false.\n if (markers === null) {\n return {\n ok: false,\n ms,\n code: 'errors-unavailable',\n serverErrors: null,\n byService: null,\n minutesSinceLast: null,\n windowMinutes,\n threshold,\n }\n }\n\n const cutoff = now - windowMinutes * 60_000\n // The window is re-applied here rather than trusted from the query, so the\n // rule lives in one spec-covered place and a marker whose `erroredAtMs` is\n // missing cannot silently count as recent. `strictNullChecks` is off\n // repo-wide, so an absent field would otherwise compare as 0 and fall\n // outside every window forever — a marker that can never be counted.\n const recent = markers.filter(\n (marker) =>\n typeof marker.erroredAtMs === 'number' && marker.erroredAtMs >= cutoff,\n )\n\n const byService: Record<string, number> = {}\n let serverErrors = 0\n let lastAtMs: number | null = null\n for (const marker of recent) {\n // `Number(...) || 0` rather than a bare add: these come out of Firestore\n // and a corrupt field must not turn the total into NaN, which compares\n // false against the threshold and would report calm forever.\n serverErrors += Number(marker.errors) || 0\n for (const [service, count] of Object.entries(marker.byService ?? {})) {\n byService[service] = (byService[service] ?? 0) + (Number(count) || 0)\n }\n const at = marker.erroredAtMs as number\n if (lastAtMs === null || at > lastAtMs) lastAtMs = at\n }\n\n const over = serverErrors > threshold\n return {\n ok: !over,\n ms,\n ...(over ? { code: 'server-error-spike' } : {}),\n serverErrors,\n byService,\n minutesSinceLast:\n lastAtMs === null\n ? null\n : Math.max(0, Math.round(((now - lastAtMs) / 60_000) * 10) / 10),\n windowMinutes,\n threshold,\n }\n}\n\n/**\n * How long a health door may reuse its verdict (AGL-2947).\n *\n * Cut from one budget: an outage must reach a person within ten minutes. A\n * green verdict computed just before the onset keeps being served for this\n * long, and only then can a prober see red. The fastest prober on most doors\n * is UptimeRobot, every five minutes, which then confirms before it notifies.\n * So the worst case is this + 5 + that confirmation: at two minutes the budget\n * holds for any confirmation delay up to three. The five minutes every door\n * used before spent the whole ten minutes before a notification could leave.\n *\n * It still bounds what a public door can be made to cost: at most one\n * computation per instance per two minutes, and {@link memoizeWithTtl} shares\n * a computation among the callers that arrive while it runs.\n */\nexport const HEALTH_PROBE_TTL_MS = 2 * 60_000\n\n/**\n * Reuse a probe result for `ttlMs`, per instance.\n *\n * The endpoint is public and unauthenticated, so an unthrottled dependency\n * check is a free amplifier: anyone could turn a loop into database reads on\n * our account. This bounds the cost to one probe per instance per interval\n * however hard it is hit.\n *\n * Callers that arrive while a probe is running share it rather than starting\n * their own. The uptime check's regions, UptimeRobot and a HEAD twin routinely\n * land within the same second of an expired entry, and each used to pay for a\n * full probe.\n *\n * FAILURES ARE CACHED TOO, deliberately. A dependency that is down will be\n * down for the next caller as well, and re-probing per request turns an\n * outage into a stampede against the thing that is already failing. A probe\n * that THROWS is not cached: its callers all see the throw, and the next\n * caller probes again.\n */\nexport function memoizeWithTtl<T>(\n ttlMs: number,\n probe: () => Promise<T>,\n now: () => number = Date.now,\n): () => Promise<T> {\n let cached: { at: number; value: T } | null = null\n let inFlight: Promise<T> | null = null\n return async () => {\n const at = now()\n const elapsed = cached ? at - cached.at : Infinity\n // `elapsed >= 0` is not paranoia. A plain `elapsed < ttlMs` treats a\n // BACKWARDS clock as permanently fresh — the entry never expires and the\n // endpoint reports a stale verdict forever, which on a health check means\n // reporting healthy through an outage. Serverless instances outlive more\n // clock weirdness than a local run suggests.\n if (cached && elapsed >= 0 && elapsed < ttlMs) return cached.value\n if (inFlight) return inFlight\n const flight = (async () => {\n const value = await probe()\n cached = { at, value }\n return value\n })()\n inFlight = flight\n // Cleared once settled, and only if it is still this flight. Clearing\n // inside the flight would run BEFORE the assignment above when `probe`\n // throws synchronously, and leave a rejected promise in the slot forever.\n const clear = () => {\n if (inFlight === flight) inFlight = null\n }\n flight.then(clear, clear)\n return flight\n }\n}\n\n/**\n * The error beacon's own liveness, reduced to a health verdict (AGL-1923).\n *\n * ## The failure this exists for\n *\n * The `Client error beacon` alert policy is a log-match: it fires when an\n * entry APPEARS in `client-errors`. A policy of that shape can only report\n * presence, so if the thing writing the entries stops writing, the policy\n * goes quiet — and quiet is the reading it also gives when everything is\n * healthy. **A dead beacon is indistinguishable from zero errors**, and\n * `reportClientErrors` has three fail-soft paths that each end in a\n * `console.warn` to a log that retains an hour and drains nowhere\n * (AGL-1799). Error Reporting would show zero errors; the policy would stay\n * silent; both readings look exactly like a clean launch.\n *\n * ## Why this shape and not a metric-absence policy\n *\n * AGL-1923 proposed a log-based metric plus `conditionAbsent`. This is the\n * same guarantee with two properties that one lacks. It is PROVABLE — the\n * verdict is graded synchronously against the write's own status, so the\n * degraded branch can be induced and observed rather than waited out over an\n * absence window. And it needs nothing new to run it: the uptime probe is\n * what winds the dead-man's switch, so there is no cron that can itself stop\n * without anyone noticing (which would only move the problem one layer out).\n *\n * ## It must be able to go GREEN\n *\n * The condition is an EVENT, not a state. A failed write degrades; the next\n * successful write clears it, within one probe TTL — nothing latches and\n * there is no retention window to age out of. That is the AGL-1843 rule\n * applied before the fact: the clearing event is named, and it is \"the next\n * heartbeat that reaches Cloud Logging\". The durable marker AGL-2713 added is\n * evidence of a LANDING, never of a failure, so it can only ever shorten the\n * red — a stale one forgives nothing.\n *\n * Pure on purpose, like its siblings: the route writes, this decides, the\n * spec exercises every branch without a network.\n */\nexport interface BeaconCheck extends HealthCheck {\n /**\n * The log id the heartbeat targets, so the body names what to query during\n * an incident rather than making someone go and find it.\n */\n logId: string\n /** Which deployment's credential was exercised — console and tenant differ. */\n service: string\n /**\n * How many heartbeat writes this probe attempted. Two means the first one\n * missed, which is a fact a green row must still carry: a credential that\n * needs a second attempt every time is on its way to failing outright.\n */\n attempts: number\n /**\n * Minutes since a heartbeat last durably landed, or null when the answer\n * could not be established — which includes \"none ever has\". Null is what\n * makes a missed heartbeat unforgivable, so the two cases fold safely.\n */\n minutesSinceHeartbeat: number | null\n /** How long a missed heartbeat is tolerated before this door reds. */\n graceMinutes: number\n}\n\n/**\n * Codes that mean the heartbeat got no ANSWER, as opposed to a refusal\n * (AGL-2713).\n *\n * The distinction is the whole of the tolerance below, so it is named once\n * and asserted directly rather than being spelled inline at the branch.\n *\n * - `credential-unavailable` — the admin app is configured and its credential\n * simply did not mint a token on this attempt. On a cold lambda that is the\n * first outbound TLS of the instance's life, and it is the measured shape of\n * both 2026-09-09 blips.\n * - `transport-*` — the Logging write never came back inside its 4s deadline,\n * or the fetch layer failed. Same class: nothing was decided.\n *\n * Everything else is a DECISION and must never be tolerated. `http-401` and\n * `http-403` are a lost `logging.logEntries.create` grant, `http-429` is\n * quota, and `no-credential` is a deployment whose `FIREBASE_*` env is absent\n * — every one of them permanent until a person fixes it, and every one of\n * them a state in which browser errors are being collected by nothing. So is\n * `heartbeat-unavailable`, which means a writer documented never to throw\n * threw.\n */\nexport function isTransientBeaconCode(code: string | undefined): boolean {\n return code === 'credential-unavailable' || (code ?? '').startsWith('transport-')\n}\n\n/**\n * How a missed heartbeat is graded (AGL-2713).\n *\n * `landedAtMs` is the durable, cross-instance fact — the last time ANY\n * instance of this deployment put an entry in the log. It has to be durable:\n * the probe memo dies with the lambda, so an in-process counter is at one on\n * every cold start and would forgive nothing at all on the one shape this\n * exists for.\n */\nexport interface BeaconTolerance {\n /** Heartbeat writes this probe made. Defaults to the single-attempt shape. */\n attempts?: number\n /** When a heartbeat last durably landed; null when that cannot be shown. */\n landedAtMs?: number | null\n /** How long a miss is forgiven after the last landing. Zero forgives none. */\n graceMs?: number\n now?: number\n}\n\n/**\n * ## Tolerating ONE miss without becoming a door that cannot red (AGL-2713)\n *\n * `/api/health/error-beacon` on the console answered `503 no-credential` twice\n * on 2026-09-09 — 15:12 and 17:22 UTC, each shortly after new capacity from\n * the 15:08 deploy, each self-clearing within minutes, with the beacon code\n * path changed by one comment line since the previous release. Between them\n * they took the 15-minute uptime workflow red and opened four GCP alert\n * events, on a monitor whose entire value is that somebody believes it.\n *\n * The tolerance is deliberately NOT \"forgive a failure\". It is three\n * conditions that must all hold, and each one is what keeps the door able to\n * go red:\n *\n * 1. the code says nothing was decided ({@link isTransientBeaconCode});\n * 2. a heartbeat is PROVEN to have landed within `graceMs` — proof being a\n * durable marker a successful write left behind, so a deployment whose\n * credential never worked has nothing to forgive with and reds on its\n * first probe;\n * 3. that proof is not from the future, because a clock that ran backwards\n * must not mint an unbounded grace.\n *\n * Absence of proof reds. A marker store that cannot be read reds. That is the\n * same rule the `*-unavailable` arms of every sibling grader apply, pointed at\n * the evidence for calm rather than at the evidence for alarm.\n */\nexport function beaconHealth(\n write: { ok: boolean; code?: string } | null,\n logId: string,\n service: string,\n ms: number,\n tolerance: BeaconTolerance = {},\n): BeaconCheck {\n const {\n attempts = 1,\n landedAtMs = null,\n graceMs = 0,\n now = Date.now(),\n } = tolerance\n const sinceLanded = landedAtMs === null ? null : now - landedAtMs\n const base = {\n ms,\n logId,\n service,\n attempts,\n minutesSinceHeartbeat:\n sinceLanded === null\n ? null\n : Math.max(0, Math.round((sinceLanded / 60_000) * 10) / 10),\n graceMinutes: Math.round(graceMs / 60_000),\n }\n // A null result is degraded by contract, the same rule `signupsHealth` and\n // `rateLimitsHealth` follow: an alarm that cannot see the thing it watches\n // must not report calm. Here it is stronger than a convention — \"we could\n // not determine whether the beacon works\" IS the AGL-1923 condition.\n if (write === null) {\n return { ...base, ok: false, code: 'heartbeat-unavailable' }\n }\n if (write.ok) return { ...base, ok: true }\n const code = write.code ?? 'heartbeat-failed'\n if (\n isTransientBeaconCode(code) &&\n sinceLanded !== null &&\n sinceLanded >= 0 &&\n sinceLanded <= graceMs\n ) {\n // Green, and saying so out loud. A tolerance nobody can see on the board\n // is indistinguishable from a door that was widened until it stopped\n // reporting, which is the failure this whole change had to avoid being.\n return { ...base, ok: true, code: 'heartbeat-missed' }\n }\n return { ...base, ok: false, code }\n}\n\n/**\n * A tenant page render, reduced to a health verdict (AGL-2486).\n *\n * ## The gap this closes\n *\n * `marketing-home` (`aglyn.com/`) and `customer-site` (`demo.aglyn.app/`) are\n * the only two external checks that watched a REAL PAGE rather than a health\n * endpoint. Both sat at 0% from 2026-08-21, because bot protection answered\n * Google's checkers with a 429 checkpoint. The health-path firewall bypass\n * that recovered `tenant-health` and `beacon-heartbeat tenant` cannot reach\n * them: they fetch `/`, not `/api/health/*`.\n *\n * The alternative was allowlisting Google's 54 checker IPs — rejected because\n * an IP-valued bypass rule is one the drift checker cannot meaningfully\n * assert, and Google rotates the list. So the render moves to where the\n * bypass already is: an endpoint under `/api/health` that renders the page\n * server-side and grades the result.\n *\n * ## Why this is not \"200 because the route exists\"\n *\n * The caller runs the SAME loader the catch-all page route runs, against the\n * SAME host, and this grades what came back. A page that resolves no host,\n * 404s, redirects away, or composes an EMPTY node tree is a failure — and\n * those are the shapes an outage actually takes. A canary that cannot go red\n * for a broken page is worse than the dark check it replaces, because it\n * reports a confidence it has not earned.\n *\n * ## What is deliberately NOT asserted\n *\n * Never a string from the page. The marker is STRUCTURAL — a host resolved\n * and a non-empty composed node tree. Asserting copy would page whoever is\n * on call for an ordinary content edit, and this endpoint is public, so\n * customer content must not appear in its body either. `nodeCount` is a\n * count, which is enough to tell \"rendered nothing\" from \"rendered\" without\n * disclosing what was rendered.\n */\nexport type RenderOutcome =\n /** The loader returned props. `nodeCount` is the composed node total. */\n | { kind: 'rendered'; hostResolved: boolean; nodeCount: number }\n /** The loader chose the not-found branch. */\n | { kind: 'not-found' }\n /** The loader chose to redirect — a home page should never do this. */\n | { kind: 'redirect' }\n /** The loader threw, or could not be reached at all. */\n | { kind: 'unavailable' }\n /**\n * No target host is configured. A failure on purpose: \"we are watching\n * nothing\" must never read the same as \"the page is fine\".\n */\n | { kind: 'not-configured' }\n\nexport interface RenderCheck extends HealthCheck {\n /**\n * Composed nodes the page produced. A COUNT, never the nodes: this endpoint\n * is public and the nodes are the customer's page. Zero is the failure.\n */\n nodeCount: number\n /**\n * Which tenant host was rendered — a CONFIGURED host, never one derived\n * from the request, so no caller can choose what we render. Empty string\n * when nothing is configured.\n */\n host: string\n}\n\nexport function renderHealth(\n outcome: RenderOutcome,\n host: string,\n ms: number,\n): RenderCheck {\n const base = { ms, host }\n switch (outcome.kind) {\n case 'rendered':\n // Both halves are load-bearing. A host that resolved but composed\n // nothing is a blank page served with a 200 — the exact outage a\n // reachability ping calls healthy.\n if (!outcome.hostResolved) {\n return { ...base, ok: false, code: 'host-unresolved', nodeCount: outcome.nodeCount }\n }\n if (outcome.nodeCount <= 0) {\n return { ...base, ok: false, code: 'rendered-empty', nodeCount: 0 }\n }\n return { ...base, ok: true, nodeCount: outcome.nodeCount }\n case 'not-found':\n return { ...base, ok: false, code: 'not-found', nodeCount: 0 }\n case 'redirect':\n return { ...base, ok: false, code: 'redirected', nodeCount: 0 }\n case 'not-configured':\n return { ...base, ok: false, code: 'not-configured', nodeCount: 0 }\n case 'unavailable':\n default:\n // Same rule as `beaconHealth`: \"we could not determine whether the page\n // renders\" is a failure, never calm.\n return { ...base, ok: false, code: 'render-unavailable', nodeCount: 0 }\n }\n}\n\n/**\n * Billing webhook delivery, reduced to a health verdict (AGL-1924).\n *\n * ## The gap this closes\n *\n * Of the alert policies in `aglyn-main`, none watched billing. Sept 1 opens to\n * PAYING customers, and the billing path is the one place where a failure\n * nobody notices converts directly into lost revenue and a support incident\n * per affected customer. The webhook has a documented history of failing\n * exactly that way: AGL-1551 (every delivery rejected `400 Invalid signature`\n * for a week, behind a green \"Active\" badge), AGL-1560 (a secret roll 400ing\n * roughly half of deliveries), AGL-1552 (the replay window making every retry\n * unreceivable), AGL-1798 (`charge.refunded` never subscribed). Each is a\n * total or near-total failure of the billing event path, and not one of them\n * would have paged anyone.\n *\n * ## Why the denominator has to come from Stripe\n *\n * `stripeEvents` alone cannot answer this. `route.ts` returns 400 BEFORE\n * claiming the idempotency document, so a rejected delivery writes nothing —\n * an empty collection reads identically whether nothing happened or\n * everything was refused. That is precisely how the 2026-08-14 checklist tick\n * came out green while AGL-1551 was live. Stripe supplies what it TRIED to\n * deliver; we supply what we processed; the gap is the failure.\n *\n * ## Why this cannot false-page on a quiet night\n *\n * The verdict never keys on the ABSENCE of events. At beta volume a night\n * with zero deliveries is legitimate, and a naive freshness rule would page\n * on it — the AGL-1843 mistake in a new costume. `undelivered` is a count of\n * deliveries Stripe attempted AND failed, so a quiet window scores zero and\n * reads healthy for the right reason. `emitted` and `processed` are carried\n * for the human reading the incident and are DELIBERATELY not inputs to the\n * verdict: no floor for them exists yet, and inventing one before the beta\n * produces a baseline would be a threshold nobody could defend.\n *\n * ## Division of labour with the AGL-1906 audit\n *\n * `tools/scripts/lib/stripe-webhook-health.mjs` asserts the endpoint's full\n * subscribed-event list against `WEBHOOK_EVENTS`. That list is deliberately\n * NOT duplicated here — a second copy is the exact failure its own comment\n * warns about, and a subscription set is config that changes on deploys, not\n * a thing that changes minute to minute. This watches the two facts that DO\n * move continuously: whether the destination is still enabled, and whether\n * deliveries are landing.\n *\n * ## It must be able to go GREEN\n *\n * Everything here is a trailing window over live Stripe state. A destination\n * re-enabled, or a fixed handler whose retries then succeed, reads healthy on\n * the next probe. There is no marker to age out and nothing to latch\n * (AGL-1843): the clearing event is \"a window with no failed deliveries and\n * an enabled endpoint\".\n *\n * Pure on purpose, like its siblings: the route fetches, this decides, the\n * spec exercises every branch without a network.\n */\nexport interface BillingWebhookFacts {\n /** Our production destination, as Stripe currently holds it. */\n endpointStatus: 'enabled' | 'disabled' | 'missing'\n /** Events Stripe attempted and failed to deliver inside the window. */\n undelivered: number\n /** Events Stripe emitted inside the window. Reported, never a verdict input. */\n emitted: number\n /**\n * Events our handler claimed inside the window. Reported, never a verdict\n * input — and null when the Firestore arm could not run, which does NOT on\n * its own make the check red: Stripe already answered the question that\n * matters.\n */\n processed: number | null\n /**\n * Deliveries that answered 200 and moved NOTHING inside the window\n * (AGL-1954) — a required event type that produced neither a committed\n * effect nor a named deliberate skip. See `webhook-delivery.ts` for why\n * that is a defect by construction and a legitimate no-op is not one.\n *\n * Null when the Firestore arm could not run, and null is NOT red on its\n * own: Stripe already answered the question that matters, and a health\n * check that reds on its own read failure trains people to ignore it.\n */\n inert: number | null\n /**\n * Deliveries that only landed on a RETRY inside the window (AGL-2039).\n *\n * The arm every other count here is structurally blind to.\n * `undelivered` comes from `delivery_success=false`, which is a TERMINAL\n * filter over EVENTS: an event that 400s three times and then succeeds\n * reads back clean and is indistinguishable from one that succeeded\n * immediately. The Stripe Dashboard scores delivery ATTEMPTS and counted\n * all three — which is why AGL-1906 read 0% against the Dashboard's 30% and\n * both figures were correct. The three attempts it could not see were\n * AGL-1551.\n *\n * The webhook stamps `retriedAtMs` when the distance from `event.created`\n * to its own claim exceeds `RETRY_LAG_SECONDS`, so this is an aggregation\n * over a marker field rather than a scan of every claim in the window.\n *\n * Null when the Firestore arm could not run, and null is NOT red on its\n * own — same rule as `processed` and `inert`.\n */\n retried: number | null\n /**\n * Required event types the destination is NOT subscribed to (AGL-1948 /\n * AGL-1798). Empty is the healthy answer; null means the endpoint did not\n * report its subscriptions and the coverage question is unanswered.\n */\n unsubscribedEvents: readonly string[] | null\n /**\n * The CONNECT destination, as Stripe currently holds it (AGL-1948).\n *\n * A SECOND destination, and the platform one being perfectly healthy says\n * nothing about it: connected-account events are delivered only to a\n * destination created with `connect: true`, so its absence is invisible to\n * every count above. AGL-2122 found exactly that — `account.updated`\n * handled in two plugins, and no destination that could ever deliver it,\n * so every merchant's `stripeChargesEnabled` rotted silently.\n *\n * Null when the endpoint census itself could not be taken.\n */\n connectEndpoint: 'enabled' | 'disabled' | 'missing' | null\n /**\n * Required Connect events the Connect destination is NOT subscribed to.\n * Empty is healthy; null means there was no destination to ask, or it did\n * not state its subscriptions.\n */\n unsubscribedConnectEvents: readonly string[] | null\n}\n\nexport interface BillingWebhookCheck extends HealthCheck {\n endpointStatus: BillingWebhookFacts['endpointStatus'] | null\n undelivered: number | null\n emitted: number | null\n processed: number | null\n inert: number | null\n retried: number | null\n unsubscribedEvents: readonly string[] | null\n connectEndpoint: BillingWebhookFacts['connectEndpoint'] | null\n unsubscribedConnectEvents: readonly string[] | null\n /** The trailing window the counts cover, so the body is self-describing. */\n windowMinutes: number\n}\n\n/**\n * Trailing window the delivery counts cover.\n *\n * Bounded from below by the alert path, not by taste, exactly like\n * `RATE_LIMIT_DEGRADED_WINDOW_MINUTES`: the probe memoizes for 5 minutes, the\n * uptime check runs every 5, and the policy wants ~10 minutes of sustained\n * failure before it emails — so a window under ~20 minutes can go red and\n * green again before anyone is told. Bounded from ABOVE by Stripe's retry\n * schedule: an event that fails and then succeeds on retry stops counting, so\n * a window much longer than an hour would hold transient blips red past their\n * own recovery.\n */\nexport const WEBHOOK_FAILURE_WINDOW_MINUTES = 60\n\n/**\n * Failed deliveries tolerated in the window.\n *\n * Zero, and unlike the signup alarm there is no organic baseline to clear: a\n * healthy account produces exactly zero, and one is already the statement\n * \"Stripe tried to tell us something about money and could not\". Stripe\n * retries on its own, so a failure visible here is not one unlucky packet.\n */\nexport const MAX_FAILED_DELIVERIES_PER_WINDOW = 0\n\n/**\n * Inert deliveries tolerated in the window (AGL-1954).\n *\n * Zero — and unlike `processed`, the count this check reports but refuses to\n * gate on because no defensible floor exists before the beta produces a\n * baseline, this one's floor follows from the definition. A delivery is\n * counted inert only when a required event type produced neither a committed\n * effect nor a NAMED deliberate skip (`classifyWebhookDelivery`). No\n * legitimate traffic pattern generates one: a tenant shopper's subscription,\n * a marketplace refund, a `won` dispute nobody claimed all name their reason\n * and classify as `ignored`. So any at all is the statement \"Stripe told us\n * about money and nothing here moved\" — the same \"any at all\" rule the\n * rate-limiter degradation markers use (AGL-1679).\n */\nexport const MAX_INERT_DELIVERIES_PER_WINDOW = 0\n\n/**\n * Retried deliveries tolerated in the window (AGL-2039).\n *\n * Zero, and the reasoning is `MAX_FAILED_DELIVERIES_PER_WINDOW`'s: a delivery\n * counted here is one whose FIRST attempt did not get through, which is a\n * failed attempt that happened, not a statistical wobble. The healthy band\n * measured against the live account is 1.0–3.7 seconds against a 120-second\n * bar, so nothing organic sits anywhere near it.\n *\n * Cheap to be wrong in the safe direction, because this cannot latch: the\n * window is trailing and the marker is on the event's own claim, so a single\n * blip reds for at most one window and then clears itself — an event, not a\n * condition (the AGL-1843 rule). It is a parameter rather than a literal so\n * that if beta traffic turns out to produce organic retries, the knob is one\n * argument and not a rewrite.\n */\nexport const MAX_RETRIED_DELIVERIES_PER_WINDOW = 0\n\nexport function billingWebhookHealth(\n facts: BillingWebhookFacts | null,\n ms: number,\n windowMinutes: number = WEBHOOK_FAILURE_WINDOW_MINUTES,\n threshold: number = MAX_FAILED_DELIVERIES_PER_WINDOW,\n inertThreshold: number = MAX_INERT_DELIVERIES_PER_WINDOW,\n retriedThreshold: number = MAX_RETRIED_DELIVERIES_PER_WINDOW,\n): BillingWebhookCheck {\n // A null census is degraded, the same rule `signupsHealth` and\n // `rateLimitsHealth` follow: an alarm that cannot see the thing it watches\n // must not report calm. Here it is also the AGL-1906 lesson — `unknown` was\n // read as `healthy` on 2026-08-14 and the endpoint was 400ing every\n // delivery at the time.\n if (facts === null) {\n return {\n ok: false,\n ms,\n code: 'stripe-unavailable',\n endpointStatus: null,\n undelivered: null,\n emitted: null,\n processed: null,\n inert: null,\n retried: null,\n unsubscribedEvents: null,\n connectEndpoint: null,\n unsubscribedConnectEvents: null,\n windowMinutes,\n }\n }\n\n const base = {\n ms,\n endpointStatus: facts.endpointStatus,\n undelivered: facts.undelivered,\n emitted: facts.emitted,\n processed: facts.processed,\n inert: facts.inert,\n retried: facts.retried,\n unsubscribedEvents: facts.unsubscribedEvents,\n connectEndpoint: facts.connectEndpoint,\n unsubscribedConnectEvents: facts.unsubscribedConnectEvents,\n windowMinutes,\n }\n\n // Order matters: a missing or disabled destination explains any delivery\n // count that follows, and is the more actionable statement. An endpoint\n // deleted or switched off in the dashboard is TOTAL silent failure — Stripe\n // stops attempting, so `undelivered` would read zero and a delivery-only\n // rule would call it healthy.\n if (facts.endpointStatus === 'missing') {\n return { ...base, ok: false, code: 'endpoint-missing' }\n }\n if (facts.endpointStatus === 'disabled') {\n return { ...base, ok: false, code: 'endpoint-disabled' }\n }\n // A required event the destination does not carry (AGL-1948 / AGL-1798),\n // ABOVE the delivery counts because it explains a silence they cannot see.\n // An unsubscribed event produces no delivery to fail, no request to reject\n // and no inert handler either — Stripe simply stops sending, and every\n // count below reads a perfectly healthy zero for as long as it lasts. It\n // also outranks `deliveries-failing` on actionability: this one names the\n // exact event to re-add, and `npm run setup:stripe` fixes it in a minute.\n //\n // `null` — the endpoint did not state its subscriptions — is deliberately\n // NOT red. It is a question that went unanswered, not an answer, and the\n // check already has `stripe-unavailable` for a census it could not take.\n if (facts.unsubscribedEvents && facts.unsubscribedEvents.length > 0) {\n return { ...base, ok: false, code: 'events-unsubscribed' }\n }\n if (facts.undelivered > threshold) {\n return { ...base, ok: false, code: 'deliveries-failing' }\n }\n // THE 200-THAT-DID-NOTHING (AGL-1954), and it is LAST on purpose: every\n // code above explains why a handler would have had nothing to do, so\n // reporting inertness over one of them would name the symptom instead of\n // the cause. Reached only when Stripe holds an enabled destination\n // subscribed to everything we asked for and delivered successfully — at\n // which point \"a delivery moved nothing\" has no innocent explanation left.\n if (facts.inert !== null && facts.inert > inertThreshold) {\n return { ...base, ok: false, code: 'handlers-inert' }\n }\n /*==========================================\n * THE ATTEMPTS NOTHING ELSE HERE CAN COUNT (AGL-2039).\n *\n * `undelivered` above is `delivery_success=false`, a TERMINAL-state filter\n * over EVENTS. An event that 400s three times and then succeeds on the\n * fourth reads back `true`, so it is zero there and zero in `emitted`,\n * `processed` and `inert` too — the handler DID eventually run. Every\n * number on this check reports a healthy window while three real delivery\n * attempts failed. That is not a hypothetical: it is the exact reading\n * AGL-1906 produced, 0.00% against the Stripe Dashboard's 30%, and the\n * three attempts the Dashboard was counting were AGL-1551's.\n *\n * BELOW `deliveries-failing` because an event failing every attempt\n * explains an event that needed several, and below `handlers-inert`\n * because a delivery that landed late and then moved nothing is worse than\n * one that landed late and worked. ABOVE the Connect codes because this is\n * this hour's revenue path and those are a slower rot.\n *\n * Null is not red: the marker is ours, so an unread Firestore is an\n * unanswered question, and `stripe-unavailable` already covers a census we\n * could not take.\n *=========================================*/\n if (facts.retried !== null && facts.retried > retriedThreshold) {\n return { ...base, ok: false, code: 'deliveries-retried' }\n }\n /*==========================================\n * THE CONNECT DESTINATION (AGL-1948, closing AGL-2122's blind spot).\n *\n * A SECOND destination, and every code above is about the first one. The\n * platform destination can be enabled, fully subscribed, delivering and\n * acting — the whole check green — while connected-account events reach\n * nothing at all, because they are delivered only to a destination created\n * with `connect: true`. AGL-2122 measured exactly that against the live\n * account on 2026-08-18: one destination, correct URL, all ten events, and\n * no Connect destination in existence. `account.updated` was handled in two\n * plugins and could never be delivered, so `syncConnectAccountStatus` never\n * ran and every merchant's charge-eligibility flag rotted where it stood.\n *\n * LAST on purpose, and for a different reason than `handlers-inert` above:\n * not because the codes above explain it, but because they do not. It is an\n * independent destination whose failure is slower — merchant readiness\n * drifting out of date — while everything above is this hour's revenue. The\n * facts ride in the body either way, so the staff board shows the Connect\n * state even when a louder code takes the headline; only the single `code`\n * is exclusive.\n *\n * `missing` IS red. Before AGL-2122 that would have been wrong, because\n * nothing created the destination and a permanently-red check teaches\n * people to ignore the board. Now that setup-stripe creates it, its absence\n * is a real regression with a one-line remedy.\n *=========================================*/\n if (facts.connectEndpoint === 'missing') {\n return { ...base, ok: false, code: 'connect-endpoint-missing' }\n }\n if (facts.connectEndpoint === 'disabled') {\n return { ...base, ok: false, code: 'connect-endpoint-disabled' }\n }\n if (\n facts.unsubscribedConnectEvents &&\n facts.unsubscribedConnectEvents.length > 0\n ) {\n return { ...base, ok: false, code: 'connect-events-unsubscribed' }\n }\n return { ...base, ok: true }\n}\n\n/**\n * Is every interval we can SELL also an interval we can BILL? (AGL-1931)\n *\n * The metered item is what turns reported usage into money. It is attached\n * from an env-configured price id that is keyed by billing interval —\n * `STRIPE_PRICE_METERED` for monthly, `STRIPE_PRICE_METERED_YEARLY` for\n * annual — because Stripe forbids one subscription mixing\n * `recurring.interval`.\n *\n * When one of those ids is missing, `meteredPriceId(interval)` returns null\n * and every subscription-mutating path does the only safe thing: it attaches\n * nothing and carries on. Failing a checkout over it would be worse. But the\n * result is a subscription that accrues usage against a meter and carries no\n * item to bill it, and NOTHING about that is visible — the plan is right, the\n * entitlements are right, the invoice looks right, and the only trace is\n * revenue that never arrives.\n *\n * That is the defect shape this check exists to end. The absence of a price\n * was indistinguishable from \"correctly no overage\", so it hid behind a\n * `console.warn` on a server log nobody reads. Here it is a 503 on the same\n * probe the uptime check and alert email already watch.\n *\n * ## Why ASYMMETRY is the loudest code\n *\n * One interval configured and the other not means one cohort of customers is\n * billed for overage and the other silently is not — off the same meter, on\n * the same plans, with no screen anywhere showing the difference. It cannot\n * be a deliberate state: there is no product reason to meter monthly\n * subscribers and exempt annual ones.\n *\n * ## Why BOTH missing is still red, but only with Stripe configured\n *\n * Both unset with no `STRIPE_SECRET_KEY` is simply an unprovisioned\n * deployment — local dev, a fresh preview — where no money is at stake and\n * going red would train everyone to ignore this check. That is the exact\n * reasoning the checkout and subscription routes already apply to their\n * warning, and it is preserved here.\n *\n * But both unset while Stripe IS configured is a deployment that can take\n * money and cannot bill a single unit of overage on ANY interval. That is\n * the same revenue loss, merely total instead of partial, and the earlier\n * code treated it as calm because it only ever compared the two ids to each\n * other. Comparing them to Stripe's presence instead is what closes it.\n */\nexport interface MeteredPricingFacts {\n /** Whether this deployment holds a Stripe secret key at all. */\n stripeConfigured: boolean\n /** `STRIPE_PRICE_METERED` — the monthly metered price — is set. */\n monthly: boolean\n /** `STRIPE_PRICE_METERED_YEARLY` — the annual metered price — is set. */\n yearly: boolean\n}\n\nexport interface MeteredPricingCheck extends HealthCheck {\n stripeConfigured: boolean\n monthly: boolean\n yearly: boolean\n /**\n * The interval that would accrue unbilled usage, named so the body says\n * which cohort is affected without anyone re-deriving it from two booleans.\n */\n unbilledInterval: 'month' | 'year' | 'both' | null\n}\n\nexport function meteredPricingHealth(\n facts: MeteredPricingFacts,\n ms: number,\n): MeteredPricingCheck {\n const base = {\n ms,\n stripeConfigured: facts.stripeConfigured,\n monthly: facts.monthly,\n yearly: facts.yearly,\n }\n\n // Unprovisioned: no Stripe, no money, no alarm.\n if (!facts.stripeConfigured) {\n return { ...base, ok: true, code: 'stripe-unconfigured', unbilledInterval: null }\n }\n if (facts.monthly && facts.yearly) {\n return { ...base, ok: true, unbilledInterval: null }\n }\n if (!facts.monthly && !facts.yearly) {\n return { ...base, ok: false, code: 'metered-price-missing', unbilledInterval: 'both' }\n }\n // Exactly one. The AGL-1931 shape.\n return {\n ...base,\n ok: false,\n code: 'metered-price-asymmetric',\n unbilledInterval: facts.yearly ? 'month' : 'year',\n }\n}\n\n/*==========================================\n * SCHEDULED JOBS, WATCHED BY ABSENCE (AGL-1955).\n *\n * ## The failure this exists for\n *\n * `Cloud Scheduler job run failed (aglyn-main)` is a log-match on\n * `resource.type=\"cloud_scheduler_job\" AND severity >= ERROR`, and the\n * `scheduled-crons.yml` workflow goes red on a non-200. Both of those are\n * triggered BY A RUN. A job that is deleted, paused, or whose `- cron:` line\n * is edited away produces no attempt, therefore no error entry, therefore no\n * red workflow, therefore nothing. Quiet reads as healthy — the same shape\n * AGL-1923 closed for the error beacon, one subsystem over.\n *\n * What is downstream of these jobs is not cosmetic: `report-usage` is the\n * only writer that meters a closed month into Stripe, `run-erasures` is the\n * GDPR deletion runner, `audit-archive` moves and then DELETES audit rows,\n * and `plugin-jobs-beat` is the only thing that makes scheduled publishing\n * and booking-hold expiry happen at all. A silently unscheduled job means\n * customers are not billed, or data is not reaped, behind a green board.\n *\n * ## The two properties that make this work\n *\n * **It does not need the job to be alive to fire.** That is the whole bug.\n * The verdict is computed by the READER — `/api/health/crons`, wound by the\n * uptime probe and by any staff member opening the Health page — against\n * marks the jobs left behind. Nothing here is on a schedule that could\n * itself stop; that would only move the problem one layer out, which is the\n * AGL-1923 argument for a graded switch over a `conditionAbsent` policy.\n *\n * **It cannot false-alarm on a legitimately idle period.** The mark is\n * stamped by the INVOCATION, not by the work: a `finish-domain-attachments`\n * run with no pending host still beats, and a quiet week for\n * `reap-plugin-artifacts` still beats. And the expected time is computed\n * from the job's own cron expression rather than from a fixed interval, so\n * `usage-email` — hourly, but only on the 1st and 2nd of the month — is\n * green for the twenty-nine days it is deliberately not running. A fixed\n * \"expect one every hour\" rule would have paged on the 3rd, every month.\n *\n * ## Why an output age rule was not enough on its own\n *\n * AGL-1843's `exportsHealth` watches the weekly export by the age of what it\n * PRODUCES, and that is the better rule where an output exists — it catches\n * \"ran and failed\" as well as \"never ran\". Most of these jobs have no such\n * output. `backfill-scope` is detect-only and deliberately writes nothing.\n * `usage-alerts` produces a notification only when somebody is over budget.\n * `reap-plugin-artifacts` deletes nothing in a normal week. For those, the\n * only honest thing whose age moves on every run is a mark the run itself\n * leaves — so that is what this reads. `exportsHealth` stays where it is:\n * the two are complementary, and `firestore-export` is watched by both.\n *\n * ## It must be able to go GREEN\n *\n * Nothing latches. A job that missed a fire and then runs again stamps a\n * fresh mark and reads healthy on the next probe — the AGL-1843 rule, whose\n * clearing event here is named and is \"the next invocation of that job\".\n *=========================================*/\n\n/** Where the marks live. One document per job id, written by the job. */\nexport const CRON_BEAT_COLLECTION = 'platformCronBeats'\n\n/**\n * The document that records when this deployment STARTED watching.\n *\n * Without it a job that has never reported cannot be told apart from a job\n * that has stopped reporting, and every board would come up red on the day\n * the feature deploys — for `usage-email`, red until the 1st of the next\n * month. The rule this enables is stated once, in `cronJobsHealth`: a job\n * with no mark is only silent once a scheduled fire time has passed SINCE\n * we started watching.\n *\n * Not a reserved id: Firestore reserves `__.*__`, and this is a plain slug\n * that no job id can collide with (job ids never contain the word `watch`,\n * and the wiring spec asserts the inventory's ids against the workflow).\n */\nexport const CRON_BEAT_WATCH_DOC = 'watch-window'\n\n/**\n * Where a degraded verdict is recorded, so a red window survives it.\n *\n * The response body names the late job, but only to whoever is holding it,\n * and the uptime probe reads the status and discards the body. Vercel's\n * runtime log keeps a `console.error` for about an hour, and the Cloud\n * Logging drain deliberately does not forward one — `isServerErrorEntry`\n * drops `level: 'error'`, because that is every fail-soft line the beacons\n * write and forwarding it is what a self-feeding bill is made of. So the\n * only sink that outlives the window is the one the endpoint already holds\n * a handle to.\n *\n * Written by the READER, like the verdict itself. One document, replaced on\n * each degraded probe, holding the failing rows and when the window opened.\n * Same collision argument as the watch document: a plain slug, and no job id\n * contains `degraded`.\n */\nexport const CRON_BEAT_DEGRADED_DOC = 'last-degraded'\n\n/**\n * Every job's latest mark in one document, `{ beats: { [jobId]: atMs } }`.\n *\n * The reader's fast path (AGL-2946). Listing the collection costs one read per\n * job on every uncached probe — ~23 reads, several hundred times a day, on a\n * project that pays for reads past the free tier — to answer a question this\n * one document answers.\n *\n * A COPY, never the authority. `writeCronBeat` writes it second, so a job\n * whose own mark landed can still be missing or stale here, and the reader\n * confirms any job this document calls late against that job's own mark\n * before reporting it. Stale costs a read; it never costs a false red.\n *\n * Same collision argument as the other two: a plain slug no job id equals.\n */\nexport const CRON_BEAT_SUMMARY_DOC = 'summary'\n\n/** The reserved documents in `CRON_BEAT_COLLECTION` that are not job marks. */\nexport const CRON_BEAT_RESERVED_DOCS: ReadonlySet<string> = new Set([\n CRON_BEAT_WATCH_DOC,\n CRON_BEAT_DEGRADED_DOC,\n CRON_BEAT_SUMMARY_DOC,\n])\n\n/**\n * How long a gap may be before the next degraded probe counts as a NEW\n * window rather than a continuation of the one on file.\n *\n * Three probe TTLs. Long enough that an instance answering intermittently\n * does not split one outage into a dozen, short enough that yesterday's\n * window is never mistaken for today's. The comparison is against the\n * failing set as well, so a different set always opens a new window.\n */\nexport const CRON_DEGRADED_WINDOW_CONTINUITY_MS = 15 * 60_000\n\n/** Who fires the job. The two are operated, and fail, differently. */\nexport type CronRunner = 'github-actions' | 'cloud-scheduler'\n\nexport interface ScheduledJob {\n /** Stable id. The Firestore document id, and the health check's name. */\n id: string\n label: string\n /**\n * Five-field cron, UTC. The SAME string the scheduler holds — the wiring\n * spec asserts each one against its runner's source of truth\n * (`.github/workflows/scheduled-crons.yml` for `github-actions`,\n * `cloud/functions/src/index.ts` for `cloud-scheduler`), so an edit there\n * that is not made here fails the build rather than quietly changing what\n * \"on time\" means.\n */\n cron: string\n runner: CronRunner\n /** What the scheduler actually invokes, for the operator reading a red row. */\n target: string\n /**\n * How late a run may be before the row goes red.\n *\n * Generous on purpose, and the generosity is not padding. GitHub only runs\n * scheduled workflows from the default branch and delays them under load —\n * routinely by minutes, occasionally by an hour. The question this check\n * answers is \"is this job still scheduled at all\", not \"did it start on the\n * second\"; a grace that reds on ordinary lateness is alert fatigue, and an\n * alert people learn to ignore is the failure this issue is about wearing\n * a different hat.\n *\n * SO THE GRACE IS A PROPERTY OF THE RUNNER, not of the job (AGL-1617). The\n * ninety minutes the two frequent sweeps used to carry was GitHub's drift\n * budget, and it was bought with detection: on a fifteen-minute schedule it\n * takes SIX consecutive missed fires before anything is said. Moving those\n * two onto Cloud Scheduler — which is punctual, as `plugin-jobs-beat` has\n * been demonstrating at `every 1 minutes` in the same payload the whole\n * time — is what let the bar go back UP, to forty-five minutes and three\n * missed fires. That direction is the point: widening a grace to make a\n * monitor agree with a runner that drops 60% of its triggers is not a fix,\n * it is deciding not to look.\n *\n * IT IS A FLOOR, NOT THE EXACT BAR. `cronJobsHealth` compares the mark\n * against the last fire that is ALREADY this old, so where the threshold\n * lands depends on the clock's phase against the schedule: forty-five\n * minutes on a fifteen-minute cron reds somewhere between 45 and 60 minutes\n * of silence, six hours on a daily one between 6h and 30h. Quote the range,\n * not the number, when reasoning about how fast a dead job is found — and\n * never write a test that only holds at one phase.\n */\n graceMinutes: number\n /** What stops happening when this job stops. Rendered on the board. */\n drives: string\n}\n\n/**\n * THE INVENTORY.\n *\n * Five GitHub Actions schedules (`.github/workflows/scheduled-crons.yml`) —\n * the weekly jobs, for which hours of drift cost nothing — and the rows\n * driven by Cloud Scheduler out of\n * `cloud/functions/src/index.ts`: `pluginJobsBeat` and `consoleAiJobsBeat`\n * (every minute), the routes the `consoleFastCrons` job carries every\n * fifteen (AGL-1617), and one `consoleDailyCron` export per daily job.\n *\n * `scheduled-crons-wiring.spec.ts` holds BOTH runners against their source —\n * the workflow for the `github-actions` rows, the functions file for the\n * `cloud-scheduler` ones — in both directions. An inventory row for a job\n * nobody schedules and a scheduled job nobody watches are the same bug seen\n * from opposite ends, and both fail the build.\n *\n * `report-usage` appears TWICE and is two jobs, not one. The 02:00 run\n * rolls up the closed month and is the only run that ever reaches Stripe;\n * the 07:00 `?month=current` run writes the in-progress figure every usage\n * budget reads (AGL-2219). Either can stop without the other, and folding\n * them into one row would hide exactly that.\n */\n/*\n * ⚠️ GRACE IS A PROPERTY OF THE RUNNER, not of the job.\n *\n * The five daily console jobs carried a SIX-HOUR grace because GitHub\n * Actions dispatches scheduled workflows on a best-effort basis and routinely\n * drifted by an hour or two. That grace bought tolerance and paid for it in\n * blindness: on 2026-08-27 GitHub dropped the whole day, and three of these\n * were thirty hours silent before anything said so.\n *\n * They run on Cloud Scheduler now, which has been punctual to the minute\n * since AGL-1617, and each function's own ceiling is 540 seconds. Ninety\n * minutes is therefore ten times the longest a healthy run can take and still\n * catches a dead job inside the working day, instead of most of a day later.\n */\nexport const SCHEDULED_JOBS: readonly ScheduledJob[] = [\n {\n id: 'report-usage',\n label: 'Metered usage rollup (closed month)',\n cron: '0 2 * * *',\n runner: 'cloud-scheduler',\n target: '/api/billing/report-usage',\n graceMinutes: 90,\n drives:\n 'The only run that meters a closed month into Stripe. If it stops, customers are not billed for what they used.',\n },\n {\n id: 'audit-archive',\n label: 'Audit archive',\n cron: '0 3 * * *',\n runner: 'cloud-scheduler',\n target: '/api/admin/audit-archive',\n graceMinutes: 90,\n drives:\n 'Moves adminAudit rows past the 90-day window into Storage and deletes them from Firestore. If it stops, the retention promise is not being kept.',\n },\n {\n id: 'run-erasures',\n label: 'GDPR erasure runner',\n cron: '0 4 * * *',\n runner: 'cloud-scheduler',\n target: '/api/admin/run-erasures',\n graceMinutes: 90,\n drives:\n 'Executes accepted erasure requests. If it stops, personal data a customer asked us to delete is still here, and the clock on that request is still running.',\n },\n {\n id: 'report-usage-current',\n label: 'Metered usage rollup (current month)',\n cron: '0 7 * * *',\n runner: 'cloud-scheduler',\n target: '/api/billing/report-usage?month=current',\n graceMinutes: 90,\n drives:\n 'Writes the in-progress month figure (AGL-2219). If it stops, every usage budget on the platform is structurally unable to fire and the Billing card stops totalling.',\n },\n {\n id: 'usage-alerts',\n label: 'Usage budget alerts',\n cron: '0 8 * * *',\n runner: 'cloud-scheduler',\n target: '/api/billing/usage-alerts',\n graceMinutes: 90,\n drives:\n 'Evaluates usage budgets and notifies. If it stops, an org sails past its budget with no warning and finds out on the invoice.',\n },\n {\n id: 'reap-sending-domains',\n label: 'Orphaned sending-domain reaper',\n // An hour after `run-erasures`, which is what produces most of its work:\n // an erasure records what it could not release rather than waiting on a\n // vendor, and this is the only thing that settles that debt.\n cron: '0 5 * * *',\n runner: 'cloud-scheduler',\n target: '/api/admin/reap-sending-domains',\n graceMinutes: 90,\n drives:\n 'Releases the provider domain object and the zone records of every sending domain whose site or workspace is gone. If it stops, each deleted site keeps one of a bounded number of provider domain slots forever — until the ceiling is reached and a site that asks for a domain of its own is refused one, which leaves it on the shared pool rather than stopping its mail — and leaves a live DKIM key in our zone under a label a future site can claim and inherit a stranger’s signature from.',\n },\n {\n id: 'reap-unverified-orgs',\n label: 'Unverified signup reaper',\n // 06:00, after `run-erasures` and the sending-domain sweep and before the\n // billing pair. It calls `eraseOrg` itself, so it wants the erasure runner\n // to have already finished with whatever it was holding.\n cron: '0 6 * * *',\n runner: 'cloud-scheduler',\n target: '/api/admin/reap-unverified-orgs?dryRun=0',\n graceMinutes: 90,\n drives:\n 'Erases workspaces whose sole owner never confirmed an email address, releasing the workspace address they took, and promotes the held address of every owner who has since verified. If it stops, a name claimed with a throwaway inbox is held until its reservation expires — and, worse, a real customer who verified keeps a pending address that becomes claimable by anyone on day twenty-one.',\n },\n {\n id: 'ai-insights-digest',\n label: 'Weekly insights',\n // 06:00 and 14:00 every day (AGL-2915): Monday's first run makes the\n // week's digests and every run after it delivers the ones the AI jobs beat\n // has written. Served by the AI plugin.\n cron: '0 6,14 * * *',\n runner: 'cloud-scheduler',\n target: '/api/admin/ai-insights-digest',\n graceMinutes: 90,\n drives:\n 'Each Monday, makes the weekly insights people asked for, and delivers them once they are written. If it stops, nobody who asked is sent their insights, and a digest parked for credits is never given up.',\n },\n {\n id: 'usage-email',\n label: 'Monthly usage summaries',\n // Hourly across the FIRST TWO DAYS only (AGL-2409). The idle-period case\n // this whole check had to get right: for twenty-nine days a month this\n // job is correctly doing nothing, and must read green while it does. On\n // Cloud Scheduler since GitHub fired the hourly entry twice in the first\n // ten hours of 2026-10-01 (AGL-3439); ninety minutes is the dailies'\n // grace, one missed fire plus half an hour.\n cron: '0 * 1-2 * *',\n runner: 'cloud-scheduler',\n target: '/api/billing/usage-email',\n graceMinutes: 90,\n drives:\n \"Mails each org last month's usage summary, chunked across 48 hourly windows. If it stops, the month's summaries are simply never sent.\",\n },\n {\n id: 'crm-task-reminders',\n label: 'CRM task reminders',\n // Hourly, all month (AGL-2659), on Cloud Scheduler since GitHub delivered\n // the hourly entry about once every six hours and left reminders that\n // late. Ninety minutes is the dailies' grace: one missed fire plus half\n // an hour, and ten times the 540s a run may take.\n cron: '0 * * * *',\n runner: 'cloud-scheduler',\n target: '/api/crm/task-reminders',\n graceMinutes: 90,\n drives:\n 'Sends each CRM task’s reminder at its own time — a console notification and an email to the assignee (AGL-2659). If it stops, every reminder a person set in the task drawer is silently late, and nothing but the morning digest says a task was due.',\n },\n {\n id: 'risk-notice-digests',\n label: 'Risk notice digests',\n // Hourly at quarter past (AGL-3368); the dailies' ninety-minute grace.\n cron: '15 * * * *',\n runner: 'cloud-scheduler',\n target: '/api/risk-notices/digests',\n graceMinutes: 90,\n drives:\n 'Sends the one summary a workspace gets when more items were held or flagged in an hour than are sent one by one (AGL-3368). If it stops, a workspace whose attack ended hears nothing about the items folded into its digest until its next notice.',\n },\n {\n id: 'firestore-export',\n label: 'Weekly Firestore export',\n cron: '0 5 * * 1',\n runner: 'github-actions',\n target: '/api/admin/firestore-export',\n graceMinutes: 1440,\n drives:\n 'Starts the independent GCS export — the restore point that exists because the managed backups kept flipping to NOT_AVAILABLE (AGL-1843). Also watched by output age on /api/health/backups.',\n },\n {\n id: 'reap-plugin-artifacts',\n label: 'Plugin artifact reaper',\n cron: '30 5 * * 1',\n runner: 'github-actions',\n target: '/api/admin/reap-plugin-artifacts',\n graceMinutes: 1440,\n drives:\n 'Deletes orphaned plugin bundles from the artifacts bucket. If it stops, storage grows without bound and nothing says so.',\n },\n {\n id: 'reverify-plugin-versions',\n label: 'Plugin verdict re-verification',\n cron: '0 6 * * 1',\n runner: 'github-actions',\n target: '/api/admin/reverify-plugin-versions',\n graceMinutes: 1440,\n drives:\n 'Re-checks stored plugin verdicts against the current verifier (AGL-1086). If it stops, a regression on a live version is never noticed.',\n },\n {\n id: 'reverify-sso-domains',\n label: 'SSO domain re-verification',\n // Mondays 07:30 UTC, after the rest of the weekly block. Weekly is the\n // right cadence because the threat needs a domain to lapse, clear a\n // registrar grace period and be re-bought — months, not days — and a\n // daily sweep would multiply DNS traffic by seven to learn nothing sooner.\n cron: '30 7 * * 1',\n runner: 'github-actions',\n target: '/api/admin/reverify-sso-domains',\n graceMinutes: 1440,\n drives:\n 'Re-checks that every live SSO domain still publishes its DNS ownership token (AGL-1210). If it stops, a domain that changed hands keeps routing another company sign-ins and nothing notices. It reports only — it never revokes, because a revoke logs out an entire enterprise.',\n },\n {\n id: 'backfill-scope',\n label: 'Scope-drift detection',\n cron: '30 6 * * 1',\n runner: 'github-actions',\n target: '/api/admin/backfill-scope',\n graceMinutes: 1440,\n drives:\n 'Reports documents with no visibleTo — invisible to every site-scoped read (AGL-1478). Detect-only by construction, so it has no output whose age could stand in for this mark.',\n },\n {\n id: 'campaigns-process-scheduled',\n label: 'Scheduled email campaigns',\n cron: '*/15 * * * *',\n // MOVED OFF GITHUB ACTIONS (AGL-1617), with the row below it. See the\n // grace note on `graceMinutes` for why that let the bar go UP.\n runner: 'cloud-scheduler',\n target: 'consoleFastCrons → console /api/campaigns/process-scheduled',\n graceMinutes: 45,\n drives:\n 'Claims and sends due campaigns. If it stops, a campaign the composer showed as Scheduled sits there and never sends — the AGL-2134 shape, which is sold on /product/marketing.',\n },\n {\n id: 'lists-materialize',\n label: 'Dynamic email lists',\n // Shares the `consoleFastCrons` job with the two rows around it — one\n // Cloud Scheduler job driving three routes, which is why adding this cost\n // no scheduler quota.\n cron: '*/15 * * * *',\n runner: 'cloud-scheduler',\n target: 'consoleFastCrons \\u2192 console /api/lists/materialize',\n graceMinutes: 45,\n drives:\n 'Re-evaluates every dynamic list against its rule. If it stops, a list keeps whatever membership it last had, and a campaign sent to it reaches an audience that silently stopped tracking the rule the merchant wrote — which looks exactly like a rule that matched nobody new.',\n },\n {\n id: 'finish-domain-attachments',\n label: 'Custom domain re-check',\n // Every fifteen minutes rather than twenty since AGL-1617: it shares one\n // Cloud Scheduler job with the campaign processor, and AGL-2010's argument\n // was always that a customer is sitting there waiting for their site.\n cron: '*/15 * * * *',\n runner: 'cloud-scheduler',\n target: 'consoleFastCrons → console /api/admin/finish-domain-attachments',\n graceMinutes: 45,\n drives:\n 'Re-checks pending custom domains after the certificate or DNS settles (AGL-2010). If it stops, a correctly-configured domain stays dark until a human presses Re-attach.',\n },\n {\n id: 'provision-sending-domains',\n label: 'Sending domain provisioning',\n // The fourth route on the `consoleFastCrons` job, sharing its schedule\n // with the three rows above. It is a console route rather than a job on\n // the platform beat because issuing a DKIM key needs a full-access\n // provider credential the tenant runtime must never hold.\n cron: '*/15 * * * *',\n runner: 'cloud-scheduler',\n target: 'consoleFastCrons → console /api/admin/provision-sending-domains',\n graceMinutes: 45,\n drives:\n 'Turns a site’s sending-domain claim into a DKIM key and the DNS records that carry it. If it stops, every site that has ASKED for a domain of its own waits on a claim with no records to publish. Their mail keeps leaving on the shared pool throughout, which makes this a stall in reputation isolation rather than stopped mail — and it is invisible to the merchant, who sees only a domain that never finishes.',\n },\n {\n id: 'drain-publish-outbox',\n label: 'Publish outbox drain',\n // The fifth route on the `consoleFastCrons` job. Fifteen minutes is the\n // bound on how long a publish whose tab went away can stay invisible,\n // and it is the schedule this shares rather than one chosen for it —\n // buying a tighter one would mean a second Cloud Scheduler job against a\n // free-tier allowance of three.\n cron: '*/15 * * * *',\n runner: 'cloud-scheduler',\n target: 'consoleFastCrons \\u2192 console /api/admin/drain-publish-outbox',\n graceMinutes: 45,\n drives:\n 'Fires the cache-drop announce for publishes whose tab closed before it landed (AGL-2575). Publishing is a client Firestore write, so the announce is a fetch from the browser and a closed tab strands it; this is the only thing that finishes one. If it stops, a stranded publish is invisible on the live site for the full hour-long document TTL, and the pending entry that records it is never read by anything.',\n },\n {\n id: 'plugin-console-crons',\n label: 'Plugin console jobs',\n // The sixth route on the `consoleFastCrons` job (AGL-2981). One route for\n // every job a plugin declares on `plugin-console-crons`, so a plugin's\n // job needs no route of its own in this list; each declared job has its\n // own row beside this one, added by `/api/health/crons` from the\n // declarations. This row answers the question under all of them: is the\n // route being posted at all.\n cron: '*/15 * * * *',\n runner: 'cloud-scheduler',\n target: 'consoleFastCrons → console /api/admin/plugin-crons',\n graceMinutes: 45,\n drives:\n 'Runs every console job a plugin declares, each with its own row on this board. If it stops, every one of those jobs stops with it — a plugin’s scheduled sends and syncs among them — and each of their rows goes silent too.',\n },\n {\n id: 'consent-group-changes',\n label: 'Consent group changes',\n // The seventh route on the `consoleFastCrons` job (AGL-3320). The editor's\n // progress panel works a change while it is open; this finishes every one\n // whose panel was closed, so fifteen minutes is how long an abandoned\n // change can sit between steps.\n cron: '*/15 * * * *',\n runner: 'cloud-scheduler',\n target: 'consoleFastCrons \\u2192 console /api/admin/consent-group-changes',\n graceMinutes: 45,\n drives:\n 'Finishes every consent group change an admin started and then left: the refusals carried between separating sites, the new declaration, the re-homed records and the delayed sweep (AGL-3320). If it stops, a change whose editor was closed stays where it stopped — before the flip the old groups stay in force, after it the records keyed by the old groups wait to move — and no further change can start until it finishes.',\n },\n {\n id: 'operator-alerts',\n label: 'Operator alerts',\n // The eighth route on the `consoleFastCrons` job (AGL-3377). It asks\n // every health endpoint so a degraded one alerts the operator without an\n // external monitor, and it carries the support SLA sweep and the daily\n // operator digest.\n cron: '*/15 * * * *',\n runner: 'cloud-scheduler',\n target: 'consoleFastCrons \\u2192 console /api/admin/operator-alerts/tick',\n graceMinutes: 45,\n drives:\n 'Asks every health check on the install so a degraded one emails the operator even with no uptime monitor watching; flags support tickets past their response time; sends the daily operator digest (AGL-3377). If it stops, a check that goes red is only noticed by whoever reads it, overdue tickets say nothing, and digest alerts pile up unsent — while every immediate alert still goes out.',\n },\n {\n id: 'ai-jobs-beat',\n label: 'AI jobs beat',\n // Its own Cloud Scheduler job, every minute (AGL-3026): the console route\n // runs steps for most of five minutes at a time, and a page waits a beat\n // between sections. The grace is the other every-minute job's, below.\n cron: '* * * * *',\n runner: 'cloud-scheduler',\n target: 'consoleAiJobsBeat → console /api/admin/ai-jobs-beat',\n graceMinutes: 30,\n drives:\n 'Runs the AI generation steps the doors leave queued: every plan, every page pass, the passes of a site scaffold, and any step an inline door’s budget handed back (AGL-3026). It runs on the console because only the console holds the AI provider key. If it stops, a generation job that plans never gets its plan and a page is never built — the job sits queued, spending nothing, and never finishes.',\n },\n {\n id: 'plugin-jobs-beat',\n label: 'Plugin job beat',\n // Cloud Scheduler says `every 1 minutes`; the equivalent five-field\n // expression is what this check evaluates against.\n cron: '* * * * *',\n runner: 'cloud-scheduler',\n target: 'firebase-schedule-pluginJobsBeat-us-central1 → tenant /api/plugins/run-jobs',\n graceMinutes: 30,\n drives:\n 'The only thing that runs scheduled publishing and booking-hold expiry (AGL-1159). It is also the one job that can fail by pointing at the WRONG deployment (AGL-2176) — the mark is stamped by the tenant runner that answers, so a beat aimed elsewhere reads as silence here.',\n },\n] as const\n\n/** One job's most recent mark, as read from `CRON_BEAT_COLLECTION`. */\nexport interface CronBeat {\n jobId: string\n /** Epoch ms of the invocation that left the mark. */\n atMs: number\n}\n\nexport interface CronJobCheck extends HealthCheck {\n /** The schedule this row was judged against, so the body is self-describing. */\n schedule: string\n runner: CronRunner\n /** Minutes since the job last reported. Null when it never has. */\n lastBeatAgeMinutes: number | null\n /**\n * The most recent fire time the job should already have reported for, ISO.\n * Null when no such time has passed since we started watching — which is\n * the healthy reading for a job that is legitimately idle.\n */\n dueAt: string | null\n graceMinutes: number\n}\n\n/**\n * A cron field, expanded to the set of values it matches.\n *\n * Supports a star, `a`, `a-b`, `a,b,c` and a trailing `/n` step on any of\n * them — every form the inventory and `scheduled-crons.yml` use, and nothing\n * else. An unparsable field returns null, which the caller treats as \"matches\n * nothing\". A schedule that can never fire then produces a null `dueAt` and\n * a green row rather than a permanent red one, and the wiring spec is what\n * catches the typo — a monitor that reds on its own parse bug teaches people\n * to ignore it.\n */\nfunction expandCronField(\n field: string,\n min: number,\n max: number,\n): Set<number> | null {\n const values = new Set<number>()\n for (const part of field.split(',')) {\n const [range, stepRaw] = part.split('/')\n const step = stepRaw === undefined ? 1 : Number.parseInt(stepRaw, 10)\n if (!Number.isFinite(step) || step <= 0) return null\n let from: number\n let to: number\n if (range === '*') {\n from = min\n to = max\n } else if (range.includes('-')) {\n const [a, b] = range.split('-')\n from = Number.parseInt(a, 10)\n to = Number.parseInt(b, 10)\n } else {\n from = Number.parseInt(range, 10)\n to = stepRaw === undefined ? from : max\n }\n if (!Number.isFinite(from) || !Number.isFinite(to)) return null\n if (from < min || to > max || from > to) return null\n for (let value = from; value <= to; value += step) values.add(value)\n }\n return values.size ? values : null\n}\n\ninterface ParsedCron {\n minute: Set<number> | null\n hour: Set<number> | null\n dayOfMonth: Set<number> | null\n month: Set<number> | null\n dayOfWeek: Set<number> | null\n /** True when either day field is restricted — the standard OR applies. */\n domRestricted: boolean\n dowRestricted: boolean\n}\n\nfunction parseCron(expression: string): ParsedCron | null {\n const fields = expression.trim().split(/\\s+/)\n if (fields.length !== 5) return null\n const [minute, hour, dayOfMonth, month, dayOfWeek] = fields\n const parsed: ParsedCron = {\n minute: expandCronField(minute, 0, 59),\n hour: expandCronField(hour, 0, 23),\n dayOfMonth: expandCronField(dayOfMonth, 1, 31),\n month: expandCronField(month, 1, 12),\n dayOfWeek: expandCronField(dayOfWeek, 0, 6),\n domRestricted: dayOfMonth !== '*',\n dowRestricted: dayOfWeek !== '*',\n }\n if (\n !parsed.minute ||\n !parsed.hour ||\n !parsed.dayOfMonth ||\n !parsed.month ||\n !parsed.dayOfWeek\n ) {\n return null\n }\n return parsed\n}\n\n/**\n * Does this UTC date fall on a day the expression can fire?\n *\n * The day-of-month / day-of-week OR is the standard cron rule: when BOTH are\n * restricted the job fires on either, not on both. No expression in the\n * inventory restricts both, and the rule is implemented anyway so that one\n * added later is not silently misjudged.\n */\nfunction cronDayMatches(parsed: ParsedCron, date: Date): boolean {\n if (!parsed.month.has(date.getUTCMonth() + 1)) return false\n const domHit = parsed.dayOfMonth.has(date.getUTCDate())\n const dowHit = parsed.dayOfWeek.has(date.getUTCDay())\n if (parsed.domRestricted && parsed.dowRestricted) return domHit || dowHit\n if (parsed.domRestricted) return domHit\n if (parsed.dowRestricted) return dowHit\n return true\n}\n\n/**\n * The latest time at or before `atMs` that `expression` fires, UTC.\n *\n * Null when the expression is unparsable, or when it does not fire inside\n * the lookback — 45 days, which comfortably clears the longest gap any\n * inventory entry has (`usage-email`'s twenty-nine idle days).\n *\n * Whole non-matching DAYS are skipped rather than walked minute by minute,\n * so the worst case is ~45 day checks plus 1,440 minute checks rather than\n * 64,800 of them.\n */\nexport function previousCronFire(\n expression: string,\n atMs: number,\n lookbackDays = 45,\n): number | null {\n const parsed = parseCron(expression)\n if (!parsed) return null\n // Floor to the minute: a fire at 05:00:00 counts as due at 05:00:30.\n let cursor = Math.floor(atMs / 60_000) * 60_000\n const floor = cursor - lookbackDays * 86_400_000\n while (cursor >= floor) {\n const date = new Date(cursor)\n if (!cronDayMatches(parsed, date)) {\n // Jump to 23:59 of the previous UTC day.\n const startOfDay = Date.UTC(\n date.getUTCFullYear(),\n date.getUTCMonth(),\n date.getUTCDate(),\n )\n cursor = startOfDay - 60_000\n continue\n }\n if (\n parsed.hour.has(date.getUTCHours()) &&\n parsed.minute.has(date.getUTCMinutes())\n ) {\n return cursor\n }\n cursor -= 60_000\n }\n return null\n}\n\n/**\n * The scheduled jobs, reduced to one health check per job (AGL-1955).\n *\n * `beats` null means the marks could not be read at all — degraded for every\n * row, the same rule `signupsHealth`, `rateLimitsHealth` and `beaconHealth`\n * follow. An alarm that cannot see the thing it watches must say so rather\n * than report calm; here it is the literal condition the issue is about.\n *\n * `watchStartedAtMs` is when this deployment first read the marks. A job with\n * no mark is silent only once a fire time has passed SINCE then — otherwise\n * every row would be red on the day this deploys, and `usage-email` would\n * stay red until the 1st.\n *\n * Pure on purpose, like its siblings: the route reads, this decides, the\n * spec exercises every branch without a network.\n */\nexport function cronJobsHealth(\n beats: readonly CronBeat[] | null,\n watchStartedAtMs: number,\n ms: number,\n now: number = Date.now(),\n jobs: readonly ScheduledJob[] = SCHEDULED_JOBS,\n): Record<string, CronJobCheck> {\n const byId = new Map((beats ?? []).map((beat) => [beat.jobId, beat.atMs]))\n const checks: Record<string, CronJobCheck> = {}\n for (const job of jobs) {\n const lastBeatMs = byId.get(job.id) ?? null\n const lastBeatAgeMinutes =\n lastBeatMs === null ? null : Math.round((now - lastBeatMs) / 60_000)\n // The most recent fire time that is already past its grace. Everything\n // more recent than that is a run we are still waiting for, on time.\n const dueMs = previousCronFire(job.cron, now - job.graceMinutes * 60_000)\n const base = {\n ms,\n schedule: job.cron,\n runner: job.runner,\n lastBeatAgeMinutes,\n dueAt: dueMs === null ? null : new Date(dueMs).toISOString(),\n graceMinutes: job.graceMinutes,\n }\n if (beats === null) {\n checks[job.id] = { ...base, ok: false, code: 'beats-unavailable' }\n continue\n }\n if (dueMs === null) {\n // Nothing was due inside the lookback. Green, and honestly so.\n checks[job.id] = { ...base, ok: true }\n continue\n }\n if (lastBeatMs === null) {\n // Never reported. Only a defect once a fire time has passed since we\n // started watching — before that it is a job we have not met yet.\n const overdue = dueMs > watchStartedAtMs\n checks[job.id] = overdue\n ? { ...base, ok: false, code: 'job-never-reported' }\n : { ...base, ok: true, code: 'awaiting-first-run' }\n continue\n }\n checks[job.id] =\n lastBeatMs < dueMs\n ? { ...base, ok: false, code: 'job-silent' }\n : { ...base, ok: true }\n }\n return checks\n}\n\n/**\n * The mark a run leaves — the only writes on the beat path.\n *\n * Structurally typed against firebase-admin's Firestore rather than importing\n * it: this module is pure and is imported by tenant Server Components, and a\n * health verdict library that drags in the admin SDK would be a new reason\n * for a page to fail. The console routes, the marketing plugin's scheduled\n * campaign processor and the tenant job runner all pass their own handle.\n *\n * Two writes, in order: the job's own document, which is the authority, then\n * its entry in {@link CRON_BEAT_SUMMARY_DOC}, which is what the reader reads\n * first. The summary merges one map key, so jobs beating at the same moment\n * do not overwrite each other's entries.\n *\n * NEVER THROWS. A beat that cannot be written must not take down the job it\n * is describing — the monitor becoming the outage is its own failure mode.\n * It returns false when the job's own mark could not be written, and a write\n * that keeps failing shows up where it should: as a silent job on the board.\n * A failed summary write still returns true, because the mark that decides\n * the verdict landed.\n */\nexport interface CronBeatStore {\n collection(name: string): {\n doc(id: string): {\n set(data: Record<string, unknown>, options?: unknown): Promise<unknown>\n }\n }\n}\n\nexport async function writeCronBeat(\n store: CronBeatStore,\n jobId: string,\n now: number = Date.now(),\n): Promise<boolean> {\n try {\n const beats = store.collection(CRON_BEAT_COLLECTION)\n await beats\n .doc(jobId)\n .set({ jobId, atMs: now, at: new Date(now).toISOString() }, { merge: true })\n try {\n await beats\n .doc(CRON_BEAT_SUMMARY_DOC)\n .set({ beats: { [jobId]: now } }, { merge: true })\n } catch {\n // The reader confirms a stale summary entry against the mark above.\n }\n return true\n } catch {\n return false\n }\n}\n\n/**\n * The beats a summary document holds, ignoring anything malformed.\n *\n * `raw` is the document's `beats` field as Firestore returns it — a plain map,\n * or undefined when the document does not exist yet.\n */\nexport function cronBeatsFromSummary(raw: unknown): CronBeat[] {\n if (!raw || typeof raw !== 'object') return []\n return Object.entries(raw as Record<string, unknown>)\n .map(([jobId, atMs]) => ({ jobId, atMs: Number(atMs) }))\n .filter((beat) => !CRON_BEAT_RESERVED_DOCS.has(beat.jobId))\n .filter((beat) => Number.isFinite(beat.atMs))\n}\n\n/**\n * DID A STRANGER ACTUALLY SIGN UP? (AGL-2715)\n *\n * Every other signup check on this platform is an inference. The auth doors\n * assert that Identity Platform is reachable and refuses an account that\n * cannot exist — configuration, not a signup. `probeCreate` reads that the\n * platform is unlocked and the probe's name is free — preconditions, not a\n * signup. `signupVolume`, `signupRefusals` and `signupDrought` count what\n * happened afterwards — a consequence, observed late.\n *\n * The strongest thing any of them could say, on the day AGL-2714 paged, was\n * \"somebody got an account recently\", and recently was 24 hours ago. AGL-2581\n * is what that costs: signup refused every visitor for three days while the\n * monitor named for signups stayed green.\n *\n * So something has to WALK it. A canary creates an account, verifies it,\n * mints a session, provisions an org, and deletes all of it — against\n * production, on a schedule — then stamps what happened into one marker. This\n * verdict reads that marker.\n *\n * ## Why the walk is not done here\n *\n * Health probes on this platform are read-only and cost-bounded by contract,\n * and this one is served from a PUBLIC unauthenticated endpoint. A probe that\n * created an account would hand anyone with `curl` an org factory. The write\n * lives in a scheduled job; the door only reports. Same split as the beacon\n * heartbeat (AGL-2713), for the same reason.\n *\n * ## The four ways this reds, and why residue is one of them\n *\n * A canary that walks perfectly and fails to clean up is not a success. It\n * leaves a real org, with a real slug reservation and a real host, on the\n * production estate — every hour, forever. `canary-left-residue` is red even\n * though the signup itself worked, because the alternative is a monitor that\n * quietly fills the database it is monitoring.\n */\nexport interface SignupCanaryMarker {\n /** When the walk finished. The freshness clock. */\n walkedAtMs?: number\n /** Whether every step of the walk succeeded. */\n ok?: boolean\n /** Which step broke, for the body. Never an error message, never a uid. */\n failedStep?: string | null\n /** Wall clock for the whole walk. */\n elapsedMs?: number\n /** Whether the walk deleted everything it created. */\n reapedCleanly?: boolean\n}\n\nexport interface SignupCanaryCheck extends HealthCheck {\n /** Age of the last recorded walk, or null when there is no marker. */\n ageMs: number | null\n /** Which step broke on that walk, when one did. */\n failedStep: string | null\n /** How long the walk took, for the latency graph. */\n walkMs: number | null\n /** Whether it cleaned up after itself. */\n reapedCleanly: boolean | null\n /** The age past which a walk stops counting as evidence. */\n staleAfterMs: number\n}\n\n/**\n * How old the last successful walk may be before this reds.\n *\n * Two hours against an hourly schedule — one missed run is tolerated, two are\n * not. A canary that runs hourly and reds at 61 minutes would page on every\n * GitHub Actions queue delay, and an alarm that cries wolf on infrastructure\n * latency is one nobody reads by the time signup actually breaks. Two misses\n * is a pattern rather than a hiccup.\n *\n * This is deliberately NOT generous enough to sleep through AGL-2581: three\n * days of refused signups would red this inside two hours.\n */\n/**\n * Workspace-slug prefix the signup canary owns, and the one piece of the\n * product that has to know the canary exists (AGL-2715).\n *\n * ## Why an exclusion is not optional\n *\n * The canary creates a real org and then deletes it. `signupDrought` compares\n * durable ATTEMPT markers against a live COUNT of orgs — and a deleted org\n * leaves the count while its attempt marker stays. So every canary walk adds\n * 1 to the denominator and 0 to the numerator, and after three walks the\n * platform reports `signup-drought` with nothing wrong.\n *\n * That is not theoretical. It fired on 2026-09-09 at six attempts against\n * zero creations — six walks that had each just proved, end to end, that a\n * stranger could sign up. A monitor that manufactures the outage it watches\n * for is worse than no monitor, and it is the same alarm fatigue AGL-2714\n * removed, arriving from inside the house.\n *\n * ## Why the slug, and what that costs\n *\n * `/api/orgs/create` receives the requested slug before it does anything, so\n * this needs no secret, no header and no second code path — the canary is\n * excluded by being what it says it is.\n *\n * The cost is that the prefix is caller-supplied: someone naming their\n * workspace `signup-canary-…` would not be counted in the drought's\n * denominator. That is bounded to one uncounted attempt per such workspace,\n * and it cannot hide an outage, because an outage is many attempts by many\n * people and only the oddly-named ones would go missing.\n *\n * The canary declares the same prefix in `tools/e2e/signup-canary.mjs`, which\n * cannot import this file — it runs outside the workspace. They are held\n * together by `apps/console/specs/signup-canary-marker-wiring.spec.ts`.\n */\nexport const SIGNUP_CANARY_ORG_SLUG_PREFIX = 'signup-canary-'\n\n/**\n * Is this org creation the canary's own?\n *\n * Exported as a function rather than leaving callers to compare strings, so\n * the one place that decides is the one place to read when asking why a\n * number looks wrong.\n */\nexport function isSignupCanaryOrgSlug(slug: string | null | undefined): boolean {\n return typeof slug === 'string' && slug.startsWith(SIGNUP_CANARY_ORG_SLUG_PREFIX)\n}\n\n/**\n * The plus tag every canary address carries.\n *\n * Identity Platform will not reuse an address, so the walk stamps a fresh one\n * per run by appending to the LOCAL PART of `SIGNUP_CANARY_EMAIL`:\n * `name+signup-canary@example.com` becomes\n * `name+signup-canary-m2rso9ab12@example.com`, which still reaches the same\n * mailbox. The tag is therefore the only part of a canary account that the\n * console can recognize at the moment it first sees one — there is no org yet,\n * no claim, and nothing on the token that says CI.\n *\n * The address is operator-supplied, so this is a convention rather than a\n * fact, and `tools/e2e/signup-canary.mjs` is what makes it one: it refuses to\n * start on a base address whose tag is not this. Held together by\n * `apps/console/specs/signup-canary-marker-wiring.spec.ts`, exactly as the\n * slug prefix above is.\n */\nexport const SIGNUP_CANARY_EMAIL_TAG = 'signup-canary'\n\n/**\n * Is this sign-up the canary's own?\n *\n * The cost mirrors the slug prefix's: a stranger who signs up as\n * `them+signup-canary@example.com` goes unannounced. That is bounded to the\n * one address, it cannot hide anything staff need — the account still exists,\n * still appears in the admin user list, and still bills — and it is the price\n * of a check that needs no secret and no second code path.\n */\nexport function isSignupCanaryEmail(email: string | null | undefined): boolean {\n if (typeof email !== 'string') return false\n const local = email.toLowerCase().split('@')[0] ?? ''\n const plus = local.indexOf('+')\n if (plus < 0) return false\n // `startsWith`, not equality: the walk's own address is the base tag plus a\n // stamp, and the base address itself must match too — a human reading the\n // mailbox should not find the two classified differently.\n return local.slice(plus + 1).startsWith(SIGNUP_CANARY_EMAIL_TAG)\n}\n\n/**\n * How old the last walk may be before the check goes red.\n *\n * Six hours, far wider than the schedule that writes it, and deliberately so.\n * GitHub Actions serves `schedule` from a best-effort queue that drops most\n * requests under load: measured 2026-09-10, `uptime-probe.yml` asks for every\n * fifteen minutes and actually fired at gaps of 1h55m to 4h38m across the\n * preceding day and a half. A window sized to the cron expression rather than\n * to that behavior reds on ordinary queue depth.\n *\n * Widening it costs no detection speed, because staleness is not how a broken\n * signup surfaces. A walk that cannot sign up FAILS and still writes its\n * marker, which reads `signup-walk-failed` on the next poll. Staleness can\n * only mean the walker never reported — a CI condition, not a product one —\n * so this window's whole job is to notice a canary that has genuinely stopped,\n * and it must not fire before then: this check grades the monitor that must\n * never be muted.\n */\nexport const SIGNUP_CANARY_STALE_AFTER_MS = 6 * 60 * 60 * 1000\n\n/**\n * Grade the last recorded signup walk.\n *\n * Pure, like every sibling: the route reads the marker, this decides. Ordered\n * by which mistake costs most — a missing marker is treated as failure, never\n * as calm, because \"no evidence\" and \"evidence of success\" are the two\n * readings this whole issue exists to stop anyone from confusing.\n */\nexport function signupCanaryHealth(\n marker: SignupCanaryMarker | null,\n ms: number,\n nowMs: number = Date.now(),\n staleAfterMs: number = SIGNUP_CANARY_STALE_AFTER_MS,\n): SignupCanaryCheck {\n const base: Omit<SignupCanaryCheck, 'ok' | 'code'> = {\n ms,\n ageMs: null,\n failedStep: null,\n walkMs: null,\n reapedCleanly: null,\n staleAfterMs,\n }\n // No marker at all, or a store that would not answer. The canary has never\n // run, or cannot be read — either way nothing here has demonstrated that a\n // stranger can sign up, and this check exists to say so out loud.\n if (!marker || typeof marker.walkedAtMs !== 'number') {\n return { ...base, ok: false, code: 'canary-unavailable' }\n }\n const ageMs = Math.max(0, nowMs - marker.walkedAtMs)\n const walkMs = typeof marker.elapsedMs === 'number' ? marker.elapsedMs : null\n const reapedCleanly =\n typeof marker.reapedCleanly === 'boolean' ? marker.reapedCleanly : null\n const failedStep =\n typeof marker.failedStep === 'string' ? marker.failedStep : null\n const carried = { ...base, ageMs, walkMs, reapedCleanly, failedStep }\n\n // A stale PASS is not a pass. A walk that succeeded long enough ago says\n // nothing about the door now, and the whole point of a canary is currency.\n // Checked before the verdict so a stale failure reports as stale rather\n // than as a failure that may since have healed.\n if (ageMs > staleAfterMs) {\n return { ...carried, ok: false, code: 'canary-stale' }\n }\n if (marker.ok !== true) {\n return { ...carried, ok: false, code: 'signup-walk-failed' }\n }\n // Walked fine, did not clean up. Red anyway — see the docblock.\n if (reapedCleanly === false) {\n return { ...carried, ok: false, code: 'canary-left-residue' }\n }\n return { ...carried, ok: true }\n}\n\n/**\n * IS ATTESTATION WORKING FOR REAL PEOPLE? (AGL-2715)\n *\n * The signup canary walks the real journey, but it attests with an App Check\n * DEBUG TOKEN — because this project's provider is reCAPTCHA Enterprise, which\n * is built to score headless automation as a bot and always will. Everything\n * else in that walk is real; the attestation step is not. So the canary is\n * structurally blind to the one failure it sits closest to: App Check refusing\n * the people it is supposed to admit.\n *\n * That blindness is not acceptable on its own, and it is not fixed by\n * argument. It is fixed by measuring the thing directly.\n *\n * ## What this reads, and why a token cannot fool it\n *\n * Firebase publishes `firebaseappcheck.googleapis.com/services/verification_count`\n * to Cloud Monitoring, labelled by `result` (ALLOW / DENY) and `security`\n * (VALID / INVALID / MISSING_UNKNOWN_ORIGIN), per service. Those counts are\n * every request from every visitor.\n *\n * The canary contributes at most one verification an hour against roughly a\n * thousand a day, and its debug-token attestation lands in the same ALLOW\n * bucket as everyone else's. So it cannot hold this green: if attestation\n * collapses for real people the rate collapses with it, one canary request\n * notwithstanding.\n *\n * ## Why only Identity Platform is graded\n *\n * `identitytoolkit.googleapis.com` is the service signup depends on, and it\n * runs clean — 939 ALLOW against 3 DENY over the 24 hours to 2026-09-09.\n * `firestore.googleapis.com` in the same window was 5493 ALLOW against 1046\n * DENY, because client Firestore reads are reached by crawlers and stale tabs\n * that legitimately fail attestation. Grading that number would either mute\n * this check with a floor low enough to admit the noise, or page on the noise.\n * The other services are reported for diagnosis and decide nothing.\n */\nexport interface AppCheckVerificationSample {\n /** `ALLOW` or `DENY`. */\n result?: string\n /** `VALID`, `INVALID`, `MISSING_UNKNOWN_ORIGIN`, … — for the body only. */\n security?: string\n /** The service the verdict was rendered for. */\n service?: string\n /** Requests in this bucket over the window. */\n count?: number\n}\n\n/**\n * IS THE EDGE STILL LETTING REAL VISITORS IN? (AGL-2720)\n *\n * Every synthetic on this platform carries the `x-aglyn-probe` firewall\n * bypass, because bot protection refuses a datacenter IP outright. So if the\n * edge started challenging real people, every check would ride past the thing\n * that was broken and stay green.\n *\n * Measured 2026-09-10 while looking for something to close that: Vercel\n * publishes no challenged-versus-allowed metric — seven endpoints probed, the\n * only one that answers returns rule actions rather than traffic. And a probe\n * cannot substitute, because the discriminator is not \"was this request\n * challenged\" but \"can a REAL BROWSER get in\": `curl` from a residential\n * address is challenged too, and that is the healthy state, since the\n * challenge is answerable and a non-JS client cannot answer it.\n *\n * What is left is the outcome, and it was already being counted for money.\n * Metered page views are written by real browsers on real customer sites\n * after the edge admitted them — 57 to 563 a day across hosts over the\n * preceding week. Nothing synthetic touches that number: the probes ride the\n * bypass and the signup canary walks the console, neither of which is a\n * metered tenant page view.\n *\n * So the question this asks is simply whether that number is still moving. A\n * platform serving tens of views an hour that serves none for hours has\n * either lost its edge or lost every customer site, and both are worth waking\n * somebody for.\n *\n * ⛔ What it is NOT. It watches the TENANT edge, because that is where the\n * volume is. The console's own front door serves a handful of signup views a\n * day — too few to floor, and now partly synthetic — so a firewall change\n * scoped only to `aglyn-console` would not show up here. It is also not a\n * latency or correctness check: it says traffic arrives, nothing more.\n *\n * ⚠️ And the volume is CONCENTRATED. On the day this was built, one host of\n * thirteen produced every view. So this reds if that one site goes quiet for\n * half a day, which is why the window is what it is — see\n * {@link EDGE_ADMISSION_QUIET_AFTER_MS}.\n */\n/**\n * What the metered-traffic sampler leaves behind.\n *\n * `advancedAtMs` is the whole point and it is the WRITER's judgement, not the\n * reader's: the sampler compares this reading against the last one and stamps\n * the moment the total actually grew. A day rollover sets the counter back to\n * zero, so a grader handed a single sample could not tell midnight from an\n * outage.\n */\nexport interface EdgeAdmissionMarker {\n /** When the sampler last ran. Context; the verdict does not use it. */\n sampledAtMs?: number\n /** The last moment the metered total was seen to GROW. The clock. */\n advancedAtMs?: number\n /** Which UTC day the total belongs to. */\n day?: string\n /** The running total for that day at the last sample. */\n total?: number\n}\n\nexport interface EdgeAdmissionCheck extends HealthCheck {\n /** How long since the metered total last moved, or null with no marker. */\n quietMs: number | null\n /** The day the sampled total belongs to, so a rollover is readable. */\n day: string | null\n /** The running total for that day at the last sample. */\n total: number | null\n /** How long the total may sit still before this reds. */\n quietAfterMs: number\n /** How long since the sampler last reported, or null with no marker. */\n sampleAgeMs: number | null\n}\n\n/**\n * How long metered page views may sit still before the edge is presumed shut.\n *\n * Twelve hours, and the width is deliberate rather than lazy.\n *\n * The quietest full day in the sampled week carried 57 views — about 2.4 an\n * hour on average, which would make six hours of zero look damning. But the\n * average is the wrong statistic here: measured 2026-09-10, the day's 62 views\n * came from ONE host of thirteen. Traffic that concentrated arrives in bursts,\n * and a burst distribution produces long empty stretches with nothing wrong.\n * A window sized to the average would page on one of them, which is how the\n * monitor that must never be ignored gets ignored.\n *\n * Twelve hours of exactly zero, on a platform serving 57 to 563 views a day,\n * is not a quiet stretch. It is slower to fire than anyone would like, and it\n * is the only thing that catches a fully blocked edge at all — nothing else\n * would ever fire, because every other check rides the bypass.\n *\n * ⚑ Tighten it once the real quiet-period distribution is known: the sampler\n * stamps `advancedAtMs` on every run, so a week of markers says what the true\n * maximum gap is. Do not tighten it from the daily average.\n */\nexport const EDGE_ADMISSION_QUIET_AFTER_MS = 12 * 60 * 60 * 1000\n\n/**\n * How old the sampler's own reading may be before it stops being evidence.\n *\n * Six hours, sized like `SIGNUP_CANARY_STALE_AFTER_MS` to a scheduler that\n * drops most of what it is asked for (AGL-2723) rather than to the cron\n * expression. Shorter than the quiet window on purpose: a stalled sampler\n * should be named as one BEFORE its frozen reading has had time to look like\n * an edge outage.\n */\nexport const EDGE_ADMISSION_SAMPLE_STALE_AFTER_MS = 6 * 60 * 60 * 1000\n\n/**\n * Grade the last recorded metered-traffic sample.\n *\n * Pure, like every sibling. The sampler decides when the total last MOVED and\n * stamps it; this only asks how long ago that was. Keeping the movement\n * detection in the writer is what lets the door stay a read: a day rollover\n * resets the counter to zero, and a grader looking at one sample could not\n * tell that from traffic stopping.\n */\nexport function edgeAdmissionHealth(\n marker: EdgeAdmissionMarker | null,\n ms: number,\n nowMs: number = Date.now(),\n quietAfterMs: number = EDGE_ADMISSION_QUIET_AFTER_MS,\n sampleStaleAfterMs: number = EDGE_ADMISSION_SAMPLE_STALE_AFTER_MS,\n): EdgeAdmissionCheck {\n const base: Omit<EdgeAdmissionCheck, 'ok' | 'code'> = {\n ms,\n quietMs: null,\n day: null,\n total: null,\n quietAfterMs,\n sampleAgeMs: null,\n }\n // No marker, or one that cannot be read. Absence of proof reds here for the\n // same reason it does on every sibling: \"nobody measured\" and \"traffic is\n // arriving\" are the two readings this exists to keep apart.\n if (!marker || typeof marker.advancedAtMs !== 'number') {\n return { ...base, ok: false, code: 'edge-admission-unavailable' }\n }\n const sampleAgeMs =\n typeof marker.sampledAtMs === 'number'\n ? Math.max(0, nowMs - marker.sampledAtMs)\n : null\n const carried = {\n ...base,\n quietMs: Math.max(0, nowMs - marker.advancedAtMs),\n day: typeof marker.day === 'string' ? marker.day : null,\n total: typeof marker.total === 'number' ? marker.total : null,\n sampleAgeMs,\n }\n // ⛔ CHECKED FIRST, AND THE ORDER IS THE POINT.\n //\n // `advancedAtMs` only moves when the sampler runs, so a sampler that has\n // stopped looks exactly like traffic that has stopped — and the responder\n // would go looking at the firewall while the real fault was a job that\n // never fired. That is the confusion `canary-stale` was built to avoid on\n // the check next door, and it costs one comparison to avoid here too.\n if (sampleAgeMs === null || sampleAgeMs > sampleStaleAfterMs) {\n return { ...carried, ok: false, code: 'edge-sample-stale' }\n }\n if (carried.quietMs !== null && carried.quietMs > quietAfterMs) {\n return { ...carried, ok: false, code: 'edge-admits-nobody' }\n }\n return { ...carried, ok: true }\n}\n\nexport interface AppCheckAttestationCheck extends HealthCheck {\n /** Allowed over total for the graded service, or null with no traffic. */\n allowRate: number | null\n /** Requests the rate is taken over. */\n allowed: number\n denied: number\n /** Below this many requests the rate says nothing and is not graded. */\n minimumRequests: number\n /** The floor the rate must clear. */\n minimumAllowRate: number\n /** The service graded. Everything else is context. */\n service: string\n /** Denials by `security`, so a red says which way it broke. */\n deniedBy: Record<string, number>\n /** The window the counts cover. */\n windowMinutes: number\n}\n\n/** The service signup depends on. See the docblock for why it is the only one. */\nexport const APP_CHECK_GRADED_SERVICE = 'identitytoolkit.googleapis.com'\n\n/**\n * Window the attestation rate is taken over.\n *\n * Six hours, not one. This metric is aggregated by Google on its own schedule\n * and arrives late, so a one-hour window reads empty for reasons that have\n * nothing to do with attestation. Six hours is long enough to always carry\n * traffic on this platform and short enough that a real collapse is caught the\n * same working day.\n */\nexport const APP_CHECK_WINDOW_MINUTES = 6 * 60\n\n/**\n * Requests below which the rate is not graded.\n *\n * A ratio over three requests is noise wearing a percentage. Twenty is a\n * fraction of the ~940 a day Identity Platform sees, so a healthy platform\n * always clears it, and a platform quiet enough not to is a platform where\n * this check has nothing to say.\n */\nexport const APP_CHECK_MIN_REQUESTS = 20\n\n/**\n * The floor the allow rate must clear.\n *\n * 0.90, against a measured 0.997. The gap is deliberate: a handful of genuine\n * denials is ordinary — a stale tab, a bot, a browser that failed its\n * reCAPTCHA — and a floor set near the observed rate would page on those.\n *\n * What this is built to catch is not a drift of a few percent; it is the\n * documented failure mode of `recaptcha-allowlist.ts`, where an origin that is\n * attached and routed but not allowlisted renders a console that can never\n * sign anyone in. That reads as ~0%, not 85%.\n */\nexport const APP_CHECK_MIN_ALLOW_RATE = 0.9\n\n/**\n * Grade attestation for real visitors.\n *\n * Pure, like every sibling: something else reads Cloud Monitoring, this\n * decides. Null samples are degraded rather than calm — an unreadable metric\n * is not evidence that attestation works, and this check exists precisely\n * because \"we did not look\" was being reported as \"it is fine\".\n */\nexport function appCheckAttestationHealth(\n samples: AppCheckVerificationSample[] | null,\n ms: number,\n service: string = APP_CHECK_GRADED_SERVICE,\n minimumRequests: number = APP_CHECK_MIN_REQUESTS,\n minimumAllowRate: number = APP_CHECK_MIN_ALLOW_RATE,\n windowMinutes: number = APP_CHECK_WINDOW_MINUTES,\n): AppCheckAttestationCheck {\n const base: Omit<AppCheckAttestationCheck, 'ok' | 'code'> = {\n ms,\n allowRate: null,\n allowed: 0,\n denied: 0,\n minimumRequests,\n minimumAllowRate,\n service,\n deniedBy: {},\n windowMinutes,\n }\n if (samples === null) {\n return { ...base, ok: false, code: 'attestation-unavailable' }\n }\n let allowed = 0\n let denied = 0\n const deniedBy: Record<string, number> = {}\n for (const sample of samples) {\n if (sample.service !== service) continue\n // `Number(...) || 0` for the reason every sibling gives: a corrupt count\n // must not turn the total into NaN, which compares false against every\n // threshold and would report calm forever.\n const count = Number(sample.count) || 0\n if (sample.result === 'ALLOW') allowed += count\n else if (sample.result === 'DENY') {\n denied += count\n const why = sample.security || 'UNKNOWN'\n deniedBy[why] = (deniedBy[why] ?? 0) + count\n }\n }\n const total = allowed + denied\n const carried = { ...base, allowed, denied, deniedBy }\n // Too little traffic to say anything. Reported, not hidden: an on-call\n // person reading a green body deserves to know this one abstained.\n if (total < minimumRequests) {\n return { ...carried, ok: true, code: 'not-enough-traffic', allowRate: null }\n }\n const allowRate = allowed / total\n return {\n ...carried,\n allowRate,\n ok: allowRate >= minimumAllowRate,\n ...(allowRate >= minimumAllowRate ? {} : { code: 'attestation-failing' }),\n }\n}\n"],"names":["HEALTH_NO_STORE","healthStatus","checks","Object","values","every","check","ok","healthBody","report","status","service","commit","version","environment","region","at","Date","toISOString","healthHeaders","healthHttpStatus","healthHeadOf","get","response","Response","headers","MAX_BACKUP_AGE_DAYS","BACKUP_IN_FLIGHT","Set","backupsHealth","backups","ms","now","options","code","states","newestReadyAgeDays","determinate","backup","state","snapshotMsOf","snapshotTime","parse","Number","NaN","newestOf","keep","filter","map","time","isFinite","reduce","newest","Math","max","ageDaysOf","round","newestReadyMs","newestAnyMs","newestAnyAgeDays","undateable","some","has","unreachable","length","MAX_EXPORT_AGE_DAYS","exportsHealth","markers","exportCount","newestExportAgeDays","newestMs","marker","timeCreated","undefined","ORG_CREATION_WINDOW_MINUTES","MAX_ORG_CREATIONS_PER_WINDOW","signupsHealth","recentOrgCreations","threshold","windowMinutes","SIGNUP_REFUSAL_WINDOW_MINUTES","MAX_SIGNUP_REFUSALS_PER_WINDOW","SIGNUP_LOCK_UNREADABLE_FIELD","MAX_UNREADABLE_SIGNUP_DECISIONS_PER_WINDOW","signupRefusalsHealth","unreadableThreshold","refusedSignups","refusedByReason","lockUnreadable","minutesSinceLast","newestAtMs","refusals","unreadable","reason","count","entries","byReason","refusedAtMs","over","blind","floor","SIGNUP_DROUGHT_WINDOW_MINUTES","MIN_SIGNUP_TRAFFIC_FOR_DROUGHT","signupDroughtHealth","attemptMarkers","orgCreations","minimumTraffic","signupPagesServed","serves","signupAttempts","attempts","drought","RATE_LIMIT_DEGRADED_WINDOW_MINUTES","MAX_DEGRADED_CALLS_PER_WINDOW","rateLimitsHealth","degradedCalls","degradedEpisodes","cutoff","recent","lastAtMs","total","calls","episodes","latest","Infinity","SERVER_ERROR_WINDOW_MINUTES","MAX_SERVER_ERRORS_PER_WINDOW","serverErrorsHealth","serverErrors","byService","erroredAtMs","errors","HEALTH_PROBE_TTL_MS","memoizeWithTtl","ttlMs","probe","cached","inFlight","elapsed","value","flight","clear","then","isTransientBeaconCode","startsWith","beaconHealth","write","logId","tolerance","landedAtMs","graceMs","sinceLanded","base","minutesSinceHeartbeat","graceMinutes","renderHealth","outcome","host","kind","hostResolved","nodeCount","WEBHOOK_FAILURE_WINDOW_MINUTES","MAX_FAILED_DELIVERIES_PER_WINDOW","MAX_INERT_DELIVERIES_PER_WINDOW","MAX_RETRIED_DELIVERIES_PER_WINDOW","billingWebhookHealth","facts","inertThreshold","retriedThreshold","endpointStatus","undelivered","emitted","processed","inert","retried","unsubscribedEvents","connectEndpoint","unsubscribedConnectEvents","meteredPricingHealth","stripeConfigured","monthly","yearly","unbilledInterval","CRON_BEAT_COLLECTION","CRON_BEAT_WATCH_DOC","CRON_BEAT_DEGRADED_DOC","CRON_BEAT_SUMMARY_DOC","CRON_BEAT_RESERVED_DOCS","CRON_DEGRADED_WINDOW_CONTINUITY_MS","SCHEDULED_JOBS","id","label","cron","runner","target","drives","expandCronField","field","min","part","split","range","stepRaw","step","parseInt","from","to","includes","a","b","add","size","parseCron","expression","fields","trim","minute","hour","dayOfMonth","month","dayOfWeek","parsed","domRestricted","dowRestricted","cronDayMatches","date","getUTCMonth","domHit","getUTCDate","dowHit","getUTCDay","previousCronFire","atMs","lookbackDays","cursor","startOfDay","UTC","getUTCFullYear","getUTCHours","getUTCMinutes","cronJobsHealth","beats","watchStartedAtMs","jobs","byId","Map","beat","jobId","job","lastBeatMs","lastBeatAgeMinutes","dueMs","schedule","dueAt","overdue","writeCronBeat","store","collection","doc","set","merge","cronBeatsFromSummary","raw","SIGNUP_CANARY_ORG_SLUG_PREFIX","isSignupCanaryOrgSlug","slug","SIGNUP_CANARY_EMAIL_TAG","isSignupCanaryEmail","email","local","toLowerCase","plus","indexOf","slice","SIGNUP_CANARY_STALE_AFTER_MS","signupCanaryHealth","nowMs","staleAfterMs","ageMs","failedStep","walkMs","reapedCleanly","walkedAtMs","elapsedMs","carried","EDGE_ADMISSION_QUIET_AFTER_MS","EDGE_ADMISSION_SAMPLE_STALE_AFTER_MS","edgeAdmissionHealth","quietAfterMs","sampleStaleAfterMs","quietMs","day","sampleAgeMs","advancedAtMs","sampledAtMs","APP_CHECK_GRADED_SERVICE","APP_CHECK_WINDOW_MINUTES","APP_CHECK_MIN_REQUESTS","APP_CHECK_MIN_ALLOW_RATE","appCheckAttestationHealth","samples","minimumRequests","minimumAllowRate","allowRate","allowed","denied","deniedBy","sample","result","why","security"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/health-report.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Shared shape for the `/api/health` endpoints (AGL-1102).\n *\n * Both the console and the tenant runtime answer this, and they must answer it\n * IDENTICALLY — an uptime series is only comparable across services if the\n * status codes and the field names agree. Two near-identical route files would\n * have drifted the first time one of them was touched.\n *\n * The rules encoded here, each of which is a way health checks routinely lie:\n *\n * **The status code is the contract.** Most uptime monitors read nothing else,\n * so a degraded dependency has to be a 503. A 200 whose body says \"degraded\"\n * is a signal nobody receives.\n *\n * **Never cacheable.** A cached health check returns 200 from the edge while\n * the origin is on fire, and the graph stays green straight through the\n * outage. Three separate caches have faked a green check in this repo already,\n * so this sets `no-store` on every response including the failures — an error\n * response is the one you least want served from a cache.\n *\n * **No internals in the body.** It is public and unauthenticated, so a failing\n * check reports a CODE and never the underlying message, which can carry\n * project ids, paths or credentials fragments.\n */\n\nexport interface HealthCheck {\n ok: boolean\n /** How long the dependency took. The number that turns into a latency graph. */\n ms: number\n /** A stable code on failure — never a raw error message. */\n code?: string\n}\n\nexport interface HealthReport {\n service: string\n checks: Record<string, HealthCheck>\n commit?: string | null\n /**\n * Which VERSION of the platform answered (AGL-2091).\n *\n * `commit` alone could only ever be answered on Aglyn's own cloud, so a\n * self-host operator had nothing to quote in a bug report and no way to ask\n * whether a fix had reached them. This is the field that answers it, and\n * unlike the commit it needs no operator configuration —\n * `platformVersion()` reads the number the build inlined from package.json.\n */\n version?: string | null\n environment?: string | null\n region?: string | null\n /** Injected so the body is deterministic in tests. */\n at?: string\n}\n\nexport const HEALTH_NO_STORE =\n 'no-store, no-cache, must-revalidate, max-age=0'\n\n/** Degraded if ANY check failed. There is no partial credit for uptime. */\nexport function healthStatus(checks: Record<string, HealthCheck>): 'ok' | 'degraded' {\n return Object.values(checks).every((check) => check.ok) ? 'ok' : 'degraded'\n}\n\nexport function healthBody(report: HealthReport): Record<string, unknown> {\n return {\n status: healthStatus(report.checks),\n service: report.service,\n checks: report.checks,\n commit: report.commit ?? null,\n version: report.version ?? null,\n environment: report.environment ?? null,\n region: report.region ?? null,\n at: report.at ?? new Date().toISOString(),\n }\n}\n\nexport function healthHeaders(status: 'ok' | 'degraded'): Record<string, string> {\n return {\n 'Cache-Control': HEALTH_NO_STORE,\n // Readable from any origin, deliberately.\n //\n // A status page must not live on the thing it reports on, so it is served\n // from a DIFFERENT deployment and reads these cross-origin. Without this\n // header the browser blocks the read and the page renders every service as\n // down during a perfectly healthy day — a status page that cries outage is\n // worse than none.\n //\n // Safe to open: the body is already public and unauthenticated, carries no\n // secrets, and a failing check reports a code rather than a message.\n 'Access-Control-Allow-Origin': '*',\n // Tells a well-behaved monitor the failure is transient rather than a\n // permanent 5xx worth escalating on the first sample.\n ...(status === 'ok' ? {} : { 'Retry-After': '30' }),\n }\n}\n\nexport function healthHttpStatus(status: 'ok' | 'degraded'): number {\n return status === 'ok' ? 200 : 503\n}\n\n/**\n * A HEAD response for a health endpoint: the SAME status and headers its GET\n * would return, with no body (AGL-1148).\n *\n * ## Why this exists\n *\n * Every health route used to answer HEAD with a hardcoded `200` and the\n * comment \"cheap liveness for monitors that only issue HEAD. Touches\n * nothing.\" Touching nothing is precisely the problem: **a HEAD that knows\n * nothing is a health check that cannot go red.** A monitor configured with\n * HEAD — which several uptime providers use by default — would have reported\n * green through any outage that left the function able to boot, which is most\n * of them. `/api/health/crons` sat at 503 for fifty-one hours; a HEAD monitor\n * pointed at it would have agreed with the green board the whole time.\n *\n * It also violated the contract this file opens with. \"The status code is the\n * contract\" cannot hold for one method and not another, and HTTP requires a\n * HEAD response to carry the same status and headers its GET would.\n *\n * ## The cost argument, which is why this is safe\n *\n * Delegating to GET means HEAD runs the dependency probe. Every route\n * memoizes that probe (`memoizeWithTtl`, 15s on the root and 5 minutes on the\n * subsystems) explicitly so a public unauthenticated endpoint cannot be made\n * to read in a loop — the same bound that already lets a fifteen-minute probe\n * hit seven endpoints for nearly nothing. HEAD now costs exactly what GET\n * costs, which is a memo lookup between refreshes, and it serializes no body.\n *\n * Headers are carried over rather than rebuilt so `Cache-Control: no-store`,\n * the open CORS origin and `Retry-After` reach a HEAD client identically — a\n * HEAD response that was cacheable while its GET was not would be the same\n * lie one layer down.\n */\nexport async function healthHeadOf(\n get: () => Promise<Response>,\n): Promise<Response> {\n const response = await get()\n return new Response(null, {\n status: response.status,\n headers: response.headers,\n })\n}\n\n/**\n * Firestore backup state, reduced to a health verdict (AGL-1490, AGL-1502).\n *\n * The 2026-08-02 backup sat at `NOT_AVAILABLE` for eleven days and nothing\n * noticed — the schedule ran, the API reported the failure, and no one was\n * listening. This turns \"is the newest backup real and recent\" into the same\n * 200/503 contract the other health checks speak, so the existing uptime\n * probe + alert path is the listener.\n *\n * Pure on purpose: the route fetches, this decides, the spec exercises every\n * branch without a network.\n */\nexport interface BackupSnapshot {\n /** `READY` | `CREATING` | `NOT_AVAILABLE` (or whatever the API adds next). */\n state?: string\n /** ISO timestamp of the data the backup captured. */\n snapshotTime?: string\n}\n\nexport interface BackupsCheck extends HealthCheck {\n /**\n * Histogram by state — `{ READY: 1, NOT_AVAILABLE: 1 }`. Counts only: the\n * endpoint is public, and backup ids or resource paths have no business in\n * it. Counts are enough to know WHAT is wrong before opening gcloud.\n */\n states: Record<string, number>\n /** Age of the newest READY backup, in days. Null when there is none. */\n newestReadyAgeDays: number | null\n /**\n * Present, and always `false`, ONLY in the third state (AGL-1843): the\n * check could not determine whether backups are healthy. Omitted entirely\n * from a real verdict, exactly as `code` is omitted from a healthy one, so\n * `ok: true` with no `determinate` key means \"measured, and fine\".\n *\n * An indeterminate check reports `ok: true` and therefore does not page.\n * That is deliberate and it is bounded — see the escalation note in\n * `backupsHealth`.\n */\n determinate?: false\n}\n\n/**\n * Weekly Sunday schedule + one day of slack. A healthy cadence never exceeds\n * 7; day 8 means the last run produced nothing usable.\n */\nexport const MAX_BACKUP_AGE_DAYS = 8\n\n/**\n * States in which a backup has not FAILED — it is simply not usable yet.\n *\n * `STATE_UNSPECIFIED` is a real member of the API's `Backup.State` enum (\"The\n * state is unspecified\"), and proto3 JSON omits a default-valued field, so a\n * backup in it arrives with **no `state` key at all**. Treating that absence\n * as a failure is the `strictNullChecks`-off trap this file is otherwise\n * careful about: a missing field must never fold into a red.\n */\nconst BACKUP_IN_FLIGHT = new Set(['CREATING', 'STATE_UNSPECIFIED'])\n\n/**\n * `backups` is the listing, or `null` when it could not be read at all.\n *\n * `options.unreachable` is `ListBackupsResponse.unreachable` — the locations\n * the API could not reach. Non-empty means the listing is a PARTIAL result\n * set. `options.code` is the code to report when `backups` is `null`.\n */\nexport function backupsHealth(\n backups: BackupSnapshot[] | null,\n ms: number,\n now: number = Date.now(),\n options: { unreachable?: string[]; code?: string } = {},\n): BackupsCheck {\n // ## THE THREE STATES (AGL-1843, second pass)\n //\n // This check used to have two: healthy, and failed. Everything it could not\n // establish — an upstream that errored, a partial listing, a run that had\n // not finished — collapsed into \"backup failed\", which is the loudest thing\n // it can say and was, in every one of those cases, false. It reported 503\n // `backup-failed` continuously for four and a half days while the backups\n // were fine, against a monitor that emails on a five-minute interval.\n //\n // ### Why `NOT_AVAILABLE` is not a failure — measured, not assumed\n //\n // The API documents the state as \"The backup is not available **at this\n // moment**\", and that temporal wording is literal. The same three backup\n // ids, read from `gcloud firestore backups list` on three dates:\n //\n // id 2026-08-13 2026-08-17/18 2026-08-24\n // 3b5238df… NOT_AVAILABLE NOT_AVAILABLE READY\n // eb4d21e3… READY NOT_AVAILABLE READY\n // d14ce827… — READY READY\n //\n // `NOT_AVAILABLE` flips in BOTH directions, on backups whose `expireTime`\n // is months away, independent of age. The 2026-08-02 backup that AGL-1490\n // was filed about — \"half the Firestore backups are NOT_AVAILABLE\" — is\n // READY today, twenty-two days old. It was never broken. Neither was the\n // 2026-08-09 one, which went READY → NOT_AVAILABLE → READY. And the\n // predicted permanent day-7 degradation did not happen: d14ce827 is READY\n // at day 8. So every rule that reads a `NOT_AVAILABLE` count as damage is\n // reading a maintenance window as an outage.\n //\n // ### What the check actually needs to answer\n //\n // \"Can we restore, and is the restore point recent?\" That question is\n // answered ENTIRELY by the age of the newest READY backup. No other row in\n // the listing can make a fresh READY backup un-restorable. So a fresh READY\n // backup is unconditionally green, and everything below it is the taxonomy\n // of why we could not find one.\n //\n // ### Why indeterminate answers 200, and why that is not fail-open\n //\n // Three things bound it, because a backups check that cannot go red is\n // worse than no backups check:\n // 1. It is time-bounded. Indeterminacy is only tolerated while a backup\n // younger than `MAX_BACKUP_AGE_DAYS` exists. A schedule that stops, or\n // that never produces anything usable, crosses the budget and goes red\n // within eight days whatever the states say.\n // 2. It never overrides staleness. If a READY backup exists and is over\n // budget, that is `backup-stale` — a newer CREATING or NOT_AVAILABLE\n // row earns no freshness credit, so a run that hangs every week cannot\n // hold the check green.\n // 3. The route classifies PERMANENT upstream failures (no credential,\n // 401/403/404) as hard red rather than indeterminate, so a revoked IAM\n // role cannot silence this forever. And `checks.exports` in the same\n // endpoint is an independent, still-hard-red probe of the GCS copy:\n // \"we cannot read the managed backups\" answers 200 only while we can\n // still prove a fresh independent copy exists.\n //\n // State 3a — the listing could not be read AT ALL. That is not a verdict;\n // it is the absence of one, and reporting it as `backup-failed` is the\n // defect this pass exists to remove.\n if (backups === null) {\n return {\n ok: true,\n ms,\n code: options.code ?? 'backups-unreadable',\n states: {},\n newestReadyAgeDays: null,\n determinate: false,\n }\n }\n\n const states: Record<string, number> = {}\n for (const backup of backups) {\n const state = backup.state ?? 'STATE_UNSPECIFIED'\n states[state] = (states[state] ?? 0) + 1\n }\n\n const snapshotMsOf = (backup: BackupSnapshot): number =>\n backup.snapshotTime ? Date.parse(backup.snapshotTime) : Number.NaN\n const newestOf = (keep: (backup: BackupSnapshot) => boolean): number | null =>\n backups\n .filter(keep)\n .map(snapshotMsOf)\n .filter((time) => Number.isFinite(time))\n .reduce<number | null>((newest, time) => Math.max(newest ?? time, time), null)\n const ageDaysOf = (time: number): number =>\n Math.round(((now - time) / 86_400_000) * 10) / 10\n\n const newestReadyMs = newestOf((backup) => backup.state === 'READY')\n const newestAnyMs = newestOf(() => true)\n const newestReadyAgeDays = newestReadyMs === null ? null : ageDaysOf(newestReadyMs)\n const newestAnyAgeDays = newestAnyMs === null ? null : ageDaysOf(newestAnyMs)\n\n // State 1 — HEALTHY. A restore point exists and it is recent. Nothing else\n // in the listing can falsify that, so nothing else is consulted.\n if (newestReadyAgeDays !== null && newestReadyAgeDays <= MAX_BACKUP_AGE_DAYS) {\n return { ok: true, ms, states, newestReadyAgeDays }\n }\n\n // State 2a — FAILED. A backup that is neither READY nor in flight and that\n // carries no usable `snapshotTime` cannot be aged at all, so we cannot even\n // say when the last run was. That is positive evidence of something wrong,\n // and \"assume it aged out\" is the assumption that hides a broken run.\n const undateable = backups.some(\n (backup) =>\n backup.state !== 'READY' &&\n !BACKUP_IN_FLIGHT.has(backup.state ?? 'STATE_UNSPECIFIED') &&\n !Number.isFinite(snapshotMsOf(backup)),\n )\n if (undateable) {\n return { ok: false, ms, code: 'backup-failed', states, newestReadyAgeDays }\n }\n\n // State 3b — the listing is a PARTIAL result set. `ListBackupsResponse`\n // carries `unreachable`: rather than failing the request when one location\n // cannot be reached, the API returns the backups it could see and names the\n // ones it could not. Positive evidence survives that (state 1 above already\n // returned); negative evidence does not — \"we saw no fresh READY backup\" is\n // not a finding when we did not see everything.\n if ((options.unreachable ?? []).length > 0) {\n return {\n ok: true,\n ms,\n code: 'backups-partial',\n states,\n newestReadyAgeDays,\n determinate: false,\n }\n }\n\n // State 3c — a recent run exists, it simply is not usable AT THIS MOMENT:\n // mid-creation on a Sunday, or inside one of the `NOT_AVAILABLE` windows\n // measured above. Only reachable when NOTHING is READY — a stale READY\n // backup falls through to `backup-stale` below, so this can never buy a\n // hanging schedule more than the eight-day budget.\n if (\n newestReadyAgeDays === null &&\n newestAnyAgeDays !== null &&\n newestAnyAgeDays <= MAX_BACKUP_AGE_DAYS\n ) {\n return {\n ok: true,\n ms,\n code: 'backups-not-ready-yet',\n states,\n newestReadyAgeDays,\n determinate: false,\n }\n }\n\n // State 2b — FAILED on freshness. Either nothing has ever been READY and\n // nothing recent exists to be waiting on, or the newest READY backup is\n // past the budget. Both mean the same operational thing: there is no recent\n // restore point, which is the only fact this check exists to protect.\n return {\n ok: false,\n ms,\n code: newestReadyAgeDays === null ? 'no-ready-backup' : 'backup-stale',\n states,\n newestReadyAgeDays,\n }\n}\n\n/**\n * GCS export freshness, reduced to a health verdict (AGL-1843).\n *\n * Exports are NOT managed backups — they are the independent copy that exists\n * BECAUSE the managed backups proved unreliable (every backup this project\n * took flipped `READY` → `NOT_AVAILABLE` at roughly one week old, AGL-1843).\n * A weekly `exportDocuments` run writes portable snapshot files to a GCS\n * bucket, and this check watches that the newest one is real and recent, so\n * a managed-backup flip no longer means \"one copy only\". The two are kept as\n * SEPARATE checks in the same endpoint precisely so the body says which layer\n * is degraded rather than blending them into one dishonest verdict.\n *\n * The input is the list of `*.overall_export_metadata` completion markers —\n * Firestore writes exactly one per FINISHED export, so the marker's creation\n * time is the moment the export completed, not the moment it started. An\n * export that hangs forever never produces a marker and fails the age rule,\n * the same trick `backupsHealth` plays with `CREATING`.\n *\n * Pure on purpose, like its siblings: the route lists, this decides, the\n * spec exercises every branch without a network.\n */\nexport interface ExportMarker {\n /** ISO timestamp the completion marker was written — when the export FINISHED. */\n timeCreated?: string\n}\n\nexport interface ExportsCheck extends HealthCheck {\n /**\n * Completed exports currently retained. A COUNT only — the endpoint is\n * public, and bucket names or object paths have no business in it. Null\n * when the listing itself failed.\n */\n exportCount: number | null\n /** Age of the newest completed export, in days. Null when there is none. */\n newestExportAgeDays: number | null\n}\n\n/**\n * Weekly cadence (Mondays, `scheduled-crons.yml`) + one day of slack — the\n * same budget as `MAX_BACKUP_AGE_DAYS`, for the same reason: a schedule that\n * stops producing is as broken as one that fails.\n */\nexport const MAX_EXPORT_AGE_DAYS = 8\n\nexport function exportsHealth(\n markers: ExportMarker[] | null,\n ms: number,\n now: number = Date.now(),\n): ExportsCheck {\n // A failed listing is degraded, not ok — same rule as `signupsHealth` and\n // `rateLimitsHealth`: an alarm that cannot see the thing it watches must\n // say so rather than report calm.\n if (markers === null) {\n return {\n ok: false,\n ms,\n code: 'exports-unavailable',\n exportCount: null,\n newestExportAgeDays: null,\n }\n }\n\n const newestMs = markers\n .map((marker) => Date.parse(marker.timeCreated ?? ''))\n .filter((time) => Number.isFinite(time))\n .reduce<number | null>((newest, time) => Math.max(newest ?? time, time), null)\n const newestExportAgeDays =\n newestMs === null ? null : Math.round(((now - newestMs) / 86_400_000) * 10) / 10\n\n // The cron exists, so \"no exports\" is a failure, not a fresh start — the\n // first export ran 2026-08-17, before this check first deployed.\n const code =\n newestExportAgeDays === null\n ? 'no-export'\n : newestExportAgeDays > MAX_EXPORT_AGE_DAYS\n ? 'export-stale'\n : undefined\n\n return {\n ok: code === undefined,\n ms,\n ...(code === undefined ? {} : { code }),\n exportCount: markers.length,\n newestExportAgeDays,\n }\n}\n\n/**\n * Org-creation volume, reduced to a health verdict (AGL-1536).\n *\n * The detection layer over the AGL-1534 rate limit: the limiter bounds each\n * uid (3/h) and each IP (10/h), so a distributed farm holding every actor\n * under both caps is invisible to it — but visible in aggregate. This turns\n * \"how many orgs appeared in the trailing hour\" into the same 200/503\n * contract the other health checks speak, so the existing uptime check +\n * alert + email path is the listener, and a wave becomes an email instead of\n * a surprise. The manual response lever is the AGL-1510 signups feature-lock.\n *\n * Pure on purpose, like `backupsHealth`: the route counts, this decides, the\n * spec exercises every branch without a network.\n */\nexport interface SignupsCheck extends HealthCheck {\n /**\n * Orgs created inside the trailing window. A COUNT only — the endpoint is\n * public, and org names, slugs or owners have no business in it. Null when\n * the count query itself failed.\n */\n recentOrgCreations: number | null\n /** The trailing window the count covers, so the body is self-describing. */\n windowMinutes: number\n /** The count above which this reports degraded. */\n threshold: number\n}\n\n/** Trailing window the org-creation count covers. */\nexport const ORG_CREATION_WINDOW_MINUTES = 60\n\n/**\n * Degraded above 10 creations/hour. Calibrated against two facts: production\n * holds 4 orgs TOTAL (2026-08), so the organic baseline is ~zero per hour;\n * and the AGL-1534 per-IP cap is 10/h, so a single maxed-out address can\n * never trip this — 11+/h necessarily means multiple addresses, which is\n * exactly the distributed signature the rate limiter cannot see. If a launch\n * day legitimately beats this, the alert asks a human to glance at the orgs\n * list and enjoy the view; that is the correct outcome, not a false positive.\n */\nexport const MAX_ORG_CREATIONS_PER_WINDOW = 10\n\nexport function signupsHealth(\n recentOrgCreations: number | null,\n ms: number,\n threshold: number = MAX_ORG_CREATIONS_PER_WINDOW,\n): SignupsCheck {\n // A failed count is degraded, not ok — an alarm that cannot see the thing\n // it watches must say so rather than report calm.\n const code =\n recentOrgCreations === null\n ? 'count-unavailable'\n : recentOrgCreations > threshold\n ? 'signup-wave'\n : undefined\n return {\n ok: code === undefined,\n ms,\n ...(code === undefined ? {} : { code }),\n recentOrgCreations,\n windowMinutes: ORG_CREATION_WINDOW_MINUTES,\n threshold,\n }\n}\n\n/**\n * Refused org creations, reduced to a health verdict (AGL-1907).\n *\n * `signupsHealth` above counts the signups that SUCCEEDED. That is the right\n * alarm for a wave that gets through, and the wrong one for a wave that is\n * being contained: when the AGL-1534 limiter starts refusing, org-creation\n * volume goes *down*, so the check that watches volume reads calmer at exactly\n * the moment scripted pressure arrives. The 429s are the signature. Before the\n * doors open on Sep 1 this is also the only thing that can answer \"has the\n * limiter ever actually fired in production\" — a control nobody has seen\n * refuse is a control nobody has tested.\n *\n * Counted from `rateLimits/signupRefused_{minuteBucket}` markers, which carry\n * per-reason counts and no identifiers. This is a separate check rather than\n * more fields on `SignupsCheck` so the AGL-1536 verdict keeps its meaning\n * exactly: a creation wave and a refusal wave are different events with\n * different responses (feature-lock the signups door vs. leave the limiter to\n * do its job and go read the refusal reasons).\n *\n * Pure on purpose, like its siblings: the route queries, this decides.\n */\nexport interface SignupRefusalMarker {\n /** Refused attempts merged into this minute bucket. */\n refusals?: number\n /**\n * Split by what refused: `{ uid?: n, ip?: n, locked?: n, held?: n }`.\n *\n * `uid` and `ip` are the AGL-1534 rate limiter's two caps. `locked` and\n * `held` come from the `beforeUserCreated` blocking function (AGL-2583) — a\n * door the limiter never sees, because it decides account creation on the\n * Identity Platform path before any Aglyn route runs.\n */\n byReason?: Record<string, number>\n /**\n * Account creations the blocking function admitted BLIND: the signups lock\n * could not be read and nothing this instance remembered said it was pulled\n * (AGL-2583).\n *\n * Deliberately its own field rather than another `byReason` entry. Nobody\n * was refused, so folding it into the refusal split would make both counts\n * lie — and it is graded on its own terms below.\n */\n unreadable?: number\n /** When the most recent refusal in this bucket happened. */\n refusedAtMs?: number\n}\n\nexport interface SignupRefusalsCheck extends HealthCheck {\n /**\n * Refused org creations inside the trailing window. A COUNT only — the\n * endpoint is public, and the limiter's keys are a uid and a hashed client\n * IP. Null when the query itself failed.\n */\n refusedSignups: number | null\n /**\n * Which cap did the refusing. A run that is almost all `ip` is one address\n * hammering; a run that is almost all `uid` is many accounts each pushing\n * past three, which is the distributed shape. Null when the query failed.\n */\n refusedByReason: Record<string, number> | null\n /**\n * Account creations decided WITHOUT the signups lock being readable\n * (AGL-2583). Graded on its own, at zero tolerance, because it is\n * categorically different from every entry in `refusedByReason`: those are\n * controls working as designed, this is the platform answering the signups\n * question without being able to consult it, and one of those is one too\n * many. Null when the query itself failed.\n */\n lockUnreadable: number | null\n /** Minutes since the most recent refusal, so the body dates itself. */\n minutesSinceLast: number | null\n /** The trailing window the count covers, so the body is self-describing. */\n windowMinutes: number\n /** The count above which this reports degraded. */\n threshold: number\n}\n\n/**\n * Trailing window the refusal count covers. One hour, matching the limiter's\n * own window (`org-create` is 3/h and 10/h) so the number is readable against\n * the caps that produced it.\n */\nexport const SIGNUP_REFUSAL_WINDOW_MINUTES = 60\n\n/**\n * Degraded above 50 refusals/hour.\n *\n * Not zero, unlike the limiter-degradation alarm: a refusal is the system\n * working, and a real person who fumbles a workspace slug four times in an\n * hour produces one. The number that means something is *sustained* refusal.\n * Calibrated the same way AGL-1536's was: production holds single-digit orgs\n * (2026-08), so organic refusals are ~zero per hour, and 50 is far above any\n * plausible human while being reached in under a minute by a script. One\n * determined address can generate this alone — that is intended, because \"one\n * address is hammering the signup door\" is worth an email even though the\n * limiter is containing it.\n */\nexport const MAX_SIGNUP_REFUSALS_PER_WINDOW = 50\n\n/**\n * The marker field the blocking function counts blind decisions under\n * (AGL-2583).\n *\n * `signupsCreationVerdict` answers `locked` when a completed read found the\n * lever pulled, `held` when a failed read fell back on an earlier one that\n * had, and — the case this names — admits the account when the read failed\n * and nothing remembered said the lever was pulled. That last one refuses\n * nobody, so it is not a `byReason` entry; it is the platform deciding the\n * signups question without being able to consult it, which is the visible\n * edge of a Firestore read outage on the account-creation path. AGL-2581 was\n * three days of that same read failing, and its only trace was a\n * `logger.warn` nothing alerts on.\n */\nexport const SIGNUP_LOCK_UNREADABLE_FIELD = 'unreadable'\n\n/**\n * Blind signups-lock decisions tolerated per window: NONE.\n *\n * Deliberately not calibrated like `MAX_SIGNUP_REFUSALS_PER_WINDOW` above,\n * which is a volume threshold over refusals that mean the system is working.\n * A blind decision is never routine — a single one says the signups lock was\n * unreadable at least once, and the lock is unenforceable for as long as that\n * lasts. False positives here cost a glance at one endpoint.\n */\nexport const MAX_UNREADABLE_SIGNUP_DECISIONS_PER_WINDOW = 0\n\nexport function signupRefusalsHealth(\n markers: SignupRefusalMarker[] | null,\n ms: number,\n now: number = Date.now(),\n threshold: number = MAX_SIGNUP_REFUSALS_PER_WINDOW,\n windowMinutes: number = SIGNUP_REFUSAL_WINDOW_MINUTES,\n unreadableThreshold: number = MAX_UNREADABLE_SIGNUP_DECISIONS_PER_WINDOW,\n): SignupRefusalsCheck {\n // A failed query is degraded, not ok — the same rule `signupsHealth` and\n // `rateLimitsHealth` follow. An alarm that cannot see the thing it watches\n // must not report calm.\n if (markers === null) {\n return {\n ok: false,\n ms,\n code: 'refusals-unavailable',\n refusedSignups: null,\n refusedByReason: null,\n lockUnreadable: null,\n minutesSinceLast: null,\n windowMinutes,\n threshold,\n }\n }\n\n let refusedSignups = 0\n let lockUnreadable = 0\n let newestAtMs: number | null = null\n const refusedByReason: Record<string, number> = {}\n for (const marker of markers) {\n refusedSignups += marker.refusals ?? 0\n // `Number(...) || 0` for the reason the split below gives: a corrupt\n // Firestore field must not turn this into NaN, which compares false\n // against every threshold and would report calm forever.\n lockUnreadable += Number(marker.unreadable) || 0\n for (const [reason, count] of Object.entries(marker.byReason ?? {})) {\n // `Number(...) || 0` rather than a bare add: these come out of Firestore\n // and a corrupt field must not turn the total into NaN, which compares\n // false against the threshold and would report calm forever.\n refusedByReason[reason] =\n (refusedByReason[reason] ?? 0) + (Number(count) || 0)\n }\n const at = marker.refusedAtMs\n if (typeof at === 'number' && (newestAtMs === null || at > newestAtMs)) {\n newestAtMs = at\n }\n }\n\n const over = refusedSignups > threshold\n // The blind count is graded FIRST and reported over the volume code. A wave\n // and an unreadable lock call for opposite responses — leave the limiter\n // alone and go read the refusal reasons, versus go find out why Firestore\n // is unreadable from the account-creation path — so the code has to name\n // the one that is actually happening.\n const blind = lockUnreadable > unreadableThreshold\n const code = blind\n ? 'signups-lock-unreadable'\n : over\n ? 'signup-refusal-wave'\n : undefined\n return {\n ok: code === undefined,\n ms,\n ...(code === undefined ? {} : { code }),\n refusedSignups,\n refusedByReason,\n lockUnreadable,\n minutesSinceLast:\n newestAtMs === null\n ? null\n : Math.max(0, Math.floor((now - newestAtMs) / 60_000)),\n windowMinutes,\n threshold,\n }\n}\n\n/**\n * NOBODY GOT AN ACCOUNT WHILE PEOPLE WERE TRYING (AGL-2583).\n *\n * `signupsHealth` watches for a FLOOD. Its healthiest possible reading —\n * zero orgs created in the trailing hour — is also what total failure looks\n * like, so the monitor named \"signups\" reports green precisely when nobody\n * can sign up. That is not a hypothetical: account creation was refused for\n * every visitor from Sep 1 to Sep 4 (AGL-2581) and the signup check was\n * green through all of it, because zero is comfortably under ten.\n *\n * ## Zero is only a signal next to a denominator\n *\n * Alone, \"no accounts this hour\" describes a quiet Tuesday night as loudly as\n * an outage, and an alarm that fires on quiet nights is an alarm everyone\n * mutes. What makes zero mean something is TRAFFIC: people arrived at the\n * signup page and none of them ended up with an account.\n *\n * ## The denominator is ATTEMPTS, and was once SERVES (AGL-2714)\n *\n * It began as signup-page serves — `signupServed_` markers written when the\n * page asks `/api/lockdown-status?feature=signups`, the one server request\n * every rendering makes. That counts LOOKERS: crawlers, link previews, a\n * person reading the plan names and closing the tab. On 2026-09-09 it paged\n * \"Signups is down\" on an hour with six such hits and no account, while the\n * signup page rendered clean, the lockdown was off, all seven auth doors were\n * green and not one signup had been refused. One of the six was the\n * diagnostic load made while investigating the alert.\n *\n * At this platform's conversion rate a quiet hour and a broken door produce\n * the same reading, so no threshold on serves could separate them. The\n * quantity was wrong, not the number.\n *\n * `signupAttempted_` markers are the right one: written at the top of\n * `/api/orgs/create` before any outcome is known, so an attempt means\n * somebody authenticated, filled the form and asked for an org. Arrivals with\n * no account is then a statement about the door rather than about demand.\n *\n * Writing the marker BEFORE the outcome is what makes this stronger than the\n * refusal check beside it: `signupRefusals` counts what the route chose to\n * turn away, so a request that 500s, hangs, or dies between the limiter and\n * the write leaves no refusal at all — and leaves an attempt with no org,\n * which is exactly this verdict.\n *\n * Not GA4, for the reason it was never GA4: its API needs a service account\n * and an OAuth round trip from a public health route, its data lands with\n * hours of latency, and a monitor that depends on an analytics vendor is a\n * monitor that reports an outage every time that vendor has one.\n *\n * The denominator UNDER-counts on purpose and that is the safe direction: a\n * marker write is fire-and-forget and a lost one is simply not counted.\n * Under-counting can only make this check quieter, never louder — it cannot\n * invent attempts that did not happen, so it cannot invent an outage.\n *\n * Serves are still read and still reported, as CONTEXT. They cost nothing\n * extra and they answer the second question an on-call person asks: was\n * anybody even looking.\n *\n * Pure on purpose, like its siblings: the route counts, this decides.\n */\nexport interface SignupServedMarker {\n /** Signup-page serves merged into this minute bucket. */\n serves?: number\n /** When the most recent serve in this bucket happened. */\n servedAtMs?: number\n}\n\nexport interface SignupAttemptMarker {\n /** Org-creation attempts merged into this minute bucket. */\n attempts?: number\n /** When the most recent attempt in this bucket happened. */\n attemptedAtMs?: number\n}\n\nexport interface SignupDroughtCheck extends HealthCheck {\n /**\n * Signup pages served inside the trailing window — the denominator. A\n * COUNT only; the markers carry no visitor, no IP and no referrer. Null\n * when the query itself failed.\n */\n signupPagesServed: number | null\n /**\n * Org-creation attempts inside the same window — the denominator the\n * verdict is actually taken on (AGL-2714). Null when that query failed.\n */\n signupAttempts: number | null\n /** Accounts (orgs) actually created in the same window. The numerator. */\n orgCreations: number | null\n /** The trailing window all three numbers cover, so the body is self-describing. */\n windowMinutes: number\n /**\n * Attempts below which zero creations is treated as a quiet hour rather\n * than an outage. Named `minimumTraffic` still, because it is the same\n * question — how much has to have happened before zero means something —\n * and renaming it would break every monitor body that reads it.\n */\n minimumTraffic: number\n}\n\n/**\n * Trailing window the drought verdict covers. One hour, matching the sibling\n * checks so the three numbers on this endpoint are all read against the same\n * clock — and long enough that a single slow afternoon does not page anyone.\n */\nexport const SIGNUP_DROUGHT_WINDOW_MINUTES = 60\n\n/**\n * Attempts in the window below which zero accounts means nothing.\n *\n * Three, against attempts rather than the five it once meant against serves\n * (AGL-2714). The unit changed, so the number had to be re-argued rather than\n * carried over.\n *\n * Not one: a browser can lose a request, and a person can start the form and\n * abandon it between authenticating and the org write. Not ten either — at a\n * handful of signups a week, waiting for ten attempts to fail would mean\n * waiting days to notice a door that stopped opening, which is the failure\n * this check exists to catch inside an hour.\n *\n * Three distinct requests to create an org, none of which produced one and\n * none of which was refused, is a door that does not open. That reading has\n * no quiet-hour interpretation: nobody attempts three times by browsing.\n */\nexport const MIN_SIGNUP_TRAFFIC_FOR_DROUGHT = 3\n\nexport function signupDroughtHealth(\n markers: SignupServedMarker[] | null,\n attemptMarkers: SignupAttemptMarker[] | null,\n orgCreations: number | null,\n ms: number,\n minimumTraffic: number = MIN_SIGNUP_TRAFFIC_FOR_DROUGHT,\n windowMinutes: number = SIGNUP_DROUGHT_WINDOW_MINUTES,\n): SignupDroughtCheck {\n // Serves are context, not the verdict, so they are summed wherever they are\n // readable and reported as null when they are not — a missing context line\n // must not decide anything on its own.\n let signupPagesServed: number | null = null\n if (markers !== null) {\n signupPagesServed = 0\n for (const marker of markers) {\n // `Number(...) || 0` for the reason the sibling gives: a corrupt\n // Firestore field must not turn the total into NaN, which compares\n // false against every threshold and would report calm forever.\n signupPagesServed += Number(marker.serves) || 0\n }\n }\n // A failed ATTEMPT query is degraded, not ok — the same rule the siblings\n // follow. Without the denominator this check has no opinion at all, and a\n // check with no opinion must not spend it saying everything is fine.\n if (attemptMarkers === null) {\n return {\n ok: false,\n ms,\n code: 'traffic-unavailable',\n signupPagesServed,\n signupAttempts: null,\n orgCreations,\n windowMinutes,\n minimumTraffic,\n }\n }\n let signupAttempts = 0\n for (const marker of attemptMarkers) {\n signupAttempts += Number(marker.attempts) || 0\n }\n if (orgCreations === null) {\n return {\n ok: false,\n ms,\n code: 'count-unavailable',\n signupPagesServed,\n signupAttempts,\n orgCreations: null,\n windowMinutes,\n minimumTraffic,\n }\n }\n // The whole verdict: people ASKED for an org and NOT ONE of them got it.\n const drought = signupAttempts >= minimumTraffic && orgCreations === 0\n return {\n ok: !drought,\n ms,\n ...(drought ? { code: 'signup-drought' } : {}),\n signupPagesServed,\n signupAttempts,\n orgCreations,\n windowMinutes,\n minimumTraffic,\n }\n}\n\n/**\n * Rate-limiter degradation, reduced to a health verdict (AGL-1693).\n *\n * `consumeRateLimit` fails SOFT: when the Firestore counter is unreachable it\n * answers from a per-instance `Map` and flags `degraded`. AGL-1679 made that\n * findable after the fact — on recovery the instance writes one summary\n * document `rateLimits/degraded_{minuteBucket}` — but nothing reads it, so a\n * degraded window during the beta still passes unnoticed in real time. This\n * turns \"did any limiter fall back recently\" into the same 200/503 contract\n * the sibling checks speak, so the AGL-1502 uptime check + alert + email path\n * is the listener.\n *\n * **The window is the whole design.** Markers carry a 30-day `expiresAt`, so\n * \"does a marker exist\" would leave the endpoint red for a month after a\n * thirty-second blip — a recovered incident permanently paging is how a check\n * gets muted, and muted is worse than absent. That is the opposite of the\n * backups check, which stays red BY DESIGN because a missing restore point is\n * a condition that persists until someone fixes it (DISASTER_RECOVERY.md gap\n * 2). A degraded limiter window is an EVENT: it is over, and what is owed is\n * a notification, not a standing alarm. So the verdict covers a trailing\n * window and clears itself.\n *\n * Filtering on `lastAtMs` rather than the marker's id: the id is bucketed on\n * `firstAtMs`, so a long episode's document is stamped with the minute the\n * outage STARTED. An id-range window would miss exactly the longest episodes,\n * which are the ones worth waking up for.\n *\n * Pure on purpose, like `backupsHealth` and `signupsHealth`: the route\n * queries, this decides, the spec exercises every branch without a network.\n */\nexport interface RateLimitDegradationMarker {\n /** Calls that fell back to the per-instance cap. */\n calls?: number\n /** Instance-episodes merged into this minute bucket. */\n episodes?: number\n /** When the episode's last fallback happened. */\n lastAtMs?: number\n /** The failure code the episode ended on. */\n code?: string\n}\n\nexport interface RateLimitsCheck extends HealthCheck {\n /**\n * Fallback calls inside the trailing window. A COUNT only — the endpoint is\n * public, and limiter keys are hashed client IPs. Null when the query\n * itself failed.\n */\n degradedCalls: number | null\n /** Instance-episodes inside the window. 2+ means several instances saw it. */\n degradedEpisodes: number | null\n /** Minutes since the most recent fallback, so the body dates itself. */\n minutesSinceLast: number | null\n /** The trailing window the counts cover, so the body is self-describing. */\n windowMinutes: number\n /** The count above which this reports degraded. */\n threshold: number\n}\n\n/**\n * Trailing window the degradation counts cover.\n *\n * Bounded from below by the alert path, not by taste. The probe memoizes for\n * `HEALTH_PROBE_TTL_MS` (2 minutes), the slowest check on this door runs\n * every 15, and the alert policy wants a minute of sustained failure — so a\n * window shorter than ~18 minutes can go red and green again before that\n * check tells anyone, which is a check that reports nothing while looking\n * like it works. 30 leaves margin and still clears itself inside half an hour.\n */\nexport const RATE_LIMIT_DEGRADED_WINDOW_MINUTES = 30\n\n/**\n * Any fallback at all is worth an email. Unlike the signup alarm there is no\n * organic baseline to clear: a healthy deployment produces exactly zero of\n * these, and one is already the statement \"for some requests, the global cap\n * was not the cap\". Firestore retries transactions internally, so a failure\n * that reaches this code is not a single unlucky contention.\n */\nexport const MAX_DEGRADED_CALLS_PER_WINDOW = 0\n\nexport function rateLimitsHealth(\n markers: RateLimitDegradationMarker[] | null,\n ms: number,\n now: number = Date.now(),\n threshold: number = MAX_DEGRADED_CALLS_PER_WINDOW,\n windowMinutes: number = RATE_LIMIT_DEGRADED_WINDOW_MINUTES,\n): RateLimitsCheck {\n // A failed query is degraded, not ok — same rule as `signupsHealth`. An\n // alarm that cannot see the thing it watches must not report calm, and this\n // one reads the collection that just failed for the limiter.\n if (markers === null) {\n return {\n ok: false,\n ms,\n code: 'markers-unavailable',\n degradedCalls: null,\n degradedEpisodes: null,\n minutesSinceLast: null,\n windowMinutes,\n threshold,\n }\n }\n\n const cutoff = now - windowMinutes * 60_000\n // The window is re-applied here rather than trusted from the query, so the\n // rule lives in one spec-covered place and a marker whose `lastAtMs` is\n // missing cannot silently count as recent.\n const recent = markers.filter(\n (marker) =>\n typeof marker.lastAtMs === 'number' && marker.lastAtMs >= cutoff,\n )\n\n const degradedCalls = recent.reduce(\n (total, marker) => total + (Number(marker.calls) || 0),\n 0,\n )\n const degradedEpisodes = recent.reduce(\n (total, marker) => total + (Number(marker.episodes) || 0),\n 0,\n )\n const lastAtMs = recent.reduce<number | null>(\n (latest, marker) => Math.max(latest ?? -Infinity, marker.lastAtMs as number),\n null,\n )\n\n return {\n ok: degradedCalls <= threshold,\n ms,\n ...(degradedCalls <= threshold ? {} : { code: 'rate-limiter-degraded' }),\n degradedCalls,\n degradedEpisodes,\n minutesSinceLast:\n lastAtMs === null\n ? null\n : Math.round(((now - lastAtMs) / 60_000) * 10) / 10,\n windowMinutes,\n threshold,\n }\n}\n\n/**\n * Uncaught server-side errors, reduced to a health verdict (AGL-1921).\n *\n * ## The gap this closes\n *\n * Of the eleven alert policies in `aglyn-main`, not one could see a server\n * error rate. Every check is a LIVENESS probe on one URL: `/api/health` can be\n * green while `/api/billing/checkout` 500s for every paying customer, which is\n * the single most likely shape of a launch-day incident and the one nothing\n * could page on. Server errors lived only in the Vercel runtime log, which\n * retains ~60 minutes and drains nowhere (AGL-1799).\n *\n * The first pass gave both apps an `onRequestError` hook that forwards each\n * error to a `server-errors` log in `aglyn-main`. That closed the CAPTURE gap\n * and left the reading gap wide open, and the reading gap is the one this repo\n * keeps losing to — `/api/health/crons` was correct about a broken job for\n * fifty-one hours because nothing asked it. Measured 2026-08-24: the\n * production credential is refused `entries:list` on that log\n * (`403 Permission denied for all log views`), so no probe here can read what\n * the hook writes. The only reader that log can have is a GCP alert policy\n * created by hand.\n *\n * So the hook ALSO counts into a `rateLimits/serverError_{minute}` marker,\n * and this grades those counts into the same 200/503 contract the sibling\n * checks speak — which hands the signal to readers that already exist and\n * already run: the 15-minute GitHub uptime probe, the external keyword\n * monitors, and `docs.aglyn.com/status`.\n *\n * ## What it can and cannot see, exactly\n *\n * It counts what `onRequestError` counts: an uncaught throw in a render or a\n * route handler, in the nodejs runtime. It does NOT count an error that kills\n * the process before the hook runs, a platform-level 5xx that never reaches\n * our code (function timeout, OOM, cold-start 502), or anything thrown in the\n * edge runtime. A Vercel log drain sees all of those and remains the real fix.\n * This is the arm that ships without a purchase, and its blind spots are\n * written down rather than glossed — `docs/UPTIME_AND_SLA.md`.\n *\n * ## The third state is the one that matters here\n *\n * A monitoring path that cannot measure MUST NOT report a zero. `null` markers\n * — the query failed — is `errors-unavailable` and degraded, never \"0 errors\".\n * That is the same rule `rateLimitsHealth` and `billingWebhookHealth` follow,\n * and on this check it is the difference between \"nothing is wrong\" and \"we\n * have no idea\", which during a 5xx spike are the two readings furthest apart.\n *\n * ## It must be able to go GREEN\n *\n * A spike is an EVENT, not a condition (the AGL-1843 rule). Markers live seven\n * days, so \"does a marker exist\" would hold this red for a week after a\n * one-minute blip, and a check that stays red after recovery is a check that\n * gets muted. The verdict covers a trailing window and clears itself.\n *\n * Pure on purpose, like its siblings: the route queries, this decides, the\n * spec exercises every branch without a network.\n */\nexport interface ServerErrorMarker {\n /** Uncaught server errors merged into this minute bucket. */\n errors?: number\n /** Split by deployment — `{ 'console-web': n, 'tenant-web': n }`. */\n byService?: Record<string, number>\n /** When the most recent error in this bucket happened. */\n erroredAtMs?: number\n}\n\nexport interface ServerErrorsCheck extends HealthCheck {\n /**\n * Uncaught server errors inside the trailing window. A COUNT only — the\n * endpoint is public, and messages, stacks and route patterns have no\n * business in it (they are in the Logging entry, which is not public). Null\n * when the query itself failed, and null is NEVER zero.\n */\n serverErrors: number | null\n /**\n * Which deployment produced them. The first thing an incident needs, and it\n * is a deployment name rather than anything about a request. Null when the\n * query failed.\n */\n byService: Record<string, number> | null\n /** Minutes since the most recent error, so the body dates itself. */\n minutesSinceLast: number | null\n /** The trailing window the count covers, so the body is self-describing. */\n windowMinutes: number\n /** The count above which this reports degraded. */\n threshold: number\n}\n\n/**\n * Trailing window the error count covers.\n *\n * Bounded from below by the alert path, not by taste — the same arithmetic as\n * `RATE_LIMIT_DEGRADED_WINDOW_MINUTES`: the probe memoizes for 2 minutes, the\n * slowest external check runs every 15, and an alert policy wants a minute of\n * sustained failure, so a window under ~18 minutes can go red and green again\n * before that check tells anyone. 30 leaves margin and still clears itself\n * inside half an hour of the last error.\n */\nexport const SERVER_ERROR_WINDOW_MINUTES = 30\n\n/**\n * Errors tolerated in the window.\n *\n * NOT zero, unlike the rate-limiter and billing alarms, and the difference is\n * real rather than a matter of nerve. Those two watch controls that a healthy\n * deployment exercises exactly never. This one watches a count that a single\n * cold-start Firestore deadline, or one visitor's malformed request reaching a\n * route that throws instead of 400ing, can make non-zero on a perfectly good\n * day — and an alarm that pages on the first of those gets muted before the\n * one that matters arrives.\n *\n * Five over thirty minutes is chosen against the only two facts available.\n * Production serves single-digit orgs (2026-08), so the organic rate of\n * UNCAUGHT server errors is ~zero — the codebase answers its expected failures\n * with 4xx, and reaching this hook means nothing handled it. And the shape\n * being caught is \"a route is failing for real users\", which does not produce\n * six errors an hour; it produces six in a minute. There is deliberately no\n * pretence of calibration: the beta window exists to produce a baseline, and\n * `SERVER_ERROR_ALARM_MAX_ERRORS` moves this without a deploy when it does.\n */\nexport const MAX_SERVER_ERRORS_PER_WINDOW = 5\n\nexport function serverErrorsHealth(\n markers: ServerErrorMarker[] | null,\n ms: number,\n now: number = Date.now(),\n threshold: number = MAX_SERVER_ERRORS_PER_WINDOW,\n windowMinutes: number = SERVER_ERROR_WINDOW_MINUTES,\n): ServerErrorsCheck {\n // ⚠️ THE MEASURED ZERO. A failed query is degraded, never calm, and on this\n // check that is not a convention borrowed from its siblings — it is the\n // whole reason the check is trustworthy. \"We could not count the errors\"\n // and \"there were no errors\" are the same JSON if this branch is missing,\n // and the first is most likely precisely when the second is false.\n if (markers === null) {\n return {\n ok: false,\n ms,\n code: 'errors-unavailable',\n serverErrors: null,\n byService: null,\n minutesSinceLast: null,\n windowMinutes,\n threshold,\n }\n }\n\n const cutoff = now - windowMinutes * 60_000\n // The window is re-applied here rather than trusted from the query, so the\n // rule lives in one spec-covered place and a marker whose `erroredAtMs` is\n // missing cannot silently count as recent. `strictNullChecks` is off\n // repo-wide, so an absent field would otherwise compare as 0 and fall\n // outside every window forever — a marker that can never be counted.\n const recent = markers.filter(\n (marker) =>\n typeof marker.erroredAtMs === 'number' && marker.erroredAtMs >= cutoff,\n )\n\n const byService: Record<string, number> = {}\n let serverErrors = 0\n let lastAtMs: number | null = null\n for (const marker of recent) {\n // `Number(...) || 0` rather than a bare add: these come out of Firestore\n // and a corrupt field must not turn the total into NaN, which compares\n // false against the threshold and would report calm forever.\n serverErrors += Number(marker.errors) || 0\n for (const [service, count] of Object.entries(marker.byService ?? {})) {\n byService[service] = (byService[service] ?? 0) + (Number(count) || 0)\n }\n const at = marker.erroredAtMs as number\n if (lastAtMs === null || at > lastAtMs) lastAtMs = at\n }\n\n const over = serverErrors > threshold\n return {\n ok: !over,\n ms,\n ...(over ? { code: 'server-error-spike' } : {}),\n serverErrors,\n byService,\n minutesSinceLast:\n lastAtMs === null\n ? null\n : Math.max(0, Math.round(((now - lastAtMs) / 60_000) * 10) / 10),\n windowMinutes,\n threshold,\n }\n}\n\n/**\n * How long a health door may reuse its verdict (AGL-2947).\n *\n * Cut from one budget: an outage must reach a person within ten minutes. A\n * green verdict computed just before the onset keeps being served for this\n * long, and only then can a prober see red. The fastest prober on most doors\n * is UptimeRobot, every five minutes, which then confirms before it notifies.\n * So the worst case is this + 5 + that confirmation: at two minutes the budget\n * holds for any confirmation delay up to three. The five minutes every door\n * used before spent the whole ten minutes before a notification could leave.\n *\n * It still bounds what a public door can be made to cost: at most one\n * computation per instance per two minutes, and {@link memoizeWithTtl} shares\n * a computation among the callers that arrive while it runs.\n */\nexport const HEALTH_PROBE_TTL_MS = 2 * 60_000\n\n/**\n * Reuse a probe result for `ttlMs`, per instance.\n *\n * The endpoint is public and unauthenticated, so an unthrottled dependency\n * check is a free amplifier: anyone could turn a loop into database reads on\n * our account. This bounds the cost to one probe per instance per interval\n * however hard it is hit.\n *\n * Callers that arrive while a probe is running share it rather than starting\n * their own. The uptime check's regions, UptimeRobot and a HEAD twin routinely\n * land within the same second of an expired entry, and each used to pay for a\n * full probe.\n *\n * FAILURES ARE CACHED TOO, deliberately. A dependency that is down will be\n * down for the next caller as well, and re-probing per request turns an\n * outage into a stampede against the thing that is already failing. A probe\n * that THROWS is not cached: its callers all see the throw, and the next\n * caller probes again.\n */\nexport function memoizeWithTtl<T>(\n ttlMs: number,\n probe: () => Promise<T>,\n now: () => number = Date.now,\n): () => Promise<T> {\n let cached: { at: number; value: T } | null = null\n let inFlight: Promise<T> | null = null\n return async () => {\n const at = now()\n const elapsed = cached ? at - cached.at : Infinity\n // `elapsed >= 0` is not paranoia. A plain `elapsed < ttlMs` treats a\n // BACKWARDS clock as permanently fresh — the entry never expires and the\n // endpoint reports a stale verdict forever, which on a health check means\n // reporting healthy through an outage. Serverless instances outlive more\n // clock weirdness than a local run suggests.\n if (cached && elapsed >= 0 && elapsed < ttlMs) return cached.value\n if (inFlight) return inFlight\n const flight = (async () => {\n const value = await probe()\n cached = { at, value }\n return value\n })()\n inFlight = flight\n // Cleared once settled, and only if it is still this flight. Clearing\n // inside the flight would run BEFORE the assignment above when `probe`\n // throws synchronously, and leave a rejected promise in the slot forever.\n const clear = () => {\n if (inFlight === flight) inFlight = null\n }\n flight.then(clear, clear)\n return flight\n }\n}\n\n/**\n * The error beacon's own liveness, reduced to a health verdict (AGL-1923).\n *\n * ## The failure this exists for\n *\n * The `Client error beacon` alert policy is a log-match: it fires when an\n * entry APPEARS in `client-errors`. A policy of that shape can only report\n * presence, so if the thing writing the entries stops writing, the policy\n * goes quiet — and quiet is the reading it also gives when everything is\n * healthy. **A dead beacon is indistinguishable from zero errors**, and\n * `reportClientErrors` has three fail-soft paths that each end in a\n * `console.warn` to a log that retains an hour and drains nowhere\n * (AGL-1799). Error Reporting would show zero errors; the policy would stay\n * silent; both readings look exactly like a clean launch.\n *\n * ## Why this shape and not a metric-absence policy\n *\n * AGL-1923 proposed a log-based metric plus `conditionAbsent`. This is the\n * same guarantee with two properties that one lacks. It is PROVABLE — the\n * verdict is graded synchronously against the write's own status, so the\n * degraded branch can be induced and observed rather than waited out over an\n * absence window. And it needs nothing new to run it: the uptime probe is\n * what winds the dead-man's switch, so there is no cron that can itself stop\n * without anyone noticing (which would only move the problem one layer out).\n *\n * ## It must be able to go GREEN\n *\n * The condition is an EVENT, not a state. A failed write degrades; the next\n * successful write clears it, within one probe TTL — nothing latches and\n * there is no retention window to age out of. That is the AGL-1843 rule\n * applied before the fact: the clearing event is named, and it is \"the next\n * heartbeat that reaches Cloud Logging\". The durable marker AGL-2713 added is\n * evidence of a LANDING, never of a failure, so it can only ever shorten the\n * red — a stale one forgives nothing.\n *\n * Pure on purpose, like its siblings: the route writes, this decides, the\n * spec exercises every branch without a network.\n */\nexport interface BeaconCheck extends HealthCheck {\n /**\n * The log id the heartbeat targets, so the body names what to query during\n * an incident rather than making someone go and find it.\n */\n logId: string\n /** Which deployment's credential was exercised — console and tenant differ. */\n service: string\n /**\n * How many heartbeat writes this probe attempted. Two means the first one\n * missed, which is a fact a green row must still carry: a credential that\n * needs a second attempt every time is on its way to failing outright.\n */\n attempts: number\n /**\n * Minutes since a heartbeat last durably landed, or null when the answer\n * could not be established — which includes \"none ever has\". Null is what\n * makes a missed heartbeat unforgivable, so the two cases fold safely.\n */\n minutesSinceHeartbeat: number | null\n /** How long a missed heartbeat is tolerated before this door reds. */\n graceMinutes: number\n}\n\n/**\n * Codes that mean the heartbeat got no ANSWER, as opposed to a refusal\n * (AGL-2713).\n *\n * The distinction is the whole of the tolerance below, so it is named once\n * and asserted directly rather than being spelled inline at the branch.\n *\n * - `credential-unavailable` — the admin app is configured and its credential\n * simply did not mint a token on this attempt. On a cold lambda that is the\n * first outbound TLS of the instance's life, and it is the measured shape of\n * both 2026-09-09 blips.\n * - `transport-*` — the Logging write never came back inside its 4s deadline,\n * or the fetch layer failed. Same class: nothing was decided.\n *\n * Everything else is a DECISION and must never be tolerated. `http-401` and\n * `http-403` are a lost `logging.logEntries.create` grant, `http-429` is\n * quota, and `no-credential` is a deployment whose `FIREBASE_*` env is absent\n * — every one of them permanent until a person fixes it, and every one of\n * them a state in which browser errors are being collected by nothing. So is\n * `heartbeat-unavailable`, which means a writer documented never to throw\n * threw.\n */\nexport function isTransientBeaconCode(code: string | undefined): boolean {\n return code === 'credential-unavailable' || (code ?? '').startsWith('transport-')\n}\n\n/**\n * How a missed heartbeat is graded (AGL-2713).\n *\n * `landedAtMs` is the durable, cross-instance fact — the last time ANY\n * instance of this deployment put an entry in the log. It has to be durable:\n * the probe memo dies with the lambda, so an in-process counter is at one on\n * every cold start and would forgive nothing at all on the one shape this\n * exists for.\n */\nexport interface BeaconTolerance {\n /** Heartbeat writes this probe made. Defaults to the single-attempt shape. */\n attempts?: number\n /** When a heartbeat last durably landed; null when that cannot be shown. */\n landedAtMs?: number | null\n /** How long a miss is forgiven after the last landing. Zero forgives none. */\n graceMs?: number\n now?: number\n}\n\n/**\n * ## Tolerating ONE miss without becoming a door that cannot red (AGL-2713)\n *\n * `/api/health/error-beacon` on the console answered `503 no-credential` twice\n * on 2026-09-09 — 15:12 and 17:22 UTC, each shortly after new capacity from\n * the 15:08 deploy, each self-clearing within minutes, with the beacon code\n * path changed by one comment line since the previous release. Between them\n * they took the 15-minute uptime workflow red and opened four GCP alert\n * events, on a monitor whose entire value is that somebody believes it.\n *\n * The tolerance is deliberately NOT \"forgive a failure\". It is three\n * conditions that must all hold, and each one is what keeps the door able to\n * go red:\n *\n * 1. the code says nothing was decided ({@link isTransientBeaconCode});\n * 2. a heartbeat is PROVEN to have landed within `graceMs` — proof being a\n * durable marker a successful write left behind, so a deployment whose\n * credential never worked has nothing to forgive with and reds on its\n * first probe;\n * 3. that proof is not from the future, because a clock that ran backwards\n * must not mint an unbounded grace.\n *\n * Absence of proof reds. A marker store that cannot be read reds. That is the\n * same rule the `*-unavailable` arms of every sibling grader apply, pointed at\n * the evidence for calm rather than at the evidence for alarm.\n */\nexport function beaconHealth(\n write: { ok: boolean; code?: string } | null,\n logId: string,\n service: string,\n ms: number,\n tolerance: BeaconTolerance = {},\n): BeaconCheck {\n const {\n attempts = 1,\n landedAtMs = null,\n graceMs = 0,\n now = Date.now(),\n } = tolerance\n const sinceLanded = landedAtMs === null ? null : now - landedAtMs\n const base = {\n ms,\n logId,\n service,\n attempts,\n minutesSinceHeartbeat:\n sinceLanded === null\n ? null\n : Math.max(0, Math.round((sinceLanded / 60_000) * 10) / 10),\n graceMinutes: Math.round(graceMs / 60_000),\n }\n // A null result is degraded by contract, the same rule `signupsHealth` and\n // `rateLimitsHealth` follow: an alarm that cannot see the thing it watches\n // must not report calm. Here it is stronger than a convention — \"we could\n // not determine whether the beacon works\" IS the AGL-1923 condition.\n if (write === null) {\n return { ...base, ok: false, code: 'heartbeat-unavailable' }\n }\n if (write.ok) return { ...base, ok: true }\n const code = write.code ?? 'heartbeat-failed'\n if (\n isTransientBeaconCode(code) &&\n sinceLanded !== null &&\n sinceLanded >= 0 &&\n sinceLanded <= graceMs\n ) {\n // Green, and saying so out loud. A tolerance nobody can see on the board\n // is indistinguishable from a door that was widened until it stopped\n // reporting, which is the failure this whole change had to avoid being.\n return { ...base, ok: true, code: 'heartbeat-missed' }\n }\n return { ...base, ok: false, code }\n}\n\n/**\n * A tenant page render, reduced to a health verdict (AGL-2486).\n *\n * ## The gap this closes\n *\n * `marketing-home` (`aglyn.com/`) and `customer-site` (`demo.aglyn.app/`) are\n * the only two external checks that watched a REAL PAGE rather than a health\n * endpoint. Both sat at 0% from 2026-08-21, because bot protection answered\n * Google's checkers with a 429 checkpoint. The health-path firewall bypass\n * that recovered `tenant-health` and `beacon-heartbeat tenant` cannot reach\n * them: they fetch `/`, not `/api/health/*`.\n *\n * The alternative was allowlisting Google's 54 checker IPs — rejected because\n * an IP-valued bypass rule is one the drift checker cannot meaningfully\n * assert, and Google rotates the list. So the render moves to where the\n * bypass already is: an endpoint under `/api/health` that renders the page\n * server-side and grades the result.\n *\n * ## Why this is not \"200 because the route exists\"\n *\n * The caller runs the SAME loader the catch-all page route runs, against the\n * SAME host, and this grades what came back. A page that resolves no host,\n * 404s, redirects away, or composes an EMPTY node tree is a failure — and\n * those are the shapes an outage actually takes. A canary that cannot go red\n * for a broken page is worse than the dark check it replaces, because it\n * reports a confidence it has not earned.\n *\n * ## What is deliberately NOT asserted\n *\n * Never a string from the page. The marker is STRUCTURAL — a host resolved\n * and a non-empty composed node tree. Asserting copy would page whoever is\n * on call for an ordinary content edit, and this endpoint is public, so\n * customer content must not appear in its body either. `nodeCount` is a\n * count, which is enough to tell \"rendered nothing\" from \"rendered\" without\n * disclosing what was rendered.\n */\nexport type RenderOutcome =\n /** The loader returned props. `nodeCount` is the composed node total. */\n | { kind: 'rendered'; hostResolved: boolean; nodeCount: number }\n /** The loader chose the not-found branch. */\n | { kind: 'not-found' }\n /** The loader chose to redirect — a home page should never do this. */\n | { kind: 'redirect' }\n /** The loader threw, or could not be reached at all. */\n | { kind: 'unavailable' }\n /**\n * No target host is configured. A failure on purpose: \"we are watching\n * nothing\" must never read the same as \"the page is fine\".\n */\n | { kind: 'not-configured' }\n\nexport interface RenderCheck extends HealthCheck {\n /**\n * Composed nodes the page produced. A COUNT, never the nodes: this endpoint\n * is public and the nodes are the customer's page. Zero is the failure.\n */\n nodeCount: number\n /**\n * Which tenant host was rendered — a CONFIGURED host, never one derived\n * from the request, so no caller can choose what we render. Empty string\n * when nothing is configured.\n */\n host: string\n}\n\nexport function renderHealth(\n outcome: RenderOutcome,\n host: string,\n ms: number,\n): RenderCheck {\n const base = { ms, host }\n switch (outcome.kind) {\n case 'rendered':\n // Both halves are load-bearing. A host that resolved but composed\n // nothing is a blank page served with a 200 — the exact outage a\n // reachability ping calls healthy.\n if (!outcome.hostResolved) {\n return { ...base, ok: false, code: 'host-unresolved', nodeCount: outcome.nodeCount }\n }\n if (outcome.nodeCount <= 0) {\n return { ...base, ok: false, code: 'rendered-empty', nodeCount: 0 }\n }\n return { ...base, ok: true, nodeCount: outcome.nodeCount }\n case 'not-found':\n return { ...base, ok: false, code: 'not-found', nodeCount: 0 }\n case 'redirect':\n return { ...base, ok: false, code: 'redirected', nodeCount: 0 }\n case 'not-configured':\n return { ...base, ok: false, code: 'not-configured', nodeCount: 0 }\n case 'unavailable':\n default:\n // Same rule as `beaconHealth`: \"we could not determine whether the page\n // renders\" is a failure, never calm.\n return { ...base, ok: false, code: 'render-unavailable', nodeCount: 0 }\n }\n}\n\n/**\n * Billing webhook delivery, reduced to a health verdict (AGL-1924).\n *\n * ## The gap this closes\n *\n * Of the alert policies in `aglyn-main`, none watched billing. Sept 1 opens to\n * PAYING customers, and the billing path is the one place where a failure\n * nobody notices converts directly into lost revenue and a support incident\n * per affected customer. The webhook has a documented history of failing\n * exactly that way: AGL-1551 (every delivery rejected `400 Invalid signature`\n * for a week, behind a green \"Active\" badge), AGL-1560 (a secret roll 400ing\n * roughly half of deliveries), AGL-1552 (the replay window making every retry\n * unreceivable), AGL-1798 (`charge.refunded` never subscribed). Each is a\n * total or near-total failure of the billing event path, and not one of them\n * would have paged anyone.\n *\n * ## Why the denominator has to come from Stripe\n *\n * `stripeEvents` alone cannot answer this. `route.ts` returns 400 BEFORE\n * claiming the idempotency document, so a rejected delivery writes nothing —\n * an empty collection reads identically whether nothing happened or\n * everything was refused. That is precisely how the 2026-08-14 checklist tick\n * came out green while AGL-1551 was live. Stripe supplies what it TRIED to\n * deliver; we supply what we processed; the gap is the failure.\n *\n * ## Why this cannot false-page on a quiet night\n *\n * The verdict never keys on the ABSENCE of events. At beta volume a night\n * with zero deliveries is legitimate, and a naive freshness rule would page\n * on it — the AGL-1843 mistake in a new costume. `undelivered` is a count of\n * deliveries Stripe attempted AND failed, so a quiet window scores zero and\n * reads healthy for the right reason. `emitted` and `processed` are carried\n * for the human reading the incident and are DELIBERATELY not inputs to the\n * verdict: no floor for them exists yet, and inventing one before the beta\n * produces a baseline would be a threshold nobody could defend.\n *\n * ## Division of labour with the AGL-1906 audit\n *\n * `tools/scripts/lib/stripe-webhook-health.mjs` asserts the endpoint's full\n * subscribed-event list against `WEBHOOK_EVENTS`. That list is deliberately\n * NOT duplicated here — a second copy is the exact failure its own comment\n * warns about, and a subscription set is config that changes on deploys, not\n * a thing that changes minute to minute. This watches the two facts that DO\n * move continuously: whether the destination is still enabled, and whether\n * deliveries are landing.\n *\n * ## It must be able to go GREEN\n *\n * Everything here is a trailing window over live Stripe state. A destination\n * re-enabled, or a fixed handler whose retries then succeed, reads healthy on\n * the next probe. There is no marker to age out and nothing to latch\n * (AGL-1843): the clearing event is \"a window with no failed deliveries and\n * an enabled endpoint\".\n *\n * Pure on purpose, like its siblings: the route fetches, this decides, the\n * spec exercises every branch without a network.\n */\nexport interface BillingWebhookFacts {\n /** Our production destination, as Stripe currently holds it. */\n endpointStatus: 'enabled' | 'disabled' | 'missing'\n /** Events Stripe attempted and failed to deliver inside the window. */\n undelivered: number\n /** Events Stripe emitted inside the window. Reported, never a verdict input. */\n emitted: number\n /**\n * Events our handler claimed inside the window. Reported, never a verdict\n * input — and null when the Firestore arm could not run, which does NOT on\n * its own make the check red: Stripe already answered the question that\n * matters.\n */\n processed: number | null\n /**\n * Deliveries that answered 200 and moved NOTHING inside the window\n * (AGL-1954) — a required event type that produced neither a committed\n * effect nor a named deliberate skip. See `webhook-delivery.ts` for why\n * that is a defect by construction and a legitimate no-op is not one.\n *\n * Null when the Firestore arm could not run, and null is NOT red on its\n * own: Stripe already answered the question that matters, and a health\n * check that reds on its own read failure trains people to ignore it.\n */\n inert: number | null\n /**\n * Deliveries that only landed on a RETRY inside the window (AGL-2039).\n *\n * The arm every other count here is structurally blind to.\n * `undelivered` comes from `delivery_success=false`, which is a TERMINAL\n * filter over EVENTS: an event that 400s three times and then succeeds\n * reads back clean and is indistinguishable from one that succeeded\n * immediately. The Stripe Dashboard scores delivery ATTEMPTS and counted\n * all three — which is why AGL-1906 read 0% against the Dashboard's 30% and\n * both figures were correct. The three attempts it could not see were\n * AGL-1551.\n *\n * The webhook stamps `retriedAtMs` when the distance from `event.created`\n * to its own claim exceeds `RETRY_LAG_SECONDS`, so this is an aggregation\n * over a marker field rather than a scan of every claim in the window.\n *\n * Null when the Firestore arm could not run, and null is NOT red on its\n * own — same rule as `processed` and `inert`.\n */\n retried: number | null\n /**\n * Required event types the destination is NOT subscribed to (AGL-1948 /\n * AGL-1798). Empty is the healthy answer; null means the endpoint did not\n * report its subscriptions and the coverage question is unanswered.\n */\n unsubscribedEvents: readonly string[] | null\n /**\n * The CONNECT destination, as Stripe currently holds it (AGL-1948).\n *\n * A SECOND destination, and the platform one being perfectly healthy says\n * nothing about it: connected-account events are delivered only to a\n * destination created with `connect: true`, so its absence is invisible to\n * every count above. AGL-2122 found exactly that — `account.updated`\n * handled in two plugins, and no destination that could ever deliver it,\n * so every merchant's `stripeChargesEnabled` rotted silently.\n *\n * Null when the endpoint census itself could not be taken.\n */\n connectEndpoint: 'enabled' | 'disabled' | 'missing' | null\n /**\n * Required Connect events the Connect destination is NOT subscribed to.\n * Empty is healthy; null means there was no destination to ask, or it did\n * not state its subscriptions.\n */\n unsubscribedConnectEvents: readonly string[] | null\n}\n\nexport interface BillingWebhookCheck extends HealthCheck {\n endpointStatus: BillingWebhookFacts['endpointStatus'] | null\n undelivered: number | null\n emitted: number | null\n processed: number | null\n inert: number | null\n retried: number | null\n unsubscribedEvents: readonly string[] | null\n connectEndpoint: BillingWebhookFacts['connectEndpoint'] | null\n unsubscribedConnectEvents: readonly string[] | null\n /** The trailing window the counts cover, so the body is self-describing. */\n windowMinutes: number\n}\n\n/**\n * Trailing window the delivery counts cover.\n *\n * Bounded from below by the alert path, not by taste, exactly like\n * `RATE_LIMIT_DEGRADED_WINDOW_MINUTES`: the probe memoizes for 5 minutes, the\n * uptime check runs every 5, and the policy wants ~10 minutes of sustained\n * failure before it emails — so a window under ~20 minutes can go red and\n * green again before anyone is told. Bounded from ABOVE by Stripe's retry\n * schedule: an event that fails and then succeeds on retry stops counting, so\n * a window much longer than an hour would hold transient blips red past their\n * own recovery.\n */\nexport const WEBHOOK_FAILURE_WINDOW_MINUTES = 60\n\n/**\n * Failed deliveries tolerated in the window.\n *\n * Zero, and unlike the signup alarm there is no organic baseline to clear: a\n * healthy account produces exactly zero, and one is already the statement\n * \"Stripe tried to tell us something about money and could not\". Stripe\n * retries on its own, so a failure visible here is not one unlucky packet.\n */\nexport const MAX_FAILED_DELIVERIES_PER_WINDOW = 0\n\n/**\n * Inert deliveries tolerated in the window (AGL-1954).\n *\n * Zero — and unlike `processed`, the count this check reports but refuses to\n * gate on because no defensible floor exists before the beta produces a\n * baseline, this one's floor follows from the definition. A delivery is\n * counted inert only when a required event type produced neither a committed\n * effect nor a NAMED deliberate skip (`classifyWebhookDelivery`). No\n * legitimate traffic pattern generates one: a tenant shopper's subscription,\n * a marketplace refund, a `won` dispute nobody claimed all name their reason\n * and classify as `ignored`. So any at all is the statement \"Stripe told us\n * about money and nothing here moved\" — the same \"any at all\" rule the\n * rate-limiter degradation markers use (AGL-1679).\n */\nexport const MAX_INERT_DELIVERIES_PER_WINDOW = 0\n\n/**\n * Retried deliveries tolerated in the window (AGL-2039).\n *\n * Zero, and the reasoning is `MAX_FAILED_DELIVERIES_PER_WINDOW`'s: a delivery\n * counted here is one whose FIRST attempt did not get through, which is a\n * failed attempt that happened, not a statistical wobble. The healthy band\n * measured against the live account is 1.0–3.7 seconds against a 120-second\n * bar, so nothing organic sits anywhere near it.\n *\n * Cheap to be wrong in the safe direction, because this cannot latch: the\n * window is trailing and the marker is on the event's own claim, so a single\n * blip reds for at most one window and then clears itself — an event, not a\n * condition (the AGL-1843 rule). It is a parameter rather than a literal so\n * that if beta traffic turns out to produce organic retries, the knob is one\n * argument and not a rewrite.\n */\nexport const MAX_RETRIED_DELIVERIES_PER_WINDOW = 0\n\nexport function billingWebhookHealth(\n facts: BillingWebhookFacts | null,\n ms: number,\n windowMinutes: number = WEBHOOK_FAILURE_WINDOW_MINUTES,\n threshold: number = MAX_FAILED_DELIVERIES_PER_WINDOW,\n inertThreshold: number = MAX_INERT_DELIVERIES_PER_WINDOW,\n retriedThreshold: number = MAX_RETRIED_DELIVERIES_PER_WINDOW,\n): BillingWebhookCheck {\n // A null census is degraded, the same rule `signupsHealth` and\n // `rateLimitsHealth` follow: an alarm that cannot see the thing it watches\n // must not report calm. Here it is also the AGL-1906 lesson — `unknown` was\n // read as `healthy` on 2026-08-14 and the endpoint was 400ing every\n // delivery at the time.\n if (facts === null) {\n return {\n ok: false,\n ms,\n code: 'stripe-unavailable',\n endpointStatus: null,\n undelivered: null,\n emitted: null,\n processed: null,\n inert: null,\n retried: null,\n unsubscribedEvents: null,\n connectEndpoint: null,\n unsubscribedConnectEvents: null,\n windowMinutes,\n }\n }\n\n const base = {\n ms,\n endpointStatus: facts.endpointStatus,\n undelivered: facts.undelivered,\n emitted: facts.emitted,\n processed: facts.processed,\n inert: facts.inert,\n retried: facts.retried,\n unsubscribedEvents: facts.unsubscribedEvents,\n connectEndpoint: facts.connectEndpoint,\n unsubscribedConnectEvents: facts.unsubscribedConnectEvents,\n windowMinutes,\n }\n\n // Order matters: a missing or disabled destination explains any delivery\n // count that follows, and is the more actionable statement. An endpoint\n // deleted or switched off in the dashboard is TOTAL silent failure — Stripe\n // stops attempting, so `undelivered` would read zero and a delivery-only\n // rule would call it healthy.\n if (facts.endpointStatus === 'missing') {\n return { ...base, ok: false, code: 'endpoint-missing' }\n }\n if (facts.endpointStatus === 'disabled') {\n return { ...base, ok: false, code: 'endpoint-disabled' }\n }\n // A required event the destination does not carry (AGL-1948 / AGL-1798),\n // ABOVE the delivery counts because it explains a silence they cannot see.\n // An unsubscribed event produces no delivery to fail, no request to reject\n // and no inert handler either — Stripe simply stops sending, and every\n // count below reads a perfectly healthy zero for as long as it lasts. It\n // also outranks `deliveries-failing` on actionability: this one names the\n // exact event to re-add, and `npm run setup:stripe` fixes it in a minute.\n //\n // `null` — the endpoint did not state its subscriptions — is deliberately\n // NOT red. It is a question that went unanswered, not an answer, and the\n // check already has `stripe-unavailable` for a census it could not take.\n if (facts.unsubscribedEvents && facts.unsubscribedEvents.length > 0) {\n return { ...base, ok: false, code: 'events-unsubscribed' }\n }\n if (facts.undelivered > threshold) {\n return { ...base, ok: false, code: 'deliveries-failing' }\n }\n // THE 200-THAT-DID-NOTHING (AGL-1954), and it is LAST on purpose: every\n // code above explains why a handler would have had nothing to do, so\n // reporting inertness over one of them would name the symptom instead of\n // the cause. Reached only when Stripe holds an enabled destination\n // subscribed to everything we asked for and delivered successfully — at\n // which point \"a delivery moved nothing\" has no innocent explanation left.\n if (facts.inert !== null && facts.inert > inertThreshold) {\n return { ...base, ok: false, code: 'handlers-inert' }\n }\n /*==========================================\n * THE ATTEMPTS NOTHING ELSE HERE CAN COUNT (AGL-2039).\n *\n * `undelivered` above is `delivery_success=false`, a TERMINAL-state filter\n * over EVENTS. An event that 400s three times and then succeeds on the\n * fourth reads back `true`, so it is zero there and zero in `emitted`,\n * `processed` and `inert` too — the handler DID eventually run. Every\n * number on this check reports a healthy window while three real delivery\n * attempts failed. That is not a hypothetical: it is the exact reading\n * AGL-1906 produced, 0.00% against the Stripe Dashboard's 30%, and the\n * three attempts the Dashboard was counting were AGL-1551's.\n *\n * BELOW `deliveries-failing` because an event failing every attempt\n * explains an event that needed several, and below `handlers-inert`\n * because a delivery that landed late and then moved nothing is worse than\n * one that landed late and worked. ABOVE the Connect codes because this is\n * this hour's revenue path and those are a slower rot.\n *\n * Null is not red: the marker is ours, so an unread Firestore is an\n * unanswered question, and `stripe-unavailable` already covers a census we\n * could not take.\n *=========================================*/\n if (facts.retried !== null && facts.retried > retriedThreshold) {\n return { ...base, ok: false, code: 'deliveries-retried' }\n }\n /*==========================================\n * THE CONNECT DESTINATION (AGL-1948, closing AGL-2122's blind spot).\n *\n * A SECOND destination, and every code above is about the first one. The\n * platform destination can be enabled, fully subscribed, delivering and\n * acting — the whole check green — while connected-account events reach\n * nothing at all, because they are delivered only to a destination created\n * with `connect: true`. AGL-2122 measured exactly that against the live\n * account on 2026-08-18: one destination, correct URL, all ten events, and\n * no Connect destination in existence. `account.updated` was handled in two\n * plugins and could never be delivered, so `syncConnectAccountStatus` never\n * ran and every merchant's charge-eligibility flag rotted where it stood.\n *\n * LAST on purpose, and for a different reason than `handlers-inert` above:\n * not because the codes above explain it, but because they do not. It is an\n * independent destination whose failure is slower — merchant readiness\n * drifting out of date — while everything above is this hour's revenue. The\n * facts ride in the body either way, so the staff board shows the Connect\n * state even when a louder code takes the headline; only the single `code`\n * is exclusive.\n *\n * `missing` IS red. Before AGL-2122 that would have been wrong, because\n * nothing created the destination and a permanently-red check teaches\n * people to ignore the board. Now that setup-stripe creates it, its absence\n * is a real regression with a one-line remedy.\n *=========================================*/\n if (facts.connectEndpoint === 'missing') {\n return { ...base, ok: false, code: 'connect-endpoint-missing' }\n }\n if (facts.connectEndpoint === 'disabled') {\n return { ...base, ok: false, code: 'connect-endpoint-disabled' }\n }\n if (\n facts.unsubscribedConnectEvents &&\n facts.unsubscribedConnectEvents.length > 0\n ) {\n return { ...base, ok: false, code: 'connect-events-unsubscribed' }\n }\n return { ...base, ok: true }\n}\n\n/**\n * Is every interval we can SELL also an interval we can BILL? (AGL-1931)\n *\n * The metered item is what turns reported usage into money. It is attached\n * from an env-configured price id that is keyed by billing interval —\n * `STRIPE_PRICE_METERED` for monthly, `STRIPE_PRICE_METERED_YEARLY` for\n * annual — because Stripe forbids one subscription mixing\n * `recurring.interval`.\n *\n * When one of those ids is missing, `meteredPriceId(interval)` returns null\n * and every subscription-mutating path does the only safe thing: it attaches\n * nothing and carries on. Failing a checkout over it would be worse. But the\n * result is a subscription that accrues usage against a meter and carries no\n * item to bill it, and NOTHING about that is visible — the plan is right, the\n * entitlements are right, the invoice looks right, and the only trace is\n * revenue that never arrives.\n *\n * That is the defect shape this check exists to end. The absence of a price\n * was indistinguishable from \"correctly no overage\", so it hid behind a\n * `console.warn` on a server log nobody reads. Here it is a 503 on the same\n * probe the uptime check and alert email already watch.\n *\n * ## Why ASYMMETRY is the loudest code\n *\n * One interval configured and the other not means one cohort of customers is\n * billed for overage and the other silently is not — off the same meter, on\n * the same plans, with no screen anywhere showing the difference. It cannot\n * be a deliberate state: there is no product reason to meter monthly\n * subscribers and exempt annual ones.\n *\n * ## Why BOTH missing is still red, but only with Stripe configured\n *\n * Both unset with no `STRIPE_SECRET_KEY` is simply an unprovisioned\n * deployment — local dev, a fresh preview — where no money is at stake and\n * going red would train everyone to ignore this check. That is the exact\n * reasoning the checkout and subscription routes already apply to their\n * warning, and it is preserved here.\n *\n * But both unset while Stripe IS configured is a deployment that can take\n * money and cannot bill a single unit of overage on ANY interval. That is\n * the same revenue loss, merely total instead of partial, and the earlier\n * code treated it as calm because it only ever compared the two ids to each\n * other. Comparing them to Stripe's presence instead is what closes it.\n */\nexport interface MeteredPricingFacts {\n /** Whether this deployment holds a Stripe secret key at all. */\n stripeConfigured: boolean\n /** `STRIPE_PRICE_METERED` — the monthly metered price — is set. */\n monthly: boolean\n /** `STRIPE_PRICE_METERED_YEARLY` — the annual metered price — is set. */\n yearly: boolean\n}\n\nexport interface MeteredPricingCheck extends HealthCheck {\n stripeConfigured: boolean\n monthly: boolean\n yearly: boolean\n /**\n * The interval that would accrue unbilled usage, named so the body says\n * which cohort is affected without anyone re-deriving it from two booleans.\n */\n unbilledInterval: 'month' | 'year' | 'both' | null\n}\n\nexport function meteredPricingHealth(\n facts: MeteredPricingFacts,\n ms: number,\n): MeteredPricingCheck {\n const base = {\n ms,\n stripeConfigured: facts.stripeConfigured,\n monthly: facts.monthly,\n yearly: facts.yearly,\n }\n\n // Unprovisioned: no Stripe, no money, no alarm.\n if (!facts.stripeConfigured) {\n return { ...base, ok: true, code: 'stripe-unconfigured', unbilledInterval: null }\n }\n if (facts.monthly && facts.yearly) {\n return { ...base, ok: true, unbilledInterval: null }\n }\n if (!facts.monthly && !facts.yearly) {\n return { ...base, ok: false, code: 'metered-price-missing', unbilledInterval: 'both' }\n }\n // Exactly one. The AGL-1931 shape.\n return {\n ...base,\n ok: false,\n code: 'metered-price-asymmetric',\n unbilledInterval: facts.yearly ? 'month' : 'year',\n }\n}\n\n/*==========================================\n * SCHEDULED JOBS, WATCHED BY ABSENCE (AGL-1955).\n *\n * ## The failure this exists for\n *\n * `Cloud Scheduler job run failed (aglyn-main)` is a log-match on\n * `resource.type=\"cloud_scheduler_job\" AND severity >= ERROR`, and the\n * `scheduled-crons.yml` workflow goes red on a non-200. Both of those are\n * triggered BY A RUN. A job that is deleted, paused, or whose `- cron:` line\n * is edited away produces no attempt, therefore no error entry, therefore no\n * red workflow, therefore nothing. Quiet reads as healthy — the same shape\n * AGL-1923 closed for the error beacon, one subsystem over.\n *\n * What is downstream of these jobs is not cosmetic: `report-usage` is the\n * only writer that meters a closed month into Stripe, `run-erasures` is the\n * GDPR deletion runner, `audit-archive` moves and then DELETES audit rows,\n * and `plugin-jobs-beat` is the only thing that makes scheduled publishing\n * and booking-hold expiry happen at all. A silently unscheduled job means\n * customers are not billed, or data is not reaped, behind a green board.\n *\n * ## The two properties that make this work\n *\n * **It does not need the job to be alive to fire.** That is the whole bug.\n * The verdict is computed by the READER — `/api/health/crons`, wound by the\n * uptime probe and by any staff member opening the Health page — against\n * marks the jobs left behind. Nothing here is on a schedule that could\n * itself stop; that would only move the problem one layer out, which is the\n * AGL-1923 argument for a graded switch over a `conditionAbsent` policy.\n *\n * **It cannot false-alarm on a legitimately idle period.** The mark is\n * stamped by the INVOCATION, not by the work: a `finish-domain-attachments`\n * run with no pending host still beats, and a quiet week for\n * `reap-plugin-artifacts` still beats. And the expected time is computed\n * from the job's own cron expression rather than from a fixed interval, so\n * `usage-email` — hourly, but only on the 1st and 2nd of the month — is\n * green for the twenty-nine days it is deliberately not running. A fixed\n * \"expect one every hour\" rule would have paged on the 3rd, every month.\n *\n * ## Why an output age rule was not enough on its own\n *\n * AGL-1843's `exportsHealth` watches the weekly export by the age of what it\n * PRODUCES, and that is the better rule where an output exists — it catches\n * \"ran and failed\" as well as \"never ran\". Most of these jobs have no such\n * output. `backfill-scope` is detect-only and deliberately writes nothing.\n * `usage-alerts` produces a notification only when somebody is over budget.\n * `reap-plugin-artifacts` deletes nothing in a normal week. For those, the\n * only honest thing whose age moves on every run is a mark the run itself\n * leaves — so that is what this reads. `exportsHealth` stays where it is:\n * the two are complementary, and `firestore-export` is watched by both.\n *\n * ## It must be able to go GREEN\n *\n * Nothing latches. A job that missed a fire and then runs again stamps a\n * fresh mark and reads healthy on the next probe — the AGL-1843 rule, whose\n * clearing event here is named and is \"the next invocation of that job\".\n *=========================================*/\n\n/** Where the marks live. One document per job id, written by the job. */\nexport const CRON_BEAT_COLLECTION = 'platformCronBeats'\n\n/**\n * The document that records when this deployment STARTED watching.\n *\n * Without it a job that has never reported cannot be told apart from a job\n * that has stopped reporting, and every board would come up red on the day\n * the feature deploys — for `usage-email`, red until the 1st of the next\n * month. The rule this enables is stated once, in `cronJobsHealth`: a job\n * with no mark is only silent once a scheduled fire time has passed SINCE\n * we started watching.\n *\n * Not a reserved id: Firestore reserves `__.*__`, and this is a plain slug\n * that no job id can collide with (job ids never contain the word `watch`,\n * and the wiring spec asserts the inventory's ids against the workflow).\n */\nexport const CRON_BEAT_WATCH_DOC = 'watch-window'\n\n/**\n * Where a degraded verdict is recorded, so a red window survives it.\n *\n * The response body names the late job, but only to whoever is holding it,\n * and the uptime probe reads the status and discards the body. Vercel's\n * runtime log keeps a `console.error` for about an hour, and the Cloud\n * Logging drain deliberately does not forward one — `isServerErrorEntry`\n * drops `level: 'error'`, because that is every fail-soft line the beacons\n * write and forwarding it is what a self-feeding bill is made of. So the\n * only sink that outlives the window is the one the endpoint already holds\n * a handle to.\n *\n * Written by the READER, like the verdict itself. One document, replaced on\n * each degraded probe, holding the failing rows and when the window opened.\n * Same collision argument as the watch document: a plain slug, and no job id\n * contains `degraded`.\n */\nexport const CRON_BEAT_DEGRADED_DOC = 'last-degraded'\n\n/**\n * Every job's latest mark in one document, `{ beats: { [jobId]: atMs } }`.\n *\n * The reader's fast path (AGL-2946). Listing the collection costs one read per\n * job on every uncached probe — ~23 reads, several hundred times a day, on a\n * project that pays for reads past the free tier — to answer a question this\n * one document answers.\n *\n * A COPY, never the authority. `writeCronBeat` writes it second, so a job\n * whose own mark landed can still be missing or stale here, and the reader\n * confirms any job this document calls late against that job's own mark\n * before reporting it. Stale costs a read; it never costs a false red.\n *\n * Same collision argument as the other two: a plain slug no job id equals.\n */\nexport const CRON_BEAT_SUMMARY_DOC = 'summary'\n\n/** The reserved documents in `CRON_BEAT_COLLECTION` that are not job marks. */\nexport const CRON_BEAT_RESERVED_DOCS: ReadonlySet<string> = new Set([\n CRON_BEAT_WATCH_DOC,\n CRON_BEAT_DEGRADED_DOC,\n CRON_BEAT_SUMMARY_DOC,\n])\n\n/**\n * How long a gap may be before the next degraded probe counts as a NEW\n * window rather than a continuation of the one on file.\n *\n * Three probe TTLs. Long enough that an instance answering intermittently\n * does not split one outage into a dozen, short enough that yesterday's\n * window is never mistaken for today's. The comparison is against the\n * failing set as well, so a different set always opens a new window.\n */\nexport const CRON_DEGRADED_WINDOW_CONTINUITY_MS = 15 * 60_000\n\n/** Who fires the job. The two are operated, and fail, differently. */\nexport type CronRunner = 'github-actions' | 'cloud-scheduler'\n\nexport interface ScheduledJob {\n /** Stable id. The Firestore document id, and the health check's name. */\n id: string\n label: string\n /**\n * Five-field cron, UTC. The SAME string the scheduler holds — the wiring\n * spec asserts each one against its runner's source of truth\n * (`.github/workflows/scheduled-crons.yml` for `github-actions`,\n * `cloud/functions/src/index.ts` for `cloud-scheduler`), so an edit there\n * that is not made here fails the build rather than quietly changing what\n * \"on time\" means.\n */\n cron: string\n runner: CronRunner\n /** What the scheduler actually invokes, for the operator reading a red row. */\n target: string\n /**\n * How late a run may be before the row goes red.\n *\n * Generous on purpose, and the generosity is not padding. GitHub only runs\n * scheduled workflows from the default branch and delays them under load —\n * routinely by minutes, occasionally by an hour. The question this check\n * answers is \"is this job still scheduled at all\", not \"did it start on the\n * second\"; a grace that reds on ordinary lateness is alert fatigue, and an\n * alert people learn to ignore is the failure this issue is about wearing\n * a different hat.\n *\n * SO THE GRACE IS A PROPERTY OF THE RUNNER, not of the job (AGL-1617). The\n * ninety minutes the two frequent sweeps used to carry was GitHub's drift\n * budget, and it was bought with detection: on a fifteen-minute schedule it\n * takes SIX consecutive missed fires before anything is said. Moving those\n * two onto Cloud Scheduler — which is punctual, as `plugin-jobs-beat` has\n * been demonstrating at `every 1 minutes` in the same payload the whole\n * time — is what let the bar go back UP, to forty-five minutes and three\n * missed fires. That direction is the point: widening a grace to make a\n * monitor agree with a runner that drops 60% of its triggers is not a fix,\n * it is deciding not to look.\n *\n * IT IS A FLOOR, NOT THE EXACT BAR. `cronJobsHealth` compares the mark\n * against the last fire that is ALREADY this old, so where the threshold\n * lands depends on the clock's phase against the schedule: forty-five\n * minutes on a fifteen-minute cron reds somewhere between 45 and 60 minutes\n * of silence, six hours on a daily one between 6h and 30h. Quote the range,\n * not the number, when reasoning about how fast a dead job is found — and\n * never write a test that only holds at one phase.\n */\n graceMinutes: number\n /** What stops happening when this job stops. Rendered on the board. */\n drives: string\n}\n\n/**\n * THE INVENTORY.\n *\n * Five GitHub Actions schedules (`.github/workflows/scheduled-crons.yml`) —\n * the weekly jobs, for which hours of drift cost nothing — and the rows\n * driven by Cloud Scheduler out of\n * `cloud/functions/src/index.ts`: `pluginJobsBeat` and `consoleAiJobsBeat`\n * (every minute), the routes the `consoleFastCrons` job carries every\n * fifteen (AGL-1617), and one `consoleDailyCron` export per daily job.\n *\n * `scheduled-crons-wiring.spec.ts` holds BOTH runners against their source —\n * the workflow for the `github-actions` rows, the functions file for the\n * `cloud-scheduler` ones — in both directions. An inventory row for a job\n * nobody schedules and a scheduled job nobody watches are the same bug seen\n * from opposite ends, and both fail the build.\n *\n * `report-usage` appears TWICE and is two jobs, not one. The 02:00 run\n * rolls up the closed month and is the only run that ever reaches Stripe;\n * the 07:00 `?month=current` run writes the in-progress figure every usage\n * budget reads (AGL-2219). Either can stop without the other, and folding\n * them into one row would hide exactly that.\n */\n/*\n * ⚠️ GRACE IS A PROPERTY OF THE RUNNER, not of the job.\n *\n * The five daily console jobs carried a SIX-HOUR grace because GitHub\n * Actions dispatches scheduled workflows on a best-effort basis and routinely\n * drifted by an hour or two. That grace bought tolerance and paid for it in\n * blindness: on 2026-08-27 GitHub dropped the whole day, and three of these\n * were thirty hours silent before anything said so.\n *\n * They run on Cloud Scheduler now, which has been punctual to the minute\n * since AGL-1617, and each function's own ceiling is 540 seconds. Ninety\n * minutes is therefore ten times the longest a healthy run can take and still\n * catches a dead job inside the working day, instead of most of a day later.\n */\nexport const SCHEDULED_JOBS: readonly ScheduledJob[] = [\n {\n id: 'report-usage',\n label: 'Metered usage rollup (closed month)',\n cron: '0 2 * * *',\n runner: 'cloud-scheduler',\n target: '/api/billing/report-usage',\n graceMinutes: 90,\n drives:\n 'The only run that meters a closed month into Stripe. If it stops, customers are not billed for what they used.',\n },\n {\n id: 'audit-archive',\n label: 'Audit archive',\n cron: '0 3 * * *',\n runner: 'cloud-scheduler',\n target: '/api/admin/audit-archive',\n graceMinutes: 90,\n drives:\n 'Moves adminAudit rows past the 90-day window into Storage and deletes them from Firestore. If it stops, the retention promise is not being kept.',\n },\n {\n id: 'run-erasures',\n label: 'GDPR erasure runner',\n cron: '0 4 * * *',\n runner: 'cloud-scheduler',\n target: '/api/admin/run-erasures',\n graceMinutes: 90,\n drives:\n 'Executes accepted erasure requests. If it stops, personal data a customer asked us to delete is still here, and the clock on that request is still running.',\n },\n {\n id: 'report-usage-current',\n label: 'Metered usage rollup (current month)',\n cron: '0 7 * * *',\n runner: 'cloud-scheduler',\n target: '/api/billing/report-usage?month=current',\n graceMinutes: 90,\n drives:\n 'Writes the in-progress month figure (AGL-2219). If it stops, every usage budget on the platform is structurally unable to fire and the Billing card stops totalling.',\n },\n {\n id: 'usage-alerts',\n label: 'Usage budget alerts',\n cron: '0 8 * * *',\n runner: 'cloud-scheduler',\n target: '/api/billing/usage-alerts',\n graceMinutes: 90,\n drives:\n 'Evaluates usage budgets and notifies. If it stops, an org sails past its budget with no warning and finds out on the invoice.',\n },\n {\n id: 'reap-sending-domains',\n label: 'Orphaned sending-domain reaper',\n // An hour after `run-erasures`, which is what produces most of its work:\n // an erasure records what it could not release rather than waiting on a\n // vendor, and this is the only thing that settles that debt.\n cron: '0 5 * * *',\n runner: 'cloud-scheduler',\n target: '/api/admin/reap-sending-domains',\n graceMinutes: 90,\n drives:\n 'Releases the provider domain object and the zone records of every sending domain whose site or workspace is gone. If it stops, each deleted site keeps one of a bounded number of provider domain slots forever — until the ceiling is reached and a site that asks for a domain of its own is refused one, which leaves it on the shared pool rather than stopping its mail — and leaves a live DKIM key in our zone under a label a future site can claim and inherit a stranger’s signature from.',\n },\n {\n id: 'reap-unverified-orgs',\n label: 'Unverified signup reaper',\n // 06:00, after `run-erasures` and the sending-domain sweep and before the\n // billing pair. It calls `eraseOrg` itself, so it wants the erasure runner\n // to have already finished with whatever it was holding.\n cron: '0 6 * * *',\n runner: 'cloud-scheduler',\n target: '/api/admin/reap-unverified-orgs?dryRun=0',\n graceMinutes: 90,\n drives:\n 'Erases workspaces whose sole owner never confirmed an email address, releasing the workspace address they took, and promotes the held address of every owner who has since verified. If it stops, a name claimed with a throwaway inbox is held until its reservation expires — and, worse, a real customer who verified keeps a pending address that becomes claimable by anyone on day twenty-one.',\n },\n {\n id: 'ai-insights-digest',\n label: 'Weekly insights',\n // 06:00 and 14:00 every day (AGL-2915): Monday's first run makes the\n // week's digests and every run after it delivers the ones the AI jobs beat\n // has written. Served by the AI plugin.\n cron: '0 6,14 * * *',\n runner: 'cloud-scheduler',\n target: '/api/admin/ai-insights-digest',\n graceMinutes: 90,\n drives:\n 'Each Monday, makes the weekly insights people asked for, and delivers them once they are written. If it stops, nobody who asked is sent their insights, and a digest parked for credits is never given up.',\n },\n {\n id: 'usage-email',\n label: 'Monthly usage summaries',\n // Hourly across the FIRST TWO DAYS only (AGL-2409). The idle-period case\n // this whole check had to get right: for twenty-nine days a month this\n // job is correctly doing nothing, and must read green while it does. On\n // Cloud Scheduler since GitHub fired the hourly entry twice in the first\n // ten hours of 2026-10-01 (AGL-3439); ninety minutes is the dailies'\n // grace, one missed fire plus half an hour.\n cron: '0 * 1-2 * *',\n runner: 'cloud-scheduler',\n target: '/api/billing/usage-email',\n graceMinutes: 90,\n drives:\n \"Mails each org last month's usage summary, chunked across 48 hourly windows. If it stops, the month's summaries are simply never sent.\",\n },\n {\n id: 'crm-task-reminders',\n label: 'CRM task reminders',\n // Hourly, all month (AGL-2659), on Cloud Scheduler since GitHub delivered\n // the hourly entry about once every six hours and left reminders that\n // late. Ninety minutes is the dailies' grace: one missed fire plus half\n // an hour, and ten times the 540s a run may take.\n cron: '0 * * * *',\n runner: 'cloud-scheduler',\n target: '/api/crm/task-reminders',\n graceMinutes: 90,\n drives:\n 'Sends each CRM task’s reminder at its own time — a console notification and an email to the assignee (AGL-2659). If it stops, every reminder a person set in the task drawer is silently late, and nothing but the morning digest says a task was due.',\n },\n {\n id: 'risk-notice-digests',\n label: 'Risk notice digests',\n // Hourly at quarter past (AGL-3368); the dailies' ninety-minute grace.\n cron: '15 * * * *',\n runner: 'cloud-scheduler',\n target: '/api/risk-notices/digests',\n graceMinutes: 90,\n drives:\n 'Sends the one summary a workspace gets when more items were held or flagged in an hour than are sent one by one (AGL-3368). If it stops, a workspace whose attack ended hears nothing about the items folded into its digest until its next notice.',\n },\n {\n id: 'web-risk-recheck',\n label: 'Link re-check (Web Risk)',\n // Daily on Cloud Scheduler (AGL-3451); the dailies' ninety-minute grace.\n cron: '30 9 * * *',\n runner: 'cloud-scheduler',\n target: '/api/admin/web-risk-recheck',\n graceMinutes: 90,\n drives:\n 'Looks up every live page’s outside links against Google Web Risk again, and holds a page whose link has been listed since it went live; a new workspace’s page also gets its security hold (AGL-3451, AGL-3450). If it stops, a page that linked to a harvester before the harvester was listed keeps serving until somebody publishes it again or a render after the cache expires catches it.',\n },\n {\n id: 'firestore-export',\n label: 'Weekly Firestore export',\n cron: '0 5 * * 1',\n runner: 'github-actions',\n target: '/api/admin/firestore-export',\n graceMinutes: 1440,\n drives:\n 'Starts the independent GCS export — the restore point that exists because the managed backups kept flipping to NOT_AVAILABLE (AGL-1843). Also watched by output age on /api/health/backups.',\n },\n {\n id: 'reap-plugin-artifacts',\n label: 'Plugin artifact reaper',\n cron: '30 5 * * 1',\n runner: 'github-actions',\n target: '/api/admin/reap-plugin-artifacts',\n graceMinutes: 1440,\n drives:\n 'Deletes orphaned plugin bundles from the artifacts bucket. If it stops, storage grows without bound and nothing says so.',\n },\n {\n id: 'reverify-plugin-versions',\n label: 'Plugin verdict re-verification',\n cron: '0 6 * * 1',\n runner: 'github-actions',\n target: '/api/admin/reverify-plugin-versions',\n graceMinutes: 1440,\n drives:\n 'Re-checks stored plugin verdicts against the current verifier (AGL-1086). If it stops, a regression on a live version is never noticed.',\n },\n {\n id: 'reverify-sso-domains',\n label: 'SSO domain re-verification',\n // Mondays 07:30 UTC, after the rest of the weekly block. Weekly is the\n // right cadence because the threat needs a domain to lapse, clear a\n // registrar grace period and be re-bought — months, not days — and a\n // daily sweep would multiply DNS traffic by seven to learn nothing sooner.\n cron: '30 7 * * 1',\n runner: 'github-actions',\n target: '/api/admin/reverify-sso-domains',\n graceMinutes: 1440,\n drives:\n 'Re-checks that every live SSO domain still publishes its DNS ownership token (AGL-1210). If it stops, a domain that changed hands keeps routing another company sign-ins and nothing notices. It reports only — it never revokes, because a revoke logs out an entire enterprise.',\n },\n {\n id: 'backfill-scope',\n label: 'Scope-drift detection',\n cron: '30 6 * * 1',\n runner: 'github-actions',\n target: '/api/admin/backfill-scope',\n graceMinutes: 1440,\n drives:\n 'Reports documents with no visibleTo — invisible to every site-scoped read (AGL-1478). Detect-only by construction, so it has no output whose age could stand in for this mark.',\n },\n {\n id: 'campaigns-process-scheduled',\n label: 'Scheduled email campaigns',\n cron: '*/15 * * * *',\n // MOVED OFF GITHUB ACTIONS (AGL-1617), with the row below it. See the\n // grace note on `graceMinutes` for why that let the bar go UP.\n runner: 'cloud-scheduler',\n target: 'consoleFastCrons → console /api/campaigns/process-scheduled',\n graceMinutes: 45,\n drives:\n 'Claims and sends due campaigns. If it stops, a campaign the composer showed as Scheduled sits there and never sends — the AGL-2134 shape, which is sold on /product/marketing.',\n },\n {\n id: 'lists-materialize',\n label: 'Dynamic email lists',\n // Shares the `consoleFastCrons` job with the two rows around it — one\n // Cloud Scheduler job driving three routes, which is why adding this cost\n // no scheduler quota.\n cron: '*/15 * * * *',\n runner: 'cloud-scheduler',\n target: 'consoleFastCrons \\u2192 console /api/lists/materialize',\n graceMinutes: 45,\n drives:\n 'Re-evaluates every dynamic list against its rule. If it stops, a list keeps whatever membership it last had, and a campaign sent to it reaches an audience that silently stopped tracking the rule the merchant wrote — which looks exactly like a rule that matched nobody new.',\n },\n {\n id: 'finish-domain-attachments',\n label: 'Custom domain re-check',\n // Every fifteen minutes rather than twenty since AGL-1617: it shares one\n // Cloud Scheduler job with the campaign processor, and AGL-2010's argument\n // was always that a customer is sitting there waiting for their site.\n cron: '*/15 * * * *',\n runner: 'cloud-scheduler',\n target: 'consoleFastCrons → console /api/admin/finish-domain-attachments',\n graceMinutes: 45,\n drives:\n 'Re-checks pending custom domains after the certificate or DNS settles (AGL-2010). If it stops, a correctly-configured domain stays dark until a human presses Re-attach.',\n },\n {\n id: 'provision-sending-domains',\n label: 'Sending domain provisioning',\n // The fourth route on the `consoleFastCrons` job, sharing its schedule\n // with the three rows above. It is a console route rather than a job on\n // the platform beat because issuing a DKIM key needs a full-access\n // provider credential the tenant runtime must never hold.\n cron: '*/15 * * * *',\n runner: 'cloud-scheduler',\n target: 'consoleFastCrons → console /api/admin/provision-sending-domains',\n graceMinutes: 45,\n drives:\n 'Turns a site’s sending-domain claim into a DKIM key and the DNS records that carry it. If it stops, every site that has ASKED for a domain of its own waits on a claim with no records to publish. Their mail keeps leaving on the shared pool throughout, which makes this a stall in reputation isolation rather than stopped mail — and it is invisible to the merchant, who sees only a domain that never finishes.',\n },\n {\n id: 'drain-publish-outbox',\n label: 'Publish outbox drain',\n // The fifth route on the `consoleFastCrons` job. Fifteen minutes is the\n // bound on how long a publish whose tab went away can stay invisible,\n // and it is the schedule this shares rather than one chosen for it —\n // buying a tighter one would mean a second Cloud Scheduler job against a\n // free-tier allowance of three.\n cron: '*/15 * * * *',\n runner: 'cloud-scheduler',\n target: 'consoleFastCrons \\u2192 console /api/admin/drain-publish-outbox',\n graceMinutes: 45,\n drives:\n 'Fires the cache-drop announce for publishes whose tab closed before it landed (AGL-2575). Publishing is a client Firestore write, so the announce is a fetch from the browser and a closed tab strands it; this is the only thing that finishes one. If it stops, a stranded publish is invisible on the live site for the full hour-long document TTL, and the pending entry that records it is never read by anything.',\n },\n {\n id: 'plugin-console-crons',\n label: 'Plugin console jobs',\n // The sixth route on the `consoleFastCrons` job (AGL-2981). One route for\n // every job a plugin declares on `plugin-console-crons`, so a plugin's\n // job needs no route of its own in this list; each declared job has its\n // own row beside this one, added by `/api/health/crons` from the\n // declarations. This row answers the question under all of them: is the\n // route being posted at all.\n cron: '*/15 * * * *',\n runner: 'cloud-scheduler',\n target: 'consoleFastCrons → console /api/admin/plugin-crons',\n graceMinutes: 45,\n drives:\n 'Runs every console job a plugin declares, each with its own row on this board. If it stops, every one of those jobs stops with it — a plugin’s scheduled sends and syncs among them — and each of their rows goes silent too.',\n },\n {\n id: 'consent-group-changes',\n label: 'Consent group changes',\n // The seventh route on the `consoleFastCrons` job (AGL-3320). The editor's\n // progress panel works a change while it is open; this finishes every one\n // whose panel was closed, so fifteen minutes is how long an abandoned\n // change can sit between steps.\n cron: '*/15 * * * *',\n runner: 'cloud-scheduler',\n target: 'consoleFastCrons \\u2192 console /api/admin/consent-group-changes',\n graceMinutes: 45,\n drives:\n 'Finishes every consent group change an admin started and then left: the refusals carried between separating sites, the new declaration, the re-homed records and the delayed sweep (AGL-3320). If it stops, a change whose editor was closed stays where it stopped — before the flip the old groups stay in force, after it the records keyed by the old groups wait to move — and no further change can start until it finishes.',\n },\n {\n id: 'operator-alerts',\n label: 'Operator alerts',\n // The eighth route on the `consoleFastCrons` job (AGL-3377). It asks\n // every health endpoint so a degraded one alerts the operator without an\n // external monitor, and it carries the support SLA sweep and the daily\n // operator digest.\n cron: '*/15 * * * *',\n runner: 'cloud-scheduler',\n target: 'consoleFastCrons \\u2192 console /api/admin/operator-alerts/tick',\n graceMinutes: 45,\n drives:\n 'Asks every health check on the install so a degraded one emails the operator even with no uptime monitor watching; flags support tickets past their response time; sends the daily operator digest (AGL-3377). If it stops, a check that goes red is only noticed by whoever reads it, overdue tickets say nothing, and digest alerts pile up unsent — while every immediate alert still goes out.',\n },\n {\n id: 'security-holds',\n label: 'Automatic security holds',\n // The ninth route on the `consoleFastCrons` job (AGL-3450): the page\n // screen records a young workspace's hold wherever it runs, and this is\n // what places it.\n cron: '*/15 * * * *',\n runner: 'cloud-scheduler',\n target: 'consoleFastCrons \\u2192 console /api/admin/security-holds',\n graceMinutes: 45,\n drives:\n 'Places the security hold the page screen asks for when it holds a phishing page from a workspace in its first two weeks: the workspace, the site (as a takedown) and the publishing account are locked as security, and staff are alerted (AGL-3450). If it stops, the page itself stays held, but the account that published it keeps working — it can publish the next page and send mail — until somebody locks it by hand.',\n },\n {\n id: 'ai-jobs-beat',\n label: 'AI jobs beat',\n // Its own Cloud Scheduler job, every minute (AGL-3026): the console route\n // runs steps for most of five minutes at a time, and a page waits a beat\n // between sections. The grace is the other every-minute job's, below.\n cron: '* * * * *',\n runner: 'cloud-scheduler',\n target: 'consoleAiJobsBeat → console /api/admin/ai-jobs-beat',\n graceMinutes: 30,\n drives:\n 'Runs the AI generation steps the doors leave queued: every plan, every page pass, the passes of a site scaffold, and any step an inline door’s budget handed back (AGL-3026). It runs on the console because only the console holds the AI provider key. If it stops, a generation job that plans never gets its plan and a page is never built — the job sits queued, spending nothing, and never finishes.',\n },\n {\n id: 'plugin-jobs-beat',\n label: 'Plugin job beat',\n // Cloud Scheduler says `every 1 minutes`; the equivalent five-field\n // expression is what this check evaluates against.\n cron: '* * * * *',\n runner: 'cloud-scheduler',\n target: 'firebase-schedule-pluginJobsBeat-us-central1 → tenant /api/plugins/run-jobs',\n graceMinutes: 30,\n drives:\n 'The only thing that runs scheduled publishing and booking-hold expiry (AGL-1159). It is also the one job that can fail by pointing at the WRONG deployment (AGL-2176) — the mark is stamped by the tenant runner that answers, so a beat aimed elsewhere reads as silence here.',\n },\n] as const\n\n/** One job's most recent mark, as read from `CRON_BEAT_COLLECTION`. */\nexport interface CronBeat {\n jobId: string\n /** Epoch ms of the invocation that left the mark. */\n atMs: number\n}\n\nexport interface CronJobCheck extends HealthCheck {\n /** The schedule this row was judged against, so the body is self-describing. */\n schedule: string\n runner: CronRunner\n /** Minutes since the job last reported. Null when it never has. */\n lastBeatAgeMinutes: number | null\n /**\n * The most recent fire time the job should already have reported for, ISO.\n * Null when no such time has passed since we started watching — which is\n * the healthy reading for a job that is legitimately idle.\n */\n dueAt: string | null\n graceMinutes: number\n}\n\n/**\n * A cron field, expanded to the set of values it matches.\n *\n * Supports a star, `a`, `a-b`, `a,b,c` and a trailing `/n` step on any of\n * them — every form the inventory and `scheduled-crons.yml` use, and nothing\n * else. An unparsable field returns null, which the caller treats as \"matches\n * nothing\". A schedule that can never fire then produces a null `dueAt` and\n * a green row rather than a permanent red one, and the wiring spec is what\n * catches the typo — a monitor that reds on its own parse bug teaches people\n * to ignore it.\n */\nfunction expandCronField(\n field: string,\n min: number,\n max: number,\n): Set<number> | null {\n const values = new Set<number>()\n for (const part of field.split(',')) {\n const [range, stepRaw] = part.split('/')\n const step = stepRaw === undefined ? 1 : Number.parseInt(stepRaw, 10)\n if (!Number.isFinite(step) || step <= 0) return null\n let from: number\n let to: number\n if (range === '*') {\n from = min\n to = max\n } else if (range.includes('-')) {\n const [a, b] = range.split('-')\n from = Number.parseInt(a, 10)\n to = Number.parseInt(b, 10)\n } else {\n from = Number.parseInt(range, 10)\n to = stepRaw === undefined ? from : max\n }\n if (!Number.isFinite(from) || !Number.isFinite(to)) return null\n if (from < min || to > max || from > to) return null\n for (let value = from; value <= to; value += step) values.add(value)\n }\n return values.size ? values : null\n}\n\ninterface ParsedCron {\n minute: Set<number> | null\n hour: Set<number> | null\n dayOfMonth: Set<number> | null\n month: Set<number> | null\n dayOfWeek: Set<number> | null\n /** True when either day field is restricted — the standard OR applies. */\n domRestricted: boolean\n dowRestricted: boolean\n}\n\nfunction parseCron(expression: string): ParsedCron | null {\n const fields = expression.trim().split(/\\s+/)\n if (fields.length !== 5) return null\n const [minute, hour, dayOfMonth, month, dayOfWeek] = fields\n const parsed: ParsedCron = {\n minute: expandCronField(minute, 0, 59),\n hour: expandCronField(hour, 0, 23),\n dayOfMonth: expandCronField(dayOfMonth, 1, 31),\n month: expandCronField(month, 1, 12),\n dayOfWeek: expandCronField(dayOfWeek, 0, 6),\n domRestricted: dayOfMonth !== '*',\n dowRestricted: dayOfWeek !== '*',\n }\n if (\n !parsed.minute ||\n !parsed.hour ||\n !parsed.dayOfMonth ||\n !parsed.month ||\n !parsed.dayOfWeek\n ) {\n return null\n }\n return parsed\n}\n\n/**\n * Does this UTC date fall on a day the expression can fire?\n *\n * The day-of-month / day-of-week OR is the standard cron rule: when BOTH are\n * restricted the job fires on either, not on both. No expression in the\n * inventory restricts both, and the rule is implemented anyway so that one\n * added later is not silently misjudged.\n */\nfunction cronDayMatches(parsed: ParsedCron, date: Date): boolean {\n if (!parsed.month.has(date.getUTCMonth() + 1)) return false\n const domHit = parsed.dayOfMonth.has(date.getUTCDate())\n const dowHit = parsed.dayOfWeek.has(date.getUTCDay())\n if (parsed.domRestricted && parsed.dowRestricted) return domHit || dowHit\n if (parsed.domRestricted) return domHit\n if (parsed.dowRestricted) return dowHit\n return true\n}\n\n/**\n * The latest time at or before `atMs` that `expression` fires, UTC.\n *\n * Null when the expression is unparsable, or when it does not fire inside\n * the lookback — 45 days, which comfortably clears the longest gap any\n * inventory entry has (`usage-email`'s twenty-nine idle days).\n *\n * Whole non-matching DAYS are skipped rather than walked minute by minute,\n * so the worst case is ~45 day checks plus 1,440 minute checks rather than\n * 64,800 of them.\n */\nexport function previousCronFire(\n expression: string,\n atMs: number,\n lookbackDays = 45,\n): number | null {\n const parsed = parseCron(expression)\n if (!parsed) return null\n // Floor to the minute: a fire at 05:00:00 counts as due at 05:00:30.\n let cursor = Math.floor(atMs / 60_000) * 60_000\n const floor = cursor - lookbackDays * 86_400_000\n while (cursor >= floor) {\n const date = new Date(cursor)\n if (!cronDayMatches(parsed, date)) {\n // Jump to 23:59 of the previous UTC day.\n const startOfDay = Date.UTC(\n date.getUTCFullYear(),\n date.getUTCMonth(),\n date.getUTCDate(),\n )\n cursor = startOfDay - 60_000\n continue\n }\n if (\n parsed.hour.has(date.getUTCHours()) &&\n parsed.minute.has(date.getUTCMinutes())\n ) {\n return cursor\n }\n cursor -= 60_000\n }\n return null\n}\n\n/**\n * The scheduled jobs, reduced to one health check per job (AGL-1955).\n *\n * `beats` null means the marks could not be read at all — degraded for every\n * row, the same rule `signupsHealth`, `rateLimitsHealth` and `beaconHealth`\n * follow. An alarm that cannot see the thing it watches must say so rather\n * than report calm; here it is the literal condition the issue is about.\n *\n * `watchStartedAtMs` is when this deployment first read the marks. A job with\n * no mark is silent only once a fire time has passed SINCE then — otherwise\n * every row would be red on the day this deploys, and `usage-email` would\n * stay red until the 1st.\n *\n * Pure on purpose, like its siblings: the route reads, this decides, the\n * spec exercises every branch without a network.\n */\nexport function cronJobsHealth(\n beats: readonly CronBeat[] | null,\n watchStartedAtMs: number,\n ms: number,\n now: number = Date.now(),\n jobs: readonly ScheduledJob[] = SCHEDULED_JOBS,\n): Record<string, CronJobCheck> {\n const byId = new Map((beats ?? []).map((beat) => [beat.jobId, beat.atMs]))\n const checks: Record<string, CronJobCheck> = {}\n for (const job of jobs) {\n const lastBeatMs = byId.get(job.id) ?? null\n const lastBeatAgeMinutes =\n lastBeatMs === null ? null : Math.round((now - lastBeatMs) / 60_000)\n // The most recent fire time that is already past its grace. Everything\n // more recent than that is a run we are still waiting for, on time.\n const dueMs = previousCronFire(job.cron, now - job.graceMinutes * 60_000)\n const base = {\n ms,\n schedule: job.cron,\n runner: job.runner,\n lastBeatAgeMinutes,\n dueAt: dueMs === null ? null : new Date(dueMs).toISOString(),\n graceMinutes: job.graceMinutes,\n }\n if (beats === null) {\n checks[job.id] = { ...base, ok: false, code: 'beats-unavailable' }\n continue\n }\n if (dueMs === null) {\n // Nothing was due inside the lookback. Green, and honestly so.\n checks[job.id] = { ...base, ok: true }\n continue\n }\n if (lastBeatMs === null) {\n // Never reported. Only a defect once a fire time has passed since we\n // started watching — before that it is a job we have not met yet.\n const overdue = dueMs > watchStartedAtMs\n checks[job.id] = overdue\n ? { ...base, ok: false, code: 'job-never-reported' }\n : { ...base, ok: true, code: 'awaiting-first-run' }\n continue\n }\n checks[job.id] =\n lastBeatMs < dueMs\n ? { ...base, ok: false, code: 'job-silent' }\n : { ...base, ok: true }\n }\n return checks\n}\n\n/**\n * The mark a run leaves — the only writes on the beat path.\n *\n * Structurally typed against firebase-admin's Firestore rather than importing\n * it: this module is pure and is imported by tenant Server Components, and a\n * health verdict library that drags in the admin SDK would be a new reason\n * for a page to fail. The console routes, the marketing plugin's scheduled\n * campaign processor and the tenant job runner all pass their own handle.\n *\n * Two writes, in order: the job's own document, which is the authority, then\n * its entry in {@link CRON_BEAT_SUMMARY_DOC}, which is what the reader reads\n * first. The summary merges one map key, so jobs beating at the same moment\n * do not overwrite each other's entries.\n *\n * NEVER THROWS. A beat that cannot be written must not take down the job it\n * is describing — the monitor becoming the outage is its own failure mode.\n * It returns false when the job's own mark could not be written, and a write\n * that keeps failing shows up where it should: as a silent job on the board.\n * A failed summary write still returns true, because the mark that decides\n * the verdict landed.\n */\nexport interface CronBeatStore {\n collection(name: string): {\n doc(id: string): {\n set(data: Record<string, unknown>, options?: unknown): Promise<unknown>\n }\n }\n}\n\nexport async function writeCronBeat(\n store: CronBeatStore,\n jobId: string,\n now: number = Date.now(),\n): Promise<boolean> {\n try {\n const beats = store.collection(CRON_BEAT_COLLECTION)\n await beats\n .doc(jobId)\n .set({ jobId, atMs: now, at: new Date(now).toISOString() }, { merge: true })\n try {\n await beats\n .doc(CRON_BEAT_SUMMARY_DOC)\n .set({ beats: { [jobId]: now } }, { merge: true })\n } catch {\n // The reader confirms a stale summary entry against the mark above.\n }\n return true\n } catch {\n return false\n }\n}\n\n/**\n * The beats a summary document holds, ignoring anything malformed.\n *\n * `raw` is the document's `beats` field as Firestore returns it — a plain map,\n * or undefined when the document does not exist yet.\n */\nexport function cronBeatsFromSummary(raw: unknown): CronBeat[] {\n if (!raw || typeof raw !== 'object') return []\n return Object.entries(raw as Record<string, unknown>)\n .map(([jobId, atMs]) => ({ jobId, atMs: Number(atMs) }))\n .filter((beat) => !CRON_BEAT_RESERVED_DOCS.has(beat.jobId))\n .filter((beat) => Number.isFinite(beat.atMs))\n}\n\n/**\n * DID A STRANGER ACTUALLY SIGN UP? (AGL-2715)\n *\n * Every other signup check on this platform is an inference. The auth doors\n * assert that Identity Platform is reachable and refuses an account that\n * cannot exist — configuration, not a signup. `probeCreate` reads that the\n * platform is unlocked and the probe's name is free — preconditions, not a\n * signup. `signupVolume`, `signupRefusals` and `signupDrought` count what\n * happened afterwards — a consequence, observed late.\n *\n * The strongest thing any of them could say, on the day AGL-2714 paged, was\n * \"somebody got an account recently\", and recently was 24 hours ago. AGL-2581\n * is what that costs: signup refused every visitor for three days while the\n * monitor named for signups stayed green.\n *\n * So something has to WALK it. A canary creates an account, verifies it,\n * mints a session, provisions an org, and deletes all of it — against\n * production, on a schedule — then stamps what happened into one marker. This\n * verdict reads that marker.\n *\n * ## Why the walk is not done here\n *\n * Health probes on this platform are read-only and cost-bounded by contract,\n * and this one is served from a PUBLIC unauthenticated endpoint. A probe that\n * created an account would hand anyone with `curl` an org factory. The write\n * lives in a scheduled job; the door only reports. Same split as the beacon\n * heartbeat (AGL-2713), for the same reason.\n *\n * ## The four ways this reds, and why residue is one of them\n *\n * A canary that walks perfectly and fails to clean up is not a success. It\n * leaves a real org, with a real slug reservation and a real host, on the\n * production estate — every hour, forever. `canary-left-residue` is red even\n * though the signup itself worked, because the alternative is a monitor that\n * quietly fills the database it is monitoring.\n */\nexport interface SignupCanaryMarker {\n /** When the walk finished. The freshness clock. */\n walkedAtMs?: number\n /** Whether every step of the walk succeeded. */\n ok?: boolean\n /** Which step broke, for the body. Never an error message, never a uid. */\n failedStep?: string | null\n /** Wall clock for the whole walk. */\n elapsedMs?: number\n /** Whether the walk deleted everything it created. */\n reapedCleanly?: boolean\n}\n\nexport interface SignupCanaryCheck extends HealthCheck {\n /** Age of the last recorded walk, or null when there is no marker. */\n ageMs: number | null\n /** Which step broke on that walk, when one did. */\n failedStep: string | null\n /** How long the walk took, for the latency graph. */\n walkMs: number | null\n /** Whether it cleaned up after itself. */\n reapedCleanly: boolean | null\n /** The age past which a walk stops counting as evidence. */\n staleAfterMs: number\n}\n\n/**\n * How old the last successful walk may be before this reds.\n *\n * Two hours against an hourly schedule — one missed run is tolerated, two are\n * not. A canary that runs hourly and reds at 61 minutes would page on every\n * GitHub Actions queue delay, and an alarm that cries wolf on infrastructure\n * latency is one nobody reads by the time signup actually breaks. Two misses\n * is a pattern rather than a hiccup.\n *\n * This is deliberately NOT generous enough to sleep through AGL-2581: three\n * days of refused signups would red this inside two hours.\n */\n/**\n * Workspace-slug prefix the signup canary owns, and the one piece of the\n * product that has to know the canary exists (AGL-2715).\n *\n * ## Why an exclusion is not optional\n *\n * The canary creates a real org and then deletes it. `signupDrought` compares\n * durable ATTEMPT markers against a live COUNT of orgs — and a deleted org\n * leaves the count while its attempt marker stays. So every canary walk adds\n * 1 to the denominator and 0 to the numerator, and after three walks the\n * platform reports `signup-drought` with nothing wrong.\n *\n * That is not theoretical. It fired on 2026-09-09 at six attempts against\n * zero creations — six walks that had each just proved, end to end, that a\n * stranger could sign up. A monitor that manufactures the outage it watches\n * for is worse than no monitor, and it is the same alarm fatigue AGL-2714\n * removed, arriving from inside the house.\n *\n * ## Why the slug, and what that costs\n *\n * `/api/orgs/create` receives the requested slug before it does anything, so\n * this needs no secret, no header and no second code path — the canary is\n * excluded by being what it says it is.\n *\n * The cost is that the prefix is caller-supplied: someone naming their\n * workspace `signup-canary-…` would not be counted in the drought's\n * denominator. That is bounded to one uncounted attempt per such workspace,\n * and it cannot hide an outage, because an outage is many attempts by many\n * people and only the oddly-named ones would go missing.\n *\n * The canary declares the same prefix in `tools/e2e/signup-canary.mjs`, which\n * cannot import this file — it runs outside the workspace. They are held\n * together by `apps/console/specs/signup-canary-marker-wiring.spec.ts`.\n */\nexport const SIGNUP_CANARY_ORG_SLUG_PREFIX = 'signup-canary-'\n\n/**\n * Is this org creation the canary's own?\n *\n * Exported as a function rather than leaving callers to compare strings, so\n * the one place that decides is the one place to read when asking why a\n * number looks wrong.\n */\nexport function isSignupCanaryOrgSlug(slug: string | null | undefined): boolean {\n return typeof slug === 'string' && slug.startsWith(SIGNUP_CANARY_ORG_SLUG_PREFIX)\n}\n\n/**\n * The plus tag every canary address carries.\n *\n * Identity Platform will not reuse an address, so the walk stamps a fresh one\n * per run by appending to the LOCAL PART of `SIGNUP_CANARY_EMAIL`:\n * `name+signup-canary@example.com` becomes\n * `name+signup-canary-m2rso9ab12@example.com`, which still reaches the same\n * mailbox. The tag is therefore the only part of a canary account that the\n * console can recognize at the moment it first sees one — there is no org yet,\n * no claim, and nothing on the token that says CI.\n *\n * The address is operator-supplied, so this is a convention rather than a\n * fact, and `tools/e2e/signup-canary.mjs` is what makes it one: it refuses to\n * start on a base address whose tag is not this. Held together by\n * `apps/console/specs/signup-canary-marker-wiring.spec.ts`, exactly as the\n * slug prefix above is.\n */\nexport const SIGNUP_CANARY_EMAIL_TAG = 'signup-canary'\n\n/**\n * Is this sign-up the canary's own?\n *\n * The cost mirrors the slug prefix's: a stranger who signs up as\n * `them+signup-canary@example.com` goes unannounced. That is bounded to the\n * one address, it cannot hide anything staff need — the account still exists,\n * still appears in the admin user list, and still bills — and it is the price\n * of a check that needs no secret and no second code path.\n */\nexport function isSignupCanaryEmail(email: string | null | undefined): boolean {\n if (typeof email !== 'string') return false\n const local = email.toLowerCase().split('@')[0] ?? ''\n const plus = local.indexOf('+')\n if (plus < 0) return false\n // `startsWith`, not equality: the walk's own address is the base tag plus a\n // stamp, and the base address itself must match too — a human reading the\n // mailbox should not find the two classified differently.\n return local.slice(plus + 1).startsWith(SIGNUP_CANARY_EMAIL_TAG)\n}\n\n/**\n * How old the last walk may be before the check goes red.\n *\n * Six hours, far wider than the schedule that writes it, and deliberately so.\n * GitHub Actions serves `schedule` from a best-effort queue that drops most\n * requests under load: measured 2026-09-10, `uptime-probe.yml` asks for every\n * fifteen minutes and actually fired at gaps of 1h55m to 4h38m across the\n * preceding day and a half. A window sized to the cron expression rather than\n * to that behavior reds on ordinary queue depth.\n *\n * Widening it costs no detection speed, because staleness is not how a broken\n * signup surfaces. A walk that cannot sign up FAILS and still writes its\n * marker, which reads `signup-walk-failed` on the next poll. Staleness can\n * only mean the walker never reported — a CI condition, not a product one —\n * so this window's whole job is to notice a canary that has genuinely stopped,\n * and it must not fire before then: this check grades the monitor that must\n * never be muted.\n */\nexport const SIGNUP_CANARY_STALE_AFTER_MS = 6 * 60 * 60 * 1000\n\n/**\n * Grade the last recorded signup walk.\n *\n * Pure, like every sibling: the route reads the marker, this decides. Ordered\n * by which mistake costs most — a missing marker is treated as failure, never\n * as calm, because \"no evidence\" and \"evidence of success\" are the two\n * readings this whole issue exists to stop anyone from confusing.\n */\nexport function signupCanaryHealth(\n marker: SignupCanaryMarker | null,\n ms: number,\n nowMs: number = Date.now(),\n staleAfterMs: number = SIGNUP_CANARY_STALE_AFTER_MS,\n): SignupCanaryCheck {\n const base: Omit<SignupCanaryCheck, 'ok' | 'code'> = {\n ms,\n ageMs: null,\n failedStep: null,\n walkMs: null,\n reapedCleanly: null,\n staleAfterMs,\n }\n // No marker at all, or a store that would not answer. The canary has never\n // run, or cannot be read — either way nothing here has demonstrated that a\n // stranger can sign up, and this check exists to say so out loud.\n if (!marker || typeof marker.walkedAtMs !== 'number') {\n return { ...base, ok: false, code: 'canary-unavailable' }\n }\n const ageMs = Math.max(0, nowMs - marker.walkedAtMs)\n const walkMs = typeof marker.elapsedMs === 'number' ? marker.elapsedMs : null\n const reapedCleanly =\n typeof marker.reapedCleanly === 'boolean' ? marker.reapedCleanly : null\n const failedStep =\n typeof marker.failedStep === 'string' ? marker.failedStep : null\n const carried = { ...base, ageMs, walkMs, reapedCleanly, failedStep }\n\n // A stale PASS is not a pass. A walk that succeeded long enough ago says\n // nothing about the door now, and the whole point of a canary is currency.\n // Checked before the verdict so a stale failure reports as stale rather\n // than as a failure that may since have healed.\n if (ageMs > staleAfterMs) {\n return { ...carried, ok: false, code: 'canary-stale' }\n }\n if (marker.ok !== true) {\n return { ...carried, ok: false, code: 'signup-walk-failed' }\n }\n // Walked fine, did not clean up. Red anyway — see the docblock.\n if (reapedCleanly === false) {\n return { ...carried, ok: false, code: 'canary-left-residue' }\n }\n return { ...carried, ok: true }\n}\n\n/**\n * IS ATTESTATION WORKING FOR REAL PEOPLE? (AGL-2715)\n *\n * The signup canary walks the real journey, but it attests with an App Check\n * DEBUG TOKEN — because this project's provider is reCAPTCHA Enterprise, which\n * is built to score headless automation as a bot and always will. Everything\n * else in that walk is real; the attestation step is not. So the canary is\n * structurally blind to the one failure it sits closest to: App Check refusing\n * the people it is supposed to admit.\n *\n * That blindness is not acceptable on its own, and it is not fixed by\n * argument. It is fixed by measuring the thing directly.\n *\n * ## What this reads, and why a token cannot fool it\n *\n * Firebase publishes `firebaseappcheck.googleapis.com/services/verification_count`\n * to Cloud Monitoring, labelled by `result` (ALLOW / DENY) and `security`\n * (VALID / INVALID / MISSING_UNKNOWN_ORIGIN), per service. Those counts are\n * every request from every visitor.\n *\n * The canary contributes at most one verification an hour against roughly a\n * thousand a day, and its debug-token attestation lands in the same ALLOW\n * bucket as everyone else's. So it cannot hold this green: if attestation\n * collapses for real people the rate collapses with it, one canary request\n * notwithstanding.\n *\n * ## Why only Identity Platform is graded\n *\n * `identitytoolkit.googleapis.com` is the service signup depends on, and it\n * runs clean — 939 ALLOW against 3 DENY over the 24 hours to 2026-09-09.\n * `firestore.googleapis.com` in the same window was 5493 ALLOW against 1046\n * DENY, because client Firestore reads are reached by crawlers and stale tabs\n * that legitimately fail attestation. Grading that number would either mute\n * this check with a floor low enough to admit the noise, or page on the noise.\n * The other services are reported for diagnosis and decide nothing.\n */\nexport interface AppCheckVerificationSample {\n /** `ALLOW` or `DENY`. */\n result?: string\n /** `VALID`, `INVALID`, `MISSING_UNKNOWN_ORIGIN`, … — for the body only. */\n security?: string\n /** The service the verdict was rendered for. */\n service?: string\n /** Requests in this bucket over the window. */\n count?: number\n}\n\n/**\n * IS THE EDGE STILL LETTING REAL VISITORS IN? (AGL-2720)\n *\n * Every synthetic on this platform carries the `x-aglyn-probe` firewall\n * bypass, because bot protection refuses a datacenter IP outright. So if the\n * edge started challenging real people, every check would ride past the thing\n * that was broken and stay green.\n *\n * Measured 2026-09-10 while looking for something to close that: Vercel\n * publishes no challenged-versus-allowed metric — seven endpoints probed, the\n * only one that answers returns rule actions rather than traffic. And a probe\n * cannot substitute, because the discriminator is not \"was this request\n * challenged\" but \"can a REAL BROWSER get in\": `curl` from a residential\n * address is challenged too, and that is the healthy state, since the\n * challenge is answerable and a non-JS client cannot answer it.\n *\n * What is left is the outcome, and it was already being counted for money.\n * Metered page views are written by real browsers on real customer sites\n * after the edge admitted them — 57 to 563 a day across hosts over the\n * preceding week. Nothing synthetic touches that number: the probes ride the\n * bypass and the signup canary walks the console, neither of which is a\n * metered tenant page view.\n *\n * So the question this asks is simply whether that number is still moving. A\n * platform serving tens of views an hour that serves none for hours has\n * either lost its edge or lost every customer site, and both are worth waking\n * somebody for.\n *\n * ⛔ What it is NOT. It watches the TENANT edge, because that is where the\n * volume is. The console's own front door serves a handful of signup views a\n * day — too few to floor, and now partly synthetic — so a firewall change\n * scoped only to `aglyn-console` would not show up here. It is also not a\n * latency or correctness check: it says traffic arrives, nothing more.\n *\n * ⚠️ And the volume is CONCENTRATED. On the day this was built, one host of\n * thirteen produced every view. So this reds if that one site goes quiet for\n * half a day, which is why the window is what it is — see\n * {@link EDGE_ADMISSION_QUIET_AFTER_MS}.\n */\n/**\n * What the metered-traffic sampler leaves behind.\n *\n * `advancedAtMs` is the whole point and it is the WRITER's judgement, not the\n * reader's: the sampler compares this reading against the last one and stamps\n * the moment the total actually grew. A day rollover sets the counter back to\n * zero, so a grader handed a single sample could not tell midnight from an\n * outage.\n */\nexport interface EdgeAdmissionMarker {\n /** When the sampler last ran. Context; the verdict does not use it. */\n sampledAtMs?: number\n /** The last moment the metered total was seen to GROW. The clock. */\n advancedAtMs?: number\n /** Which UTC day the total belongs to. */\n day?: string\n /** The running total for that day at the last sample. */\n total?: number\n}\n\nexport interface EdgeAdmissionCheck extends HealthCheck {\n /** How long since the metered total last moved, or null with no marker. */\n quietMs: number | null\n /** The day the sampled total belongs to, so a rollover is readable. */\n day: string | null\n /** The running total for that day at the last sample. */\n total: number | null\n /** How long the total may sit still before this reds. */\n quietAfterMs: number\n /** How long since the sampler last reported, or null with no marker. */\n sampleAgeMs: number | null\n}\n\n/**\n * How long metered page views may sit still before the edge is presumed shut.\n *\n * Twelve hours, and the width is deliberate rather than lazy.\n *\n * The quietest full day in the sampled week carried 57 views — about 2.4 an\n * hour on average, which would make six hours of zero look damning. But the\n * average is the wrong statistic here: measured 2026-09-10, the day's 62 views\n * came from ONE host of thirteen. Traffic that concentrated arrives in bursts,\n * and a burst distribution produces long empty stretches with nothing wrong.\n * A window sized to the average would page on one of them, which is how the\n * monitor that must never be ignored gets ignored.\n *\n * Twelve hours of exactly zero, on a platform serving 57 to 563 views a day,\n * is not a quiet stretch. It is slower to fire than anyone would like, and it\n * is the only thing that catches a fully blocked edge at all — nothing else\n * would ever fire, because every other check rides the bypass.\n *\n * ⚑ Tighten it once the real quiet-period distribution is known: the sampler\n * stamps `advancedAtMs` on every run, so a week of markers says what the true\n * maximum gap is. Do not tighten it from the daily average.\n */\nexport const EDGE_ADMISSION_QUIET_AFTER_MS = 12 * 60 * 60 * 1000\n\n/**\n * How old the sampler's own reading may be before it stops being evidence.\n *\n * Six hours, sized like `SIGNUP_CANARY_STALE_AFTER_MS` to a scheduler that\n * drops most of what it is asked for (AGL-2723) rather than to the cron\n * expression. Shorter than the quiet window on purpose: a stalled sampler\n * should be named as one BEFORE its frozen reading has had time to look like\n * an edge outage.\n */\nexport const EDGE_ADMISSION_SAMPLE_STALE_AFTER_MS = 6 * 60 * 60 * 1000\n\n/**\n * Grade the last recorded metered-traffic sample.\n *\n * Pure, like every sibling. The sampler decides when the total last MOVED and\n * stamps it; this only asks how long ago that was. Keeping the movement\n * detection in the writer is what lets the door stay a read: a day rollover\n * resets the counter to zero, and a grader looking at one sample could not\n * tell that from traffic stopping.\n */\nexport function edgeAdmissionHealth(\n marker: EdgeAdmissionMarker | null,\n ms: number,\n nowMs: number = Date.now(),\n quietAfterMs: number = EDGE_ADMISSION_QUIET_AFTER_MS,\n sampleStaleAfterMs: number = EDGE_ADMISSION_SAMPLE_STALE_AFTER_MS,\n): EdgeAdmissionCheck {\n const base: Omit<EdgeAdmissionCheck, 'ok' | 'code'> = {\n ms,\n quietMs: null,\n day: null,\n total: null,\n quietAfterMs,\n sampleAgeMs: null,\n }\n // No marker, or one that cannot be read. Absence of proof reds here for the\n // same reason it does on every sibling: \"nobody measured\" and \"traffic is\n // arriving\" are the two readings this exists to keep apart.\n if (!marker || typeof marker.advancedAtMs !== 'number') {\n return { ...base, ok: false, code: 'edge-admission-unavailable' }\n }\n const sampleAgeMs =\n typeof marker.sampledAtMs === 'number'\n ? Math.max(0, nowMs - marker.sampledAtMs)\n : null\n const carried = {\n ...base,\n quietMs: Math.max(0, nowMs - marker.advancedAtMs),\n day: typeof marker.day === 'string' ? marker.day : null,\n total: typeof marker.total === 'number' ? marker.total : null,\n sampleAgeMs,\n }\n // ⛔ CHECKED FIRST, AND THE ORDER IS THE POINT.\n //\n // `advancedAtMs` only moves when the sampler runs, so a sampler that has\n // stopped looks exactly like traffic that has stopped — and the responder\n // would go looking at the firewall while the real fault was a job that\n // never fired. That is the confusion `canary-stale` was built to avoid on\n // the check next door, and it costs one comparison to avoid here too.\n if (sampleAgeMs === null || sampleAgeMs > sampleStaleAfterMs) {\n return { ...carried, ok: false, code: 'edge-sample-stale' }\n }\n if (carried.quietMs !== null && carried.quietMs > quietAfterMs) {\n return { ...carried, ok: false, code: 'edge-admits-nobody' }\n }\n return { ...carried, ok: true }\n}\n\nexport interface AppCheckAttestationCheck extends HealthCheck {\n /** Allowed over total for the graded service, or null with no traffic. */\n allowRate: number | null\n /** Requests the rate is taken over. */\n allowed: number\n denied: number\n /** Below this many requests the rate says nothing and is not graded. */\n minimumRequests: number\n /** The floor the rate must clear. */\n minimumAllowRate: number\n /** The service graded. Everything else is context. */\n service: string\n /** Denials by `security`, so a red says which way it broke. */\n deniedBy: Record<string, number>\n /** The window the counts cover. */\n windowMinutes: number\n}\n\n/** The service signup depends on. See the docblock for why it is the only one. */\nexport const APP_CHECK_GRADED_SERVICE = 'identitytoolkit.googleapis.com'\n\n/**\n * Window the attestation rate is taken over.\n *\n * Six hours, not one. This metric is aggregated by Google on its own schedule\n * and arrives late, so a one-hour window reads empty for reasons that have\n * nothing to do with attestation. Six hours is long enough to always carry\n * traffic on this platform and short enough that a real collapse is caught the\n * same working day.\n */\nexport const APP_CHECK_WINDOW_MINUTES = 6 * 60\n\n/**\n * Requests below which the rate is not graded.\n *\n * A ratio over three requests is noise wearing a percentage. Twenty is a\n * fraction of the ~940 a day Identity Platform sees, so a healthy platform\n * always clears it, and a platform quiet enough not to is a platform where\n * this check has nothing to say.\n */\nexport const APP_CHECK_MIN_REQUESTS = 20\n\n/**\n * The floor the allow rate must clear.\n *\n * 0.90, against a measured 0.997. The gap is deliberate: a handful of genuine\n * denials is ordinary — a stale tab, a bot, a browser that failed its\n * reCAPTCHA — and a floor set near the observed rate would page on those.\n *\n * What this is built to catch is not a drift of a few percent; it is the\n * documented failure mode of `recaptcha-allowlist.ts`, where an origin that is\n * attached and routed but not allowlisted renders a console that can never\n * sign anyone in. That reads as ~0%, not 85%.\n */\nexport const APP_CHECK_MIN_ALLOW_RATE = 0.9\n\n/**\n * Grade attestation for real visitors.\n *\n * Pure, like every sibling: something else reads Cloud Monitoring, this\n * decides. Null samples are degraded rather than calm — an unreadable metric\n * is not evidence that attestation works, and this check exists precisely\n * because \"we did not look\" was being reported as \"it is fine\".\n */\nexport function appCheckAttestationHealth(\n samples: AppCheckVerificationSample[] | null,\n ms: number,\n service: string = APP_CHECK_GRADED_SERVICE,\n minimumRequests: number = APP_CHECK_MIN_REQUESTS,\n minimumAllowRate: number = APP_CHECK_MIN_ALLOW_RATE,\n windowMinutes: number = APP_CHECK_WINDOW_MINUTES,\n): AppCheckAttestationCheck {\n const base: Omit<AppCheckAttestationCheck, 'ok' | 'code'> = {\n ms,\n allowRate: null,\n allowed: 0,\n denied: 0,\n minimumRequests,\n minimumAllowRate,\n service,\n deniedBy: {},\n windowMinutes,\n }\n if (samples === null) {\n return { ...base, ok: false, code: 'attestation-unavailable' }\n }\n let allowed = 0\n let denied = 0\n const deniedBy: Record<string, number> = {}\n for (const sample of samples) {\n if (sample.service !== service) continue\n // `Number(...) || 0` for the reason every sibling gives: a corrupt count\n // must not turn the total into NaN, which compares false against every\n // threshold and would report calm forever.\n const count = Number(sample.count) || 0\n if (sample.result === 'ALLOW') allowed += count\n else if (sample.result === 'DENY') {\n denied += count\n const why = sample.security || 'UNKNOWN'\n deniedBy[why] = (deniedBy[why] ?? 0) + count\n }\n }\n const total = allowed + denied\n const carried = { ...base, allowed, denied, deniedBy }\n // Too little traffic to say anything. Reported, not hidden: an on-call\n // person reading a green body deserves to know this one abstained.\n if (total < minimumRequests) {\n return { ...carried, ok: true, code: 'not-enough-traffic', allowRate: null }\n }\n const allowRate = allowed / total\n return {\n ...carried,\n allowRate,\n ok: allowRate >= minimumAllowRate,\n ...(allowRate >= minimumAllowRate ? {} : { code: 'attestation-failing' }),\n }\n}\n"],"names":["HEALTH_NO_STORE","healthStatus","checks","Object","values","every","check","ok","healthBody","report","status","service","commit","version","environment","region","at","Date","toISOString","healthHeaders","healthHttpStatus","healthHeadOf","get","response","Response","headers","MAX_BACKUP_AGE_DAYS","BACKUP_IN_FLIGHT","Set","backupsHealth","backups","ms","now","options","code","states","newestReadyAgeDays","determinate","backup","state","snapshotMsOf","snapshotTime","parse","Number","NaN","newestOf","keep","filter","map","time","isFinite","reduce","newest","Math","max","ageDaysOf","round","newestReadyMs","newestAnyMs","newestAnyAgeDays","undateable","some","has","unreachable","length","MAX_EXPORT_AGE_DAYS","exportsHealth","markers","exportCount","newestExportAgeDays","newestMs","marker","timeCreated","undefined","ORG_CREATION_WINDOW_MINUTES","MAX_ORG_CREATIONS_PER_WINDOW","signupsHealth","recentOrgCreations","threshold","windowMinutes","SIGNUP_REFUSAL_WINDOW_MINUTES","MAX_SIGNUP_REFUSALS_PER_WINDOW","SIGNUP_LOCK_UNREADABLE_FIELD","MAX_UNREADABLE_SIGNUP_DECISIONS_PER_WINDOW","signupRefusalsHealth","unreadableThreshold","refusedSignups","refusedByReason","lockUnreadable","minutesSinceLast","newestAtMs","refusals","unreadable","reason","count","entries","byReason","refusedAtMs","over","blind","floor","SIGNUP_DROUGHT_WINDOW_MINUTES","MIN_SIGNUP_TRAFFIC_FOR_DROUGHT","signupDroughtHealth","attemptMarkers","orgCreations","minimumTraffic","signupPagesServed","serves","signupAttempts","attempts","drought","RATE_LIMIT_DEGRADED_WINDOW_MINUTES","MAX_DEGRADED_CALLS_PER_WINDOW","rateLimitsHealth","degradedCalls","degradedEpisodes","cutoff","recent","lastAtMs","total","calls","episodes","latest","Infinity","SERVER_ERROR_WINDOW_MINUTES","MAX_SERVER_ERRORS_PER_WINDOW","serverErrorsHealth","serverErrors","byService","erroredAtMs","errors","HEALTH_PROBE_TTL_MS","memoizeWithTtl","ttlMs","probe","cached","inFlight","elapsed","value","flight","clear","then","isTransientBeaconCode","startsWith","beaconHealth","write","logId","tolerance","landedAtMs","graceMs","sinceLanded","base","minutesSinceHeartbeat","graceMinutes","renderHealth","outcome","host","kind","hostResolved","nodeCount","WEBHOOK_FAILURE_WINDOW_MINUTES","MAX_FAILED_DELIVERIES_PER_WINDOW","MAX_INERT_DELIVERIES_PER_WINDOW","MAX_RETRIED_DELIVERIES_PER_WINDOW","billingWebhookHealth","facts","inertThreshold","retriedThreshold","endpointStatus","undelivered","emitted","processed","inert","retried","unsubscribedEvents","connectEndpoint","unsubscribedConnectEvents","meteredPricingHealth","stripeConfigured","monthly","yearly","unbilledInterval","CRON_BEAT_COLLECTION","CRON_BEAT_WATCH_DOC","CRON_BEAT_DEGRADED_DOC","CRON_BEAT_SUMMARY_DOC","CRON_BEAT_RESERVED_DOCS","CRON_DEGRADED_WINDOW_CONTINUITY_MS","SCHEDULED_JOBS","id","label","cron","runner","target","drives","expandCronField","field","min","part","split","range","stepRaw","step","parseInt","from","to","includes","a","b","add","size","parseCron","expression","fields","trim","minute","hour","dayOfMonth","month","dayOfWeek","parsed","domRestricted","dowRestricted","cronDayMatches","date","getUTCMonth","domHit","getUTCDate","dowHit","getUTCDay","previousCronFire","atMs","lookbackDays","cursor","startOfDay","UTC","getUTCFullYear","getUTCHours","getUTCMinutes","cronJobsHealth","beats","watchStartedAtMs","jobs","byId","Map","beat","jobId","job","lastBeatMs","lastBeatAgeMinutes","dueMs","schedule","dueAt","overdue","writeCronBeat","store","collection","doc","set","merge","cronBeatsFromSummary","raw","SIGNUP_CANARY_ORG_SLUG_PREFIX","isSignupCanaryOrgSlug","slug","SIGNUP_CANARY_EMAIL_TAG","isSignupCanaryEmail","email","local","toLowerCase","plus","indexOf","slice","SIGNUP_CANARY_STALE_AFTER_MS","signupCanaryHealth","nowMs","staleAfterMs","ageMs","failedStep","walkMs","reapedCleanly","walkedAtMs","elapsedMs","carried","EDGE_ADMISSION_QUIET_AFTER_MS","EDGE_ADMISSION_SAMPLE_STALE_AFTER_MS","edgeAdmissionHealth","quietAfterMs","sampleStaleAfterMs","quietMs","day","sampleAgeMs","advancedAtMs","sampledAtMs","APP_CHECK_GRADED_SERVICE","APP_CHECK_WINDOW_MINUTES","APP_CHECK_MIN_REQUESTS","APP_CHECK_MIN_ALLOW_RATE","appCheckAttestationHealth","samples","minimumRequests","minimumAllowRate","allowRate","allowed","denied","deniedBy","sample","result","why","security"],"mappings":"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"}