@aglyn/tenant-data-admin 1.0.0-beta.244 → 1.0.0-beta.246
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -7
- package/src/lib/server/client-error-report.d.ts +7 -0
- package/src/lib/server/client-error-report.js +6 -1
- package/src/lib/server/client-error-report.js.map +1 -1
- package/src/lib/server/erase.d.ts +34 -0
- package/src/lib/server/erase.js +48 -0
- package/src/lib/server/erase.js.map +1 -1
- package/src/lib/server/operator-health.js +10 -5
- package/src/lib/server/operator-health.js.map +1 -1
- package/src/lib/server/realm-plugins.js +11 -5
- package/src/lib/server/realm-plugins.js.map +1 -1
- package/src/lib/server/render-monitor.d.ts +6 -0
- package/src/lib/server/render-monitor.js +28 -3
- package/src/lib/server/render-monitor.js.map +1 -1
- package/src/lib/server/serve-media-cdn.d.ts +1 -1
- package/src/lib/server/serve-media-cdn.js +7 -3
- package/src/lib/server/serve-media-cdn.js.map +1 -1
|
@@ -29,7 +29,11 @@
|
|
|
29
29
|
* inside a transaction on the check's document.
|
|
30
30
|
*
|
|
31
31
|
* Cheap on the hot path: each instance remembers the last state it wrote, so
|
|
32
|
-
* a poll that finds nothing changed costs no Firestore round trip at all.
|
|
32
|
+
* a poll that finds nothing changed costs no Firestore round trip at all. An
|
|
33
|
+
* instance's FIRST poll of a check reads the document outside a transaction
|
|
34
|
+
* and writes nothing when the stored status already agrees: serverless
|
|
35
|
+
* instances come and go all day, and rewriting `detail` on every cold start
|
|
36
|
+
* was a write per check per instance that nothing reads.
|
|
33
37
|
* The Admin SDK is loaded on the first write rather than at import, so every
|
|
34
38
|
* health route can import this without initializing anything. Never throws.
|
|
35
39
|
*/ async function firestore() {
|
|
@@ -60,16 +64,17 @@ export async function recordHealthState(checkId, status, detail, options = {}) {
|
|
|
60
64
|
try {
|
|
61
65
|
const db = await firestore();
|
|
62
66
|
const ref = db.collection(OPERATOR_HEALTH_COLLECTION).doc(id);
|
|
67
|
+
const stored = await ref.get();
|
|
68
|
+
if (stored.exists && stored.get('status') === status) {
|
|
69
|
+
lastWritten.set(id, status);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
63
72
|
const edge = await db.runTransaction(async (transaction)=>{
|
|
64
73
|
var _ref;
|
|
65
74
|
const snapshot = await transaction.get(ref);
|
|
66
75
|
const prior = snapshot.exists ? snapshot.data() : null;
|
|
67
76
|
if ((prior == null ? void 0 : prior.status) === status) {
|
|
68
77
|
var _prior_sinceMs;
|
|
69
|
-
transaction.update(ref, {
|
|
70
|
-
detail: summary,
|
|
71
|
-
updatedAtMs: now
|
|
72
|
-
});
|
|
73
78
|
return {
|
|
74
79
|
transition: null,
|
|
75
80
|
priorSinceMs: (_prior_sinceMs = prior.sinceMs) != null ? _prior_sinceMs : now
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/operator-health.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 * HEALTH TRANSITIONS (AGL-3377): the health checks, remembered.\n *\n * Every `/api/health/*` endpoint answers 200 or 503 to whoever asks and\n * forgets the answer. An install with an external uptime monitor hears the\n * 503; an install without one — most self-hosted ones — hears nothing, and\n * a dead cron or a stale backup is found on the day it is needed.\n *\n * `recordHealthState` keeps the last verdict per check in\n * `operatorHealthState/{checkId}` and raises an operator alert on the EDGE:\n * `system.healthDegraded` when a healthy (or never-seen) check goes\n * degraded, `system.healthRecovered` when it comes back. Once each, however\n * many instances and pollers see the same state, because the edge is decided\n * inside a transaction on the check's document.\n *\n * Cheap on the hot path: each instance remembers the last state it wrote, so\n * a poll that finds nothing changed costs no Firestore round trip at all.\n * The Admin SDK is loaded on the first write rather than at import, so every\n * health route can import this without initializing anything. Never throws.\n */\n\nasync function firestore(): Promise<any> {\n const { default: firebaseAdmin } = await import('./firebase-admin')\n return firebaseAdmin.app().firestore()\n}\n\nexport const OPERATOR_HEALTH_COLLECTION = 'operatorHealthState'\n\nexport type OperatorHealthStatus = 'ok' | 'degraded'\n\nexport interface OperatorHealthStateDoc {\n checkId: string\n label: string\n status: OperatorHealthStatus\n /** When the check entered `status`. */\n sinceMs: number\n detail: string\n updatedAtMs: number\n}\n\nexport type OperatorHealthTransition = 'degraded' | 'recovered' | null\n\n/** The last status this instance wrote per check. */\nconst lastWritten = new Map<string, OperatorHealthStatus>()\n\n/** Test seam. */\nexport function resetOperatorHealthCacheForTests(): void {\n lastWritten.clear()\n}\n\n/** \"3 h 5 min\", for a recovery's duration. */\nexport function describeHealthDuration(ms: number): string {\n const minutes = Math.max(1, Math.round(ms / 60_000))\n if (minutes < 60) return `${minutes} min`\n const hours = Math.floor(minutes / 60)\n const rest = minutes % 60\n if (hours < 48) return rest ? `${hours} h ${rest} min` : `${hours} h`\n return `${Math.round(hours / 24)} days`\n}\n\nexport async function recordHealthState(\n checkId: string,\n status: OperatorHealthStatus,\n detail: string,\n options: { label?: string; now?: number } = {},\n): Promise<OperatorHealthTransition> {\n const id = String(checkId ?? '').trim()\n if (!id) return null\n if (lastWritten.get(id) === status) return null\n const now = options.now ?? Date.now()\n const label = String(options.label ?? '').trim() || id\n const summary = String(detail ?? '').trim().slice(0, 1_000)\n try {\n const db = await firestore()\n const ref = db.collection(OPERATOR_HEALTH_COLLECTION).doc(id)\n const edge = await db.runTransaction(async (transaction: any) => {\n const snapshot = await transaction.get(ref)\n const prior = (snapshot.exists ? snapshot.data() : null) as\n | Partial<OperatorHealthStateDoc>\n | null\n if (prior?.status === status) {\n transaction.update(ref, { detail: summary, updatedAtMs: now })\n return { transition: null, priorSinceMs: prior.sinceMs ?? now }\n }\n const next: OperatorHealthStateDoc = {\n checkId: id,\n label,\n status,\n sinceMs: now,\n detail: summary,\n updatedAtMs: now,\n }\n transaction.set(ref, next)\n // A check first seen healthy is the ordinary case and says nothing; a\n // check first seen degraded is an install that was broken before\n // anybody watched, which is exactly the one to say out loud.\n const transition: OperatorHealthTransition =\n status === 'degraded' ? 'degraded' : prior ? 'recovered' : null\n return { transition, priorSinceMs: prior?.sinceMs ?? now }\n })\n lastWritten.set(id, status)\n if (edge.transition) {\n const { raiseOperatorAlert } = await import('./operator-alerts')\n if (edge.transition === 'degraded') {\n await raiseOperatorAlert('system.healthDegraded', {\n dedupeKey: id,\n context: { check: label, detail: summary },\n })\n } else {\n await raiseOperatorAlert('system.healthRecovered', {\n dedupeKey: id,\n context: {\n check: label,\n duration: describeHealthDuration(now - edge.priorSinceMs),\n },\n })\n }\n }\n return edge.transition\n } catch (error) {\n // Once per verdict per instance: a store that is down must not turn\n // every poll of a public endpoint into another log line. The next\n // change of verdict tries again.\n lastWritten.set(id, status)\n console.error(`[operator-health] ${id} could not be recorded`, error)\n return null\n }\n}\n\n/** Every recorded check, for the staff page and the sweep's report. */\nexport async function listHealthStates(): Promise<OperatorHealthStateDoc[]> {\n try {\n const snapshot = await (await firestore()).collection(OPERATOR_HEALTH_COLLECTION).get()\n return snapshot.docs.map((doc: any) => doc.data() as OperatorHealthStateDoc)\n } catch (error) {\n console.error('[operator-health] states unreadable', error)\n return []\n }\n}\n\n/**\n * One health endpoint's body as a state: `status` is the endpoint's own\n * verdict, and the detail names each failing check and its code.\n */\nexport function healthStateOfBody(\n body: Record<string, unknown> | null,\n httpStatus: number | null,\n): { status: OperatorHealthStatus; detail: string } {\n if (!body) {\n return {\n status: 'degraded',\n detail:\n httpStatus === null\n ? 'The endpoint did not answer.'\n : `The endpoint answered ${httpStatus} with no readable body.`,\n }\n }\n const checks = (body['checks'] ?? {}) as Record<string, Record<string, unknown> | undefined>\n const failing = Object.entries(checks)\n .filter(([, check]) => check && check['ok'] !== true)\n .map(([name, check]) =>\n typeof check?.['code'] === 'string' ? `${name} (${check['code']})` : name,\n )\n const degraded =\n body['status'] === 'degraded' ||\n (body['status'] !== 'ok' && (httpStatus === 503 || failing.length > 0))\n return {\n status: degraded ? 'degraded' : 'ok',\n detail: degraded\n ? failing.length\n ? `Failing: ${failing.join(', ')}.`\n : `The endpoint answered ${httpStatus ?? 'nothing'}.`\n : 'All checks pass.',\n }\n}\n\n/**\n * What each health endpoint is called on the board and in an alert, keyed by\n * the `service` its body names. An unlisted service is called by its name.\n */\nexport const HEALTH_SERVICE_LABELS: Readonly<Record<string, string>> = {\n console: 'Console serving',\n 'console-auth-doors': 'Ways in',\n 'console-backups': 'Backups & exports',\n 'console-billing': 'Billing webhook',\n 'console-crons': 'Scheduled jobs',\n 'console-error-beacon': 'Console error beacon',\n 'console-journeys': 'Console journeys',\n 'console-rate-limits': 'Rate limiters',\n 'console-server-errors': 'Server errors',\n 'console-signup-volume': 'Signup volume',\n 'console-email': 'Email delivery',\n tenant: 'Site serving',\n 'tenant-error-beacon': 'Site error beacon',\n 'tenant-funnel': 'Site capture funnel',\n 'tenant-render-site': 'Site rendering',\n 'tenant-render-marketing': 'Marketing site rendering',\n}\n\n/**\n * Records a health endpoint's own response and hands it back unchanged\n * (AGL-3377): `return recordHealthResponse(Response.json(healthBody(…)))`.\n * The check id is the body's `service`, so the endpoint, the scheduled sweep\n * and any other poller all write the same state. Costs nothing while the\n * verdict is the one this instance last wrote. Never throws.\n */\nexport async function recordHealthResponse(response: Response): Promise<Response> {\n try {\n const body = (await response.clone().json()) as Record<string, unknown> | null\n const service = String(body?.['service'] ?? '').trim()\n if (service) {\n const { status, detail } = healthStateOfBody(body, response.status)\n await recordHealthState(service, status, detail, {\n label: HEALTH_SERVICE_LABELS[service] ?? service,\n })\n }\n } catch (error) {\n console.error('[operator-health] a health response could not be recorded', error)\n }\n return response\n}\n"],"names":["firestore","default","firebaseAdmin","app","OPERATOR_HEALTH_COLLECTION","lastWritten","Map","resetOperatorHealthCacheForTests","clear","describeHealthDuration","ms","minutes","Math","max","round","hours","floor","rest","recordHealthState","checkId","status","detail","options","id","String","trim","get","now","Date","label","summary","slice","db","ref","collection","doc","edge","runTransaction","transaction","snapshot","prior","exists","data","update","updatedAtMs","transition","priorSinceMs","sinceMs","next","set","raiseOperatorAlert","dedupeKey","context","check","duration","error","console","listHealthStates","docs","map","healthStateOfBody","body","httpStatus","checks","failing","Object","entries","filter","name","degraded","length","join","HEALTH_SERVICE_LABELS","tenant","recordHealthResponse","response","clone","json","service"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/operator-health.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 * HEALTH TRANSITIONS (AGL-3377): the health checks, remembered.\n *\n * Every `/api/health/*` endpoint answers 200 or 503 to whoever asks and\n * forgets the answer. An install with an external uptime monitor hears the\n * 503; an install without one — most self-hosted ones — hears nothing, and\n * a dead cron or a stale backup is found on the day it is needed.\n *\n * `recordHealthState` keeps the last verdict per check in\n * `operatorHealthState/{checkId}` and raises an operator alert on the EDGE:\n * `system.healthDegraded` when a healthy (or never-seen) check goes\n * degraded, `system.healthRecovered` when it comes back. Once each, however\n * many instances and pollers see the same state, because the edge is decided\n * inside a transaction on the check's document.\n *\n * Cheap on the hot path: each instance remembers the last state it wrote, so\n * a poll that finds nothing changed costs no Firestore round trip at all. An\n * instance's FIRST poll of a check reads the document outside a transaction\n * and writes nothing when the stored status already agrees: serverless\n * instances come and go all day, and rewriting `detail` on every cold start\n * was a write per check per instance that nothing reads.\n * The Admin SDK is loaded on the first write rather than at import, so every\n * health route can import this without initializing anything. Never throws.\n */\n\nasync function firestore(): Promise<any> {\n const { default: firebaseAdmin } = await import('./firebase-admin')\n return firebaseAdmin.app().firestore()\n}\n\nexport const OPERATOR_HEALTH_COLLECTION = 'operatorHealthState'\n\nexport type OperatorHealthStatus = 'ok' | 'degraded'\n\nexport interface OperatorHealthStateDoc {\n checkId: string\n label: string\n status: OperatorHealthStatus\n /** When the check entered `status`. */\n sinceMs: number\n detail: string\n updatedAtMs: number\n}\n\nexport type OperatorHealthTransition = 'degraded' | 'recovered' | null\n\n/** The last status this instance wrote per check. */\nconst lastWritten = new Map<string, OperatorHealthStatus>()\n\n/** Test seam. */\nexport function resetOperatorHealthCacheForTests(): void {\n lastWritten.clear()\n}\n\n/** \"3 h 5 min\", for a recovery's duration. */\nexport function describeHealthDuration(ms: number): string {\n const minutes = Math.max(1, Math.round(ms / 60_000))\n if (minutes < 60) return `${minutes} min`\n const hours = Math.floor(minutes / 60)\n const rest = minutes % 60\n if (hours < 48) return rest ? `${hours} h ${rest} min` : `${hours} h`\n return `${Math.round(hours / 24)} days`\n}\n\nexport async function recordHealthState(\n checkId: string,\n status: OperatorHealthStatus,\n detail: string,\n options: { label?: string; now?: number } = {},\n): Promise<OperatorHealthTransition> {\n const id = String(checkId ?? '').trim()\n if (!id) return null\n if (lastWritten.get(id) === status) return null\n const now = options.now ?? Date.now()\n const label = String(options.label ?? '').trim() || id\n const summary = String(detail ?? '').trim().slice(0, 1_000)\n try {\n const db = await firestore()\n const ref = db.collection(OPERATOR_HEALTH_COLLECTION).doc(id)\n const stored = await ref.get()\n if (stored.exists && stored.get('status') === status) {\n lastWritten.set(id, status)\n return null\n }\n const edge = await db.runTransaction(async (transaction: any) => {\n const snapshot = await transaction.get(ref)\n const prior = (snapshot.exists ? snapshot.data() : null) as\n | Partial<OperatorHealthStateDoc>\n | null\n if (prior?.status === status) {\n return { transition: null, priorSinceMs: prior.sinceMs ?? now }\n }\n const next: OperatorHealthStateDoc = {\n checkId: id,\n label,\n status,\n sinceMs: now,\n detail: summary,\n updatedAtMs: now,\n }\n transaction.set(ref, next)\n // A check first seen healthy is the ordinary case and says nothing; a\n // check first seen degraded is an install that was broken before\n // anybody watched, which is exactly the one to say out loud.\n const transition: OperatorHealthTransition =\n status === 'degraded' ? 'degraded' : prior ? 'recovered' : null\n return { transition, priorSinceMs: prior?.sinceMs ?? now }\n })\n lastWritten.set(id, status)\n if (edge.transition) {\n const { raiseOperatorAlert } = await import('./operator-alerts')\n if (edge.transition === 'degraded') {\n await raiseOperatorAlert('system.healthDegraded', {\n dedupeKey: id,\n context: { check: label, detail: summary },\n })\n } else {\n await raiseOperatorAlert('system.healthRecovered', {\n dedupeKey: id,\n context: {\n check: label,\n duration: describeHealthDuration(now - edge.priorSinceMs),\n },\n })\n }\n }\n return edge.transition\n } catch (error) {\n // Once per verdict per instance: a store that is down must not turn\n // every poll of a public endpoint into another log line. The next\n // change of verdict tries again.\n lastWritten.set(id, status)\n console.error(`[operator-health] ${id} could not be recorded`, error)\n return null\n }\n}\n\n/** Every recorded check, for the staff page and the sweep's report. */\nexport async function listHealthStates(): Promise<OperatorHealthStateDoc[]> {\n try {\n const snapshot = await (await firestore()).collection(OPERATOR_HEALTH_COLLECTION).get()\n return snapshot.docs.map((doc: any) => doc.data() as OperatorHealthStateDoc)\n } catch (error) {\n console.error('[operator-health] states unreadable', error)\n return []\n }\n}\n\n/**\n * One health endpoint's body as a state: `status` is the endpoint's own\n * verdict, and the detail names each failing check and its code.\n */\nexport function healthStateOfBody(\n body: Record<string, unknown> | null,\n httpStatus: number | null,\n): { status: OperatorHealthStatus; detail: string } {\n if (!body) {\n return {\n status: 'degraded',\n detail:\n httpStatus === null\n ? 'The endpoint did not answer.'\n : `The endpoint answered ${httpStatus} with no readable body.`,\n }\n }\n const checks = (body['checks'] ?? {}) as Record<string, Record<string, unknown> | undefined>\n const failing = Object.entries(checks)\n .filter(([, check]) => check && check['ok'] !== true)\n .map(([name, check]) =>\n typeof check?.['code'] === 'string' ? `${name} (${check['code']})` : name,\n )\n const degraded =\n body['status'] === 'degraded' ||\n (body['status'] !== 'ok' && (httpStatus === 503 || failing.length > 0))\n return {\n status: degraded ? 'degraded' : 'ok',\n detail: degraded\n ? failing.length\n ? `Failing: ${failing.join(', ')}.`\n : `The endpoint answered ${httpStatus ?? 'nothing'}.`\n : 'All checks pass.',\n }\n}\n\n/**\n * What each health endpoint is called on the board and in an alert, keyed by\n * the `service` its body names. An unlisted service is called by its name.\n */\nexport const HEALTH_SERVICE_LABELS: Readonly<Record<string, string>> = {\n console: 'Console serving',\n 'console-auth-doors': 'Ways in',\n 'console-backups': 'Backups & exports',\n 'console-billing': 'Billing webhook',\n 'console-crons': 'Scheduled jobs',\n 'console-error-beacon': 'Console error beacon',\n 'console-journeys': 'Console journeys',\n 'console-rate-limits': 'Rate limiters',\n 'console-server-errors': 'Server errors',\n 'console-signup-volume': 'Signup volume',\n 'console-email': 'Email delivery',\n tenant: 'Site serving',\n 'tenant-error-beacon': 'Site error beacon',\n 'tenant-funnel': 'Site capture funnel',\n 'tenant-render-site': 'Site rendering',\n 'tenant-render-marketing': 'Marketing site rendering',\n}\n\n/**\n * Records a health endpoint's own response and hands it back unchanged\n * (AGL-3377): `return recordHealthResponse(Response.json(healthBody(…)))`.\n * The check id is the body's `service`, so the endpoint, the scheduled sweep\n * and any other poller all write the same state. Costs nothing while the\n * verdict is the one this instance last wrote. Never throws.\n */\nexport async function recordHealthResponse(response: Response): Promise<Response> {\n try {\n const body = (await response.clone().json()) as Record<string, unknown> | null\n const service = String(body?.['service'] ?? '').trim()\n if (service) {\n const { status, detail } = healthStateOfBody(body, response.status)\n await recordHealthState(service, status, detail, {\n label: HEALTH_SERVICE_LABELS[service] ?? service,\n })\n }\n } catch (error) {\n console.error('[operator-health] a health response could not be recorded', error)\n }\n return response\n}\n"],"names":["firestore","default","firebaseAdmin","app","OPERATOR_HEALTH_COLLECTION","lastWritten","Map","resetOperatorHealthCacheForTests","clear","describeHealthDuration","ms","minutes","Math","max","round","hours","floor","rest","recordHealthState","checkId","status","detail","options","id","String","trim","get","now","Date","label","summary","slice","db","ref","collection","doc","stored","exists","set","edge","runTransaction","transaction","snapshot","prior","data","transition","priorSinceMs","sinceMs","next","updatedAtMs","raiseOperatorAlert","dedupeKey","context","check","duration","error","console","listHealthStates","docs","map","healthStateOfBody","body","httpStatus","checks","failing","Object","entries","filter","name","degraded","length","join","HEALTH_SERVICE_LABELS","tenant","recordHealthResponse","response","clone","json","service"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;CAuBC,GAED,eAAeA;IACb,MAAM,EAAEC,SAASC,aAAa,EAAE,GAAG,MAAM,MAAM,CAAC;IAChD,OAAOA,cAAcC,GAAG,GAAGH,SAAS;AACtC;AAEA,OAAO,MAAMI,6BAA6B,sBAAqB;AAgB/D,mDAAmD,GACnD,MAAMC,cAAc,IAAIC;AAExB,eAAe,GACf,OAAO,SAASC;IACdF,YAAYG,KAAK;AACnB;AAEA,4CAA4C,GAC5C,OAAO,SAASC,uBAAuBC,EAAU;IAC/C,MAAMC,UAAUC,KAAKC,GAAG,CAAC,GAAGD,KAAKE,KAAK,CAACJ,KAAK;IAC5C,IAAIC,UAAU,IAAI,OAAO,GAAGA,QAAQ,IAAI,CAAC;IACzC,MAAMI,QAAQH,KAAKI,KAAK,CAACL,UAAU;IACnC,MAAMM,OAAON,UAAU;IACvB,IAAII,QAAQ,IAAI,OAAOE,OAAO,GAAGF,MAAM,GAAG,EAAEE,KAAK,IAAI,CAAC,GAAG,GAAGF,MAAM,EAAE,CAAC;IACrE,OAAO,GAAGH,KAAKE,KAAK,CAACC,QAAQ,IAAI,KAAK,CAAC;AACzC;AAEA,OAAO,eAAeG,kBACpBC,OAAe,EACfC,MAA4B,EAC5BC,MAAc,EACdC,UAA4C,CAAC,CAAC;QAKlCA,cACSA;IAJrB,MAAMC,KAAKC,OAAOL,kBAAAA,UAAW,IAAIM,IAAI;IACrC,IAAI,CAACF,IAAI,OAAO;IAChB,IAAIlB,YAAYqB,GAAG,CAACH,QAAQH,QAAQ,OAAO;IAC3C,MAAMO,OAAML,eAAAA,QAAQK,GAAG,YAAXL,eAAeM,KAAKD,GAAG;IACnC,MAAME,QAAQL,QAAOF,iBAAAA,QAAQO,KAAK,YAAbP,iBAAiB,IAAIG,IAAI,MAAMF;IACpD,MAAMO,UAAUN,OAAOH,iBAAAA,SAAU,IAAII,IAAI,GAAGM,KAAK,CAAC,GAAG;IACrD,IAAI;QACF,MAAMC,KAAK,MAAMhC;QACjB,MAAMiC,MAAMD,GAAGE,UAAU,CAAC9B,4BAA4B+B,GAAG,CAACZ;QAC1D,MAAMa,SAAS,MAAMH,IAAIP,GAAG;QAC5B,IAAIU,OAAOC,MAAM,IAAID,OAAOV,GAAG,CAAC,cAAcN,QAAQ;YACpDf,YAAYiC,GAAG,CAACf,IAAIH;YACpB,OAAO;QACT;QACA,MAAMmB,OAAO,MAAMP,GAAGQ,cAAc,CAAC,OAAOC;;YAC1C,MAAMC,WAAW,MAAMD,YAAYf,GAAG,CAACO;YACvC,MAAMU,QAASD,SAASL,MAAM,GAAGK,SAASE,IAAI,KAAK;YAGnD,IAAID,CAAAA,yBAAAA,MAAOvB,MAAM,MAAKA,QAAQ;oBACauB;gBAAzC,OAAO;oBAAEE,YAAY;oBAAMC,YAAY,GAAEH,iBAAAA,MAAMI,OAAO,YAAbJ,iBAAiBhB;gBAAI;YAChE;YACA,MAAMqB,OAA+B;gBACnC7B,SAASI;gBACTM;gBACAT;gBACA2B,SAASpB;gBACTN,QAAQS;gBACRmB,aAAatB;YACf;YACAc,YAAYH,GAAG,CAACL,KAAKe;YACrB,sEAAsE;YACtE,iEAAiE;YACjE,6DAA6D;YAC7D,MAAMH,aACJzB,WAAW,aAAa,aAAauB,QAAQ,cAAc;YAC7D,OAAO;gBAAEE;gBAAYC,YAAY,UAAEH,yBAAAA,MAAOI,OAAO,mBAAIpB;YAAI;QAC3D;QACAtB,YAAYiC,GAAG,CAACf,IAAIH;QACpB,IAAImB,KAAKM,UAAU,EAAE;YACnB,MAAM,EAAEK,kBAAkB,EAAE,GAAG,MAAM,MAAM,CAAC;YAC5C,IAAIX,KAAKM,UAAU,KAAK,YAAY;gBAClC,MAAMK,mBAAmB,yBAAyB;oBAChDC,WAAW5B;oBACX6B,SAAS;wBAAEC,OAAOxB;wBAAOR,QAAQS;oBAAQ;gBAC3C;YACF,OAAO;gBACL,MAAMoB,mBAAmB,0BAA0B;oBACjDC,WAAW5B;oBACX6B,SAAS;wBACPC,OAAOxB;wBACPyB,UAAU7C,uBAAuBkB,MAAMY,KAAKO,YAAY;oBAC1D;gBACF;YACF;QACF;QACA,OAAOP,KAAKM,UAAU;IACxB,EAAE,OAAOU,OAAO;QACd,oEAAoE;QACpE,kEAAkE;QAClE,iCAAiC;QACjClD,YAAYiC,GAAG,CAACf,IAAIH;QACpBoC,QAAQD,KAAK,CAAC,CAAC,kBAAkB,EAAEhC,GAAG,sBAAsB,CAAC,EAAEgC;QAC/D,OAAO;IACT;AACF;AAEA,qEAAqE,GACrE,OAAO,eAAeE;IACpB,IAAI;QACF,MAAMf,WAAW,MAAM,AAAC,CAAA,MAAM1C,WAAU,EAAGkC,UAAU,CAAC9B,4BAA4BsB,GAAG;QACrF,OAAOgB,SAASgB,IAAI,CAACC,GAAG,CAAC,CAACxB,MAAaA,IAAIS,IAAI;IACjD,EAAE,OAAOW,OAAO;QACdC,QAAQD,KAAK,CAAC,uCAAuCA;QACrD,OAAO,EAAE;IACX;AACF;AAEA;;;CAGC,GACD,OAAO,SAASK,kBACdC,IAAoC,EACpCC,UAAyB;QAWTD;IAThB,IAAI,CAACA,MAAM;QACT,OAAO;YACLzC,QAAQ;YACRC,QACEyC,eAAe,OACX,iCACA,CAAC,sBAAsB,EAAEA,WAAW,uBAAuB,CAAC;QACpE;IACF;IACA,MAAMC,UAAUF,eAAAA,IAAI,CAAC,SAAS,YAAdA,eAAkB,CAAC;IACnC,MAAMG,UAAUC,OAAOC,OAAO,CAACH,QAC5BI,MAAM,CAAC,CAAC,GAAGd,MAAM,GAAKA,SAASA,KAAK,CAAC,KAAK,KAAK,MAC/CM,GAAG,CAAC,CAAC,CAACS,MAAMf,MAAM,GACjB,QAAOA,yBAAAA,KAAO,CAAC,OAAO,MAAK,WAAW,GAAGe,KAAK,EAAE,EAAEf,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,GAAGe;IAEzE,MAAMC,WACJR,IAAI,CAAC,SAAS,KAAK,cAClBA,IAAI,CAAC,SAAS,KAAK,QAASC,CAAAA,eAAe,OAAOE,QAAQM,MAAM,GAAG,CAAA;IACtE,OAAO;QACLlD,QAAQiD,WAAW,aAAa;QAChChD,QAAQgD,WACJL,QAAQM,MAAM,GACZ,CAAC,SAAS,EAAEN,QAAQO,IAAI,CAAC,MAAM,CAAC,CAAC,GACjC,CAAC,sBAAsB,EAAET,qBAAAA,aAAc,UAAU,CAAC,CAAC,GACrD;IACN;AACF;AAEA;;;CAGC,GACD,OAAO,MAAMU,wBAA0D;IACrEhB,SAAS;IACT,sBAAsB;IACtB,mBAAmB;IACnB,mBAAmB;IACnB,iBAAiB;IACjB,wBAAwB;IACxB,oBAAoB;IACpB,uBAAuB;IACvB,yBAAyB;IACzB,yBAAyB;IACzB,iBAAiB;IACjBiB,QAAQ;IACR,uBAAuB;IACvB,iBAAiB;IACjB,sBAAsB;IACtB,2BAA2B;AAC7B,EAAC;AAED;;;;;;CAMC,GACD,OAAO,eAAeC,qBAAqBC,QAAkB;IAC3D,IAAI;;QACF,MAAMd,OAAQ,MAAMc,SAASC,KAAK,GAAGC,IAAI;QACzC,MAAMC,UAAUtD,eAAOqC,wBAAAA,IAAM,CAAC,UAAU,mBAAI,IAAIpC,IAAI;QACpD,IAAIqD,SAAS;gBAGFN;YAFT,MAAM,EAAEpD,MAAM,EAAEC,MAAM,EAAE,GAAGuC,kBAAkBC,MAAMc,SAASvD,MAAM;YAClE,MAAMF,kBAAkB4D,SAAS1D,QAAQC,QAAQ;gBAC/CQ,KAAK,GAAE2C,iCAAAA,qBAAqB,CAACM,QAAQ,YAA9BN,iCAAkCM;YAC3C;QACF;IACF,EAAE,OAAOvB,OAAO;QACdC,QAAQD,KAAK,CAAC,6DAA6DA;IAC7E;IACA,OAAOoB;AACT"}
|
|
@@ -165,11 +165,17 @@ import { tenantDataTag, withRenderCache } from "../render-cache.js";
|
|
|
165
165
|
/**
|
|
166
166
|
* SECURITY-RELEVANT TTL (AGL-1302): the join drops revoked and taken-down
|
|
167
167
|
* versions, so this cache bounds how long a killed plugin keeps loading on
|
|
168
|
-
* published sites
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
|
|
168
|
+
* published sites when nothing announces the kill. Every write that changes
|
|
169
|
+
* the answer announces itself through `tenant-data:{hostId}`: a takedown or
|
|
170
|
+
* a revoked version through `dropCachesForListing`, an install or uninstall
|
|
171
|
+
* through the marketplace's `announcePinChange` (AGL-3741). So this is the
|
|
172
|
+
* backstop for a fan-out that failed or was capped, not the propagation —
|
|
173
|
+
* five minutes, where it was one, because at one minute it re-read every
|
|
174
|
+
* site's pins, listings, versions and revocations sixty times an hour. Only
|
|
175
|
+
* the HOST-scoped call (the tenant render path) is cached; the console's
|
|
176
|
+
* org-scoped call stays fresh so an install shows up in the workspace the
|
|
177
|
+
* moment it lands.
|
|
178
|
+
*/ const REALM_PLUGIN_INSTALLS_TTL_SECONDS = 300;
|
|
173
179
|
export async function getRealmPluginInstalls(options) {
|
|
174
180
|
const hostId = options.hostId;
|
|
175
181
|
if (!hostId || options.orgId) return resolveRealmPluginInstalls(options);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/realm-plugins.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\nimport {\n getPluginConfigSchema,\n isPluginRevoked,\n mergePluginConfig,\n readPluginContributions,\n resolvePluginConfig,\n type PluginContributions,\n type PluginRevocation,\n type RealmPluginInstall,\n} from '@aglyn/aglyn/server'\nimport { PLUGIN_DISTRIBUTION } from '@aglyn/aglyn/plugin-manager/plugin-distribution'\nimport { firebaseAdmin } from './firebase-admin'\nimport { resolveOrgIdForHost } from './organizations'\nimport { tenantDataTag, withRenderCache } from '../render-cache'\n\n/**\n * One `pluginSettings` document, or `undefined` if it cannot be read.\n *\n * A failed read degrades to \"no answer at this level\" rather than throwing,\n * so a transient Firestore error costs a handler its overrides and not its\n * request. The caller layers what comes back, so a missing site document and\n * a missing site override are the same thing to it.\n */\nasync function readPluginSettings(\n scope: 'orgs' | 'hosts',\n scopeId: string,\n pluginId: string,\n): Promise<Record<string, unknown> | undefined> {\n try {\n return (\n await firebaseAdmin\n .app()\n .firestore()\n .collection(scope)\n .doc(scopeId)\n .collection('pluginSettings')\n .doc(pluginId)\n .get()\n ).data()\n } catch {\n return undefined\n }\n}\n\n/**\n * Server-side plugin config read (AGL-428): the stored overrides merged over\n * the plugin's declared defaults (type-coerced — the docs are\n * manager-writable). Without a registered schema the raw doc (or {}) comes\n * back, so handlers degrade to their own fallbacks.\n *\n * `hostId` resolves the SITE's answer on top of the workspace's (AGL-428,\n * AGL-1014), and a handler that has one must pass it. A per-site setting the console can\n * store and the server never reads is not a setting: the operator lowers the\n * flagship branch's booking horizon, the console shows the number they typed,\n * and `/api/bookings/slots` keeps offering the workspace's — a disagreement\n * with no error and no surface that shows both sides.\n *\n * The site read is CONDITIONAL on being handed a host, so a request with no\n * site in scope pays nothing for a document that could not apply to it. It\n * runs alongside the org read rather than after it: the two documents are\n * independent, and sequencing them would put a second serial round trip on\n * every request that resolves config.\n */\nexport async function getPluginConfig(\n orgId: string | null | undefined,\n pluginId: string,\n options: { hostId?: string | null } = {},\n): Promise<Record<string, unknown>> {\n const schema = getPluginConfigSchema(pluginId)\n const hostId = options.hostId\n if (!orgId && !hostId) return schema ? mergePluginConfig(schema, null) : {}\n const [org, host] = await Promise.all([\n orgId ? readPluginSettings('orgs', orgId, pluginId) : undefined,\n hostId ? readPluginSettings('hosts', hostId, pluginId) : undefined,\n ])\n // Coercion happens once, at the end, inside `resolvePluginConfig` — a\n // malformed site value falls back to the WORKSPACE value it was trying to\n // replace rather than skipping past it to the schema default.\n if (schema) return resolvePluginConfig(schema, { org, host })\n // No schema: there is no field list to layer by, so the site document\n // simply narrows the workspace's, key for key.\n return { ...(org ?? {}), ...(host ?? {}) }\n}\n\n/**\n * Server-side resolution of a workspace's TRUSTED-REALM plugin installs\n * (AGL-420). Install docs pin `{version, sha256}`, but the trust grant\n * (`trust: 'realm'` + the platform Ed25519 `signature`) lives on the\n * server-only version doc — staff sign AFTER review, possibly long after\n * the install — so this join is the single source the loaders consume:\n *\n * 1. Read the org's installs (and the host's, when a host is in scope).\n * 2. Join each pin with its version document, in the store the\n * distributing plugin declares (`PLUGIN_DISTRIBUTION`, AGL-3080 — the\n * marketplace's listings today); only versions carrying `trust: 'realm'`\n * survive.\n * 3. Drop revoked versions (the kill switch keyed by the listing id) — a\n * revocation beats a still-present trust grant.\n * 4. Drop hidden listings (AGL-948) — `resolveMarketplacePluginVersion`\n * returns null for a listing under staff takedown. `deletedAt` is not\n * a blocker; see that function for why the two differ.\n *\n * The returned sha256/signature come from the VERSION doc, not the install\n * copy, so a tampered install doc cannot smuggle different bytes past the\n * loader's content check.\n */\n\ninterface InstallPin {\n listingId: string\n version: string\n}\n\n/**\n * One version-doc read, shaped for the remote loaders' `resolveVersion`.\n *\n * Also the staff-takedown gate (AGL-948): a listing carrying `hiddenAt`\n * resolves to null, so an already-installed plugin stops loading the\n * moment staff hide it. Takedown used to only de-list — a plugin pulled\n * for abuse kept executing in every workspace that had installed it until\n * someone separately wrote a revocation. It lives HERE rather than in the\n * join because every remote path funnels through this function: the\n * console and site realm joins, and both apps' remote-server-bundle\n * loaders.\n *\n * `deletedAt` is deliberately NOT a blocker. A publisher retiring a\n * listing must not break the sites already paying for it; unpublish\n * blocks new installs (`install-plugin`) and that is all it means.\n */\nexport async function resolveMarketplacePluginVersion(\n listingId: string,\n version: string,\n): Promise<{\n sha256: string\n signature?: string\n trust?: string\n hostAbi?: number\n pluginId?: string\n identity?: string\n contributes?: PluginContributions\n} | null> {\n // No distributing plugin declared a store: nothing can be vouched for, so\n // nothing resolves (see `plugin-distribution.ts` — absent is closed).\n const distribution = PLUGIN_DISTRIBUTION\n if (!distribution) return null\n const firestore = firebaseAdmin.app().firestore()\n const listingRef = firestore.collection(distribution.listings).doc(listingId)\n // THE REVOCATION READ IS IN THIS `Promise.all`, NOT AFTER IT (2026-08-26).\n //\n // It is keyed on `listingId` alone, so it never depended on the two reads\n // above — awaiting it below them cost a SECOND sequential Firestore round\n // trip on every resolution, and `resolveRealmPluginInstalls` calls this\n // once per pinned install. That is the shape that grows with the number of\n // plugins an org has installed: `/api/orgs/realm-plugins` is deliberately\n // uncached (see the TTL note below), and at a P75 of 907ms it was already\n // the slowest org-scoped console route before anyone uploaded in volume.\n //\n // The trade is one extra read on the paths that exit early (a hidden\n // listing, a missing version doc) in exchange for halving the round trips\n // on the path that actually happens. The kill switch is UNCHANGED: the\n // check below still runs before any value is returned, and it still reads\n // live — this moves when the read is issued, never whether it is honored.\n const [listing, snapshot, revocationSnapshot] = await Promise.all([\n listingRef.get(),\n listingRef.collection(distribution.versions).doc(version).get(),\n firestore.collection(distribution.revocations).doc(listingId).get(),\n ])\n // A missing listing doc is NOT a blocker: Firestore does not cascade to\n // subcollections, so a hard-deleted listing leaves working installs\n // resolving off an orphaned version doc. Only an explicit takedown stops\n // them.\n if (listing.get('hiddenAt')) return null\n const data = snapshot.data()\n if (!data?.sha256) return null\n // THE KILL SWITCH BELONGS AT THE CHOKEPOINT (AGL-2307).\n //\n // It used to live one level up, in `resolveRealmPluginInstalls` — which is\n // the path the tenant render and the console gate take, and NOT the path\n // remote SERVER bundles take. `loadRemoteServerBundles` resolves through\n // this function and nothing else, so a per-version revocation left a realm\n // server handler running: `hiddenAt` caught a full takedown, but a targeted\n // revocation deliberately does not hide the listing, which is the whole\n // difference between the two controls.\n //\n // Placed here rather than added as a second copy at the server loader, for\n // the reason this file's own comment gives about `hiddenAt`: this is the\n // one function every realm consumer funnels through, and a control that has\n // to be remembered per consumer is one that will be missed by the next one.\n const revocation = revocationSnapshot.data() as PluginRevocation | undefined\n if (isPluginRevoked(revocation, version)) return null\n const hostAbi = Number(data.manifest?.hostAbi)\n // Where the loaders may run this version (AGL-3116), from the manifest it\n // was published and reviewed with. A block that does not read is treated\n // as absent, so the documented default applies rather than \"nowhere\".\n const contributes = readPluginContributions(data.manifest?.contributes)\n const pluginId =\n typeof data.manifest?.id === 'string' && data.manifest.id\n ? String(data.manifest.id)\n : undefined\n // The namespace these bytes were published under (AGL-3390), which the\n // loaders hold every registration to. Absent on a version published before\n // identities existed, and such a version never server-renders.\n const identity =\n typeof data.identity === 'string' && data.identity\n ? String(data.identity)\n : undefined\n return {\n sha256: String(data.sha256),\n ...(data.signature ? { signature: String(data.signature) } : {}),\n ...(data.trust ? { trust: String(data.trust) } : {}),\n ...(Number.isInteger(hostAbi) && hostAbi > 0 ? { hostAbi } : {}),\n ...(pluginId ? { pluginId } : {}),\n ...(identity ? { identity } : {}),\n ...(contributes ? { contributes } : {}),\n }\n}\n\n/**\n * SECURITY-RELEVANT TTL (AGL-1302): the join drops revoked and taken-down\n * versions, so this cache bounds how long a killed plugin keeps loading on\n * published sites — 60s, on par with the page ISR window that already\n * bounded it. Only the HOST-scoped call (the tenant render path) is cached;\n * the console's org-scoped call stays fresh so an install shows up in the\n * workspace the moment it lands.\n */\nconst REALM_PLUGIN_INSTALLS_TTL_SECONDS = 60\n\nexport async function getRealmPluginInstalls(options: {\n orgId?: string\n hostId?: string\n}): Promise<RealmPluginInstall[]> {\n const hostId = options.hostId\n if (!hostId || options.orgId) return resolveRealmPluginInstalls(options)\n try {\n return await withRenderCache({\n key: ['tenant-realm-plugins', hostId],\n revalidate: REALM_PLUGIN_INSTALLS_TTL_SECONDS,\n tags: [tenantDataTag(hostId)],\n read: () => resolveRealmPluginInstalls(options),\n })\n } catch (error) {\n console.error(error)\n return resolveRealmPluginInstalls(options)\n }\n}\n\nasync function resolveRealmPluginInstalls(options: {\n orgId?: string\n hostId?: string\n}): Promise<RealmPluginInstall[]> {\n const firestore = firebaseAdmin.app().firestore()\n const orgId =\n options.orgId ??\n (options.hostId ? await resolveOrgIdForHost(options.hostId) : null)\n\n // Switchboard gate (AGL-424): a workspace with an EXPLICIT\n // `enabledPlugins` list only realm-loads listings on it (install sync\n // keeps the list in step; toggling one off disables it without\n // uninstalling). Absent field = default-open, so pre-switchboard\n // installs keep loading.\n let enabledFilter: readonly string[] | null = null\n if (orgId) {\n const configured = (\n await firestore.collection('orgs').doc(orgId).get()\n ).get('enabledPlugins')\n if (Array.isArray(configured)) enabledFilter = configured.map(String)\n }\n\n const pins = new Map<string, InstallPin>()\n const collect = async (\n ref: FirebaseFirestore.CollectionReference,\n ): Promise<void> => {\n const snapshot = await ref.get()\n for (const doc of snapshot.docs) {\n const version = doc.get('version')\n if (typeof version === 'string' && version) {\n pins.set(doc.id, { listingId: doc.id, version })\n }\n }\n }\n if (orgId) {\n await collect(firestore.collection('orgs').doc(orgId).collection('installs'))\n }\n if (options.hostId) {\n // Host pins win over org pins for the same listing (more specific).\n await collect(\n firestore.collection('hosts').doc(options.hostId).collection('installs'),\n )\n }\n if (enabledFilter) {\n for (const listingId of [...pins.keys()]) {\n if (!enabledFilter.includes(listingId)) pins.delete(listingId)\n }\n }\n if (!pins.size) return []\n\n const installs = await Promise.all(\n [...pins.values()].map(async (pin): Promise<RealmPluginInstall | null> => {\n const pinned = await resolveMarketplacePluginVersion(\n pin.listingId,\n pin.version,\n )\n // The revocation check moved INTO `resolveMarketplacePluginVersion`\n // (AGL-2307) so the remote-server loader — which resolves through that\n // function and never reaches this join — gets it too. A `null` here\n // therefore already means \"taken down, unknown, or killed\".\n if (!pinned || pinned.trust !== 'realm' || !pinned.signature) return null\n return {\n listingId: pin.listingId,\n version: pin.version,\n sha256: pinned.sha256,\n trust: 'realm',\n signature: pinned.signature,\n ...(pinned.hostAbi !== undefined ? { hostAbi: pinned.hostAbi } : {}),\n ...(pinned.pluginId ? { pluginId: pinned.pluginId } : {}),\n ...(pinned.identity ? { identity: pinned.identity } : {}),\n ...(pinned.contributes ? { contributes: pinned.contributes } : {}),\n }\n }),\n )\n return installs.filter((install): install is RealmPluginInstall =>\n Boolean(install),\n )\n}\n"],"names":["getPluginConfigSchema","isPluginRevoked","mergePluginConfig","readPluginContributions","resolvePluginConfig","PLUGIN_DISTRIBUTION","firebaseAdmin","resolveOrgIdForHost","tenantDataTag","withRenderCache","readPluginSettings","scope","scopeId","pluginId","app","firestore","collection","doc","get","data","undefined","getPluginConfig","orgId","options","schema","hostId","org","host","Promise","all","resolveMarketplacePluginVersion","listingId","version","distribution","listingRef","listings","listing","snapshot","revocationSnapshot","versions","revocations","sha256","revocation","hostAbi","Number","manifest","contributes","id","String","identity","signature","trust","isInteger","REALM_PLUGIN_INSTALLS_TTL_SECONDS","getRealmPluginInstalls","resolveRealmPluginInstalls","key","revalidate","tags","read","error","console","enabledFilter","configured","Array","isArray","map","pins","Map","collect","ref","docs","set","keys","includes","delete","size","installs","values","pin","pinned","filter","install","Boolean"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/realm-plugins.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\nimport {\n getPluginConfigSchema,\n isPluginRevoked,\n mergePluginConfig,\n readPluginContributions,\n resolvePluginConfig,\n type PluginContributions,\n type PluginRevocation,\n type RealmPluginInstall,\n} from '@aglyn/aglyn/server'\nimport { PLUGIN_DISTRIBUTION } from '@aglyn/aglyn/plugin-manager/plugin-distribution'\nimport { firebaseAdmin } from './firebase-admin'\nimport { resolveOrgIdForHost } from './organizations'\nimport { tenantDataTag, withRenderCache } from '../render-cache'\n\n/**\n * One `pluginSettings` document, or `undefined` if it cannot be read.\n *\n * A failed read degrades to \"no answer at this level\" rather than throwing,\n * so a transient Firestore error costs a handler its overrides and not its\n * request. The caller layers what comes back, so a missing site document and\n * a missing site override are the same thing to it.\n */\nasync function readPluginSettings(\n scope: 'orgs' | 'hosts',\n scopeId: string,\n pluginId: string,\n): Promise<Record<string, unknown> | undefined> {\n try {\n return (\n await firebaseAdmin\n .app()\n .firestore()\n .collection(scope)\n .doc(scopeId)\n .collection('pluginSettings')\n .doc(pluginId)\n .get()\n ).data()\n } catch {\n return undefined\n }\n}\n\n/**\n * Server-side plugin config read (AGL-428): the stored overrides merged over\n * the plugin's declared defaults (type-coerced — the docs are\n * manager-writable). Without a registered schema the raw doc (or {}) comes\n * back, so handlers degrade to their own fallbacks.\n *\n * `hostId` resolves the SITE's answer on top of the workspace's (AGL-428,\n * AGL-1014), and a handler that has one must pass it. A per-site setting the console can\n * store and the server never reads is not a setting: the operator lowers the\n * flagship branch's booking horizon, the console shows the number they typed,\n * and `/api/bookings/slots` keeps offering the workspace's — a disagreement\n * with no error and no surface that shows both sides.\n *\n * The site read is CONDITIONAL on being handed a host, so a request with no\n * site in scope pays nothing for a document that could not apply to it. It\n * runs alongside the org read rather than after it: the two documents are\n * independent, and sequencing them would put a second serial round trip on\n * every request that resolves config.\n */\nexport async function getPluginConfig(\n orgId: string | null | undefined,\n pluginId: string,\n options: { hostId?: string | null } = {},\n): Promise<Record<string, unknown>> {\n const schema = getPluginConfigSchema(pluginId)\n const hostId = options.hostId\n if (!orgId && !hostId) return schema ? mergePluginConfig(schema, null) : {}\n const [org, host] = await Promise.all([\n orgId ? readPluginSettings('orgs', orgId, pluginId) : undefined,\n hostId ? readPluginSettings('hosts', hostId, pluginId) : undefined,\n ])\n // Coercion happens once, at the end, inside `resolvePluginConfig` — a\n // malformed site value falls back to the WORKSPACE value it was trying to\n // replace rather than skipping past it to the schema default.\n if (schema) return resolvePluginConfig(schema, { org, host })\n // No schema: there is no field list to layer by, so the site document\n // simply narrows the workspace's, key for key.\n return { ...(org ?? {}), ...(host ?? {}) }\n}\n\n/**\n * Server-side resolution of a workspace's TRUSTED-REALM plugin installs\n * (AGL-420). Install docs pin `{version, sha256}`, but the trust grant\n * (`trust: 'realm'` + the platform Ed25519 `signature`) lives on the\n * server-only version doc — staff sign AFTER review, possibly long after\n * the install — so this join is the single source the loaders consume:\n *\n * 1. Read the org's installs (and the host's, when a host is in scope).\n * 2. Join each pin with its version document, in the store the\n * distributing plugin declares (`PLUGIN_DISTRIBUTION`, AGL-3080 — the\n * marketplace's listings today); only versions carrying `trust: 'realm'`\n * survive.\n * 3. Drop revoked versions (the kill switch keyed by the listing id) — a\n * revocation beats a still-present trust grant.\n * 4. Drop hidden listings (AGL-948) — `resolveMarketplacePluginVersion`\n * returns null for a listing under staff takedown. `deletedAt` is not\n * a blocker; see that function for why the two differ.\n *\n * The returned sha256/signature come from the VERSION doc, not the install\n * copy, so a tampered install doc cannot smuggle different bytes past the\n * loader's content check.\n */\n\ninterface InstallPin {\n listingId: string\n version: string\n}\n\n/**\n * One version-doc read, shaped for the remote loaders' `resolveVersion`.\n *\n * Also the staff-takedown gate (AGL-948): a listing carrying `hiddenAt`\n * resolves to null, so an already-installed plugin stops loading the\n * moment staff hide it. Takedown used to only de-list — a plugin pulled\n * for abuse kept executing in every workspace that had installed it until\n * someone separately wrote a revocation. It lives HERE rather than in the\n * join because every remote path funnels through this function: the\n * console and site realm joins, and both apps' remote-server-bundle\n * loaders.\n *\n * `deletedAt` is deliberately NOT a blocker. A publisher retiring a\n * listing must not break the sites already paying for it; unpublish\n * blocks new installs (`install-plugin`) and that is all it means.\n */\nexport async function resolveMarketplacePluginVersion(\n listingId: string,\n version: string,\n): Promise<{\n sha256: string\n signature?: string\n trust?: string\n hostAbi?: number\n pluginId?: string\n identity?: string\n contributes?: PluginContributions\n} | null> {\n // No distributing plugin declared a store: nothing can be vouched for, so\n // nothing resolves (see `plugin-distribution.ts` — absent is closed).\n const distribution = PLUGIN_DISTRIBUTION\n if (!distribution) return null\n const firestore = firebaseAdmin.app().firestore()\n const listingRef = firestore.collection(distribution.listings).doc(listingId)\n // THE REVOCATION READ IS IN THIS `Promise.all`, NOT AFTER IT (2026-08-26).\n //\n // It is keyed on `listingId` alone, so it never depended on the two reads\n // above — awaiting it below them cost a SECOND sequential Firestore round\n // trip on every resolution, and `resolveRealmPluginInstalls` calls this\n // once per pinned install. That is the shape that grows with the number of\n // plugins an org has installed: `/api/orgs/realm-plugins` is deliberately\n // uncached (see the TTL note below), and at a P75 of 907ms it was already\n // the slowest org-scoped console route before anyone uploaded in volume.\n //\n // The trade is one extra read on the paths that exit early (a hidden\n // listing, a missing version doc) in exchange for halving the round trips\n // on the path that actually happens. The kill switch is UNCHANGED: the\n // check below still runs before any value is returned, and it still reads\n // live — this moves when the read is issued, never whether it is honored.\n const [listing, snapshot, revocationSnapshot] = await Promise.all([\n listingRef.get(),\n listingRef.collection(distribution.versions).doc(version).get(),\n firestore.collection(distribution.revocations).doc(listingId).get(),\n ])\n // A missing listing doc is NOT a blocker: Firestore does not cascade to\n // subcollections, so a hard-deleted listing leaves working installs\n // resolving off an orphaned version doc. Only an explicit takedown stops\n // them.\n if (listing.get('hiddenAt')) return null\n const data = snapshot.data()\n if (!data?.sha256) return null\n // THE KILL SWITCH BELONGS AT THE CHOKEPOINT (AGL-2307).\n //\n // It used to live one level up, in `resolveRealmPluginInstalls` — which is\n // the path the tenant render and the console gate take, and NOT the path\n // remote SERVER bundles take. `loadRemoteServerBundles` resolves through\n // this function and nothing else, so a per-version revocation left a realm\n // server handler running: `hiddenAt` caught a full takedown, but a targeted\n // revocation deliberately does not hide the listing, which is the whole\n // difference between the two controls.\n //\n // Placed here rather than added as a second copy at the server loader, for\n // the reason this file's own comment gives about `hiddenAt`: this is the\n // one function every realm consumer funnels through, and a control that has\n // to be remembered per consumer is one that will be missed by the next one.\n const revocation = revocationSnapshot.data() as PluginRevocation | undefined\n if (isPluginRevoked(revocation, version)) return null\n const hostAbi = Number(data.manifest?.hostAbi)\n // Where the loaders may run this version (AGL-3116), from the manifest it\n // was published and reviewed with. A block that does not read is treated\n // as absent, so the documented default applies rather than \"nowhere\".\n const contributes = readPluginContributions(data.manifest?.contributes)\n const pluginId =\n typeof data.manifest?.id === 'string' && data.manifest.id\n ? String(data.manifest.id)\n : undefined\n // The namespace these bytes were published under (AGL-3390), which the\n // loaders hold every registration to. Absent on a version published before\n // identities existed, and such a version never server-renders.\n const identity =\n typeof data.identity === 'string' && data.identity\n ? String(data.identity)\n : undefined\n return {\n sha256: String(data.sha256),\n ...(data.signature ? { signature: String(data.signature) } : {}),\n ...(data.trust ? { trust: String(data.trust) } : {}),\n ...(Number.isInteger(hostAbi) && hostAbi > 0 ? { hostAbi } : {}),\n ...(pluginId ? { pluginId } : {}),\n ...(identity ? { identity } : {}),\n ...(contributes ? { contributes } : {}),\n }\n}\n\n/**\n * SECURITY-RELEVANT TTL (AGL-1302): the join drops revoked and taken-down\n * versions, so this cache bounds how long a killed plugin keeps loading on\n * published sites when nothing announces the kill. Every write that changes\n * the answer announces itself through `tenant-data:{hostId}`: a takedown or\n * a revoked version through `dropCachesForListing`, an install or uninstall\n * through the marketplace's `announcePinChange` (AGL-3741). So this is the\n * backstop for a fan-out that failed or was capped, not the propagation —\n * five minutes, where it was one, because at one minute it re-read every\n * site's pins, listings, versions and revocations sixty times an hour. Only\n * the HOST-scoped call (the tenant render path) is cached; the console's\n * org-scoped call stays fresh so an install shows up in the workspace the\n * moment it lands.\n */\nconst REALM_PLUGIN_INSTALLS_TTL_SECONDS = 300\n\nexport async function getRealmPluginInstalls(options: {\n orgId?: string\n hostId?: string\n}): Promise<RealmPluginInstall[]> {\n const hostId = options.hostId\n if (!hostId || options.orgId) return resolveRealmPluginInstalls(options)\n try {\n return await withRenderCache({\n key: ['tenant-realm-plugins', hostId],\n revalidate: REALM_PLUGIN_INSTALLS_TTL_SECONDS,\n tags: [tenantDataTag(hostId)],\n read: () => resolveRealmPluginInstalls(options),\n })\n } catch (error) {\n console.error(error)\n return resolveRealmPluginInstalls(options)\n }\n}\n\nasync function resolveRealmPluginInstalls(options: {\n orgId?: string\n hostId?: string\n}): Promise<RealmPluginInstall[]> {\n const firestore = firebaseAdmin.app().firestore()\n const orgId =\n options.orgId ??\n (options.hostId ? await resolveOrgIdForHost(options.hostId) : null)\n\n // Switchboard gate (AGL-424): a workspace with an EXPLICIT\n // `enabledPlugins` list only realm-loads listings on it (install sync\n // keeps the list in step; toggling one off disables it without\n // uninstalling). Absent field = default-open, so pre-switchboard\n // installs keep loading.\n let enabledFilter: readonly string[] | null = null\n if (orgId) {\n const configured = (\n await firestore.collection('orgs').doc(orgId).get()\n ).get('enabledPlugins')\n if (Array.isArray(configured)) enabledFilter = configured.map(String)\n }\n\n const pins = new Map<string, InstallPin>()\n const collect = async (\n ref: FirebaseFirestore.CollectionReference,\n ): Promise<void> => {\n const snapshot = await ref.get()\n for (const doc of snapshot.docs) {\n const version = doc.get('version')\n if (typeof version === 'string' && version) {\n pins.set(doc.id, { listingId: doc.id, version })\n }\n }\n }\n if (orgId) {\n await collect(firestore.collection('orgs').doc(orgId).collection('installs'))\n }\n if (options.hostId) {\n // Host pins win over org pins for the same listing (more specific).\n await collect(\n firestore.collection('hosts').doc(options.hostId).collection('installs'),\n )\n }\n if (enabledFilter) {\n for (const listingId of [...pins.keys()]) {\n if (!enabledFilter.includes(listingId)) pins.delete(listingId)\n }\n }\n if (!pins.size) return []\n\n const installs = await Promise.all(\n [...pins.values()].map(async (pin): Promise<RealmPluginInstall | null> => {\n const pinned = await resolveMarketplacePluginVersion(\n pin.listingId,\n pin.version,\n )\n // The revocation check moved INTO `resolveMarketplacePluginVersion`\n // (AGL-2307) so the remote-server loader — which resolves through that\n // function and never reaches this join — gets it too. A `null` here\n // therefore already means \"taken down, unknown, or killed\".\n if (!pinned || pinned.trust !== 'realm' || !pinned.signature) return null\n return {\n listingId: pin.listingId,\n version: pin.version,\n sha256: pinned.sha256,\n trust: 'realm',\n signature: pinned.signature,\n ...(pinned.hostAbi !== undefined ? { hostAbi: pinned.hostAbi } : {}),\n ...(pinned.pluginId ? { pluginId: pinned.pluginId } : {}),\n ...(pinned.identity ? { identity: pinned.identity } : {}),\n ...(pinned.contributes ? { contributes: pinned.contributes } : {}),\n }\n }),\n )\n return installs.filter((install): install is RealmPluginInstall =>\n Boolean(install),\n )\n}\n"],"names":["getPluginConfigSchema","isPluginRevoked","mergePluginConfig","readPluginContributions","resolvePluginConfig","PLUGIN_DISTRIBUTION","firebaseAdmin","resolveOrgIdForHost","tenantDataTag","withRenderCache","readPluginSettings","scope","scopeId","pluginId","app","firestore","collection","doc","get","data","undefined","getPluginConfig","orgId","options","schema","hostId","org","host","Promise","all","resolveMarketplacePluginVersion","listingId","version","distribution","listingRef","listings","listing","snapshot","revocationSnapshot","versions","revocations","sha256","revocation","hostAbi","Number","manifest","contributes","id","String","identity","signature","trust","isInteger","REALM_PLUGIN_INSTALLS_TTL_SECONDS","getRealmPluginInstalls","resolveRealmPluginInstalls","key","revalidate","tags","read","error","console","enabledFilter","configured","Array","isArray","map","pins","Map","collect","ref","docs","set","keys","includes","delete","size","installs","values","pin","pinned","filter","install","Boolean"],"mappings":"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"}
|
|
@@ -143,6 +143,12 @@ export declare function originOf(value: string | undefined | null): string;
|
|
|
143
143
|
* published site to watch. Values that are not origins are dropped.
|
|
144
144
|
*/
|
|
145
145
|
export declare function resolveRenderMonitorTargets(env: Readonly<Record<string, string | undefined>>): string[];
|
|
146
|
+
/**
|
|
147
|
+
* Whether the target at `index` (of `count`, in
|
|
148
|
+
* {@link resolveRenderMonitorTargets} order) draws its layout probes in the
|
|
149
|
+
* five-minute `slot`: the first always, the rest one per slot in turn.
|
|
150
|
+
*/
|
|
151
|
+
export declare function renderMonitorProbesLayout(index: number, count: number, slot: number): boolean;
|
|
146
152
|
/** The state document id for one origin. */
|
|
147
153
|
export declare function renderMonitorCheckId(origin: string): string;
|
|
148
154
|
/** The state document id for one host, as {@link renderMonitorCheckId} names it. */
|
|
@@ -86,6 +86,17 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
86
86
|
* One page per site per run, rotating through that site's list: a platform
|
|
87
87
|
* break fails every page with an image, so every run, and alerts as fast as
|
|
88
88
|
* before; a break on one page alone is seen on its turn.
|
|
89
|
+
*
|
|
90
|
+
* ## Layout probes take turns past the first site
|
|
91
|
+
*
|
|
92
|
+
* A fresh layout reads the site's whole screen and collection lists, and a
|
|
93
|
+
* large site makes that the most expensive request the platform serves: the
|
|
94
|
+
* marketing site's 109 screens made this monitor a fifth of the project's
|
|
95
|
+
* Firestore reads. The first target — the demonstration site — draws both
|
|
96
|
+
* layout probes on every run, and a platform-wide render break fails it
|
|
97
|
+
* first; every other site draws them on its turn, one site per run (see
|
|
98
|
+
* {@link renderMonitorProbesLayout}). `page` probes still run on every run,
|
|
99
|
+
* so the page-body break they exist for is seen as fast as before.
|
|
89
100
|
*/ /** The state documents' collection: the health checks' own. */ const RENDER_MONITOR_COLLECTION = 'operatorHealthState';
|
|
90
101
|
/** Every render monitor document id starts with this. */ export const RENDER_MONITOR_CHECK_PREFIX = 'render-monitor--';
|
|
91
102
|
/** The beat id `/api/health/crons` reads for the scheduled run. */ export const RENDER_MONITOR_JOB_ID = 'render-monitor';
|
|
@@ -342,6 +353,14 @@ function spellingLetters(host) {
|
|
|
342
353
|
].filter(Boolean))
|
|
343
354
|
];
|
|
344
355
|
}
|
|
356
|
+
/**
|
|
357
|
+
* Whether the target at `index` (of `count`, in
|
|
358
|
+
* {@link resolveRenderMonitorTargets} order) draws its layout probes in the
|
|
359
|
+
* five-minute `slot`: the first always, the rest one per slot in turn.
|
|
360
|
+
*/ export function renderMonitorProbesLayout(index, count, slot) {
|
|
361
|
+
if (index <= 0 || count <= 2) return true;
|
|
362
|
+
return Math.abs(slot) % (count - 1) === index - 1;
|
|
363
|
+
}
|
|
345
364
|
/** The state document id for one origin. */ export function renderMonitorCheckId(origin) {
|
|
346
365
|
return renderMonitorHostCheckId(new URL(origin).host);
|
|
347
366
|
}
|
|
@@ -539,9 +558,15 @@ function probeLine(row) {
|
|
|
539
558
|
const canRenderPages = Boolean(renderOrigin && probeOptions.headers['x-vercel-protection-bypass']);
|
|
540
559
|
const runAt = clock();
|
|
541
560
|
const slot = Math.floor(runAt / 300000);
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
|
|
561
|
+
const targets = resolveRenderMonitorTargets(env);
|
|
562
|
+
const due = targets.map((origin, index)=>({
|
|
563
|
+
origin,
|
|
564
|
+
layout: renderMonitorProbesLayout(index, targets.length, slot),
|
|
565
|
+
site: canRenderPages ? pages.find((page)=>page.origin === origin) : undefined
|
|
566
|
+
}))// A site with nothing to draw this run keeps its state as it is.
|
|
567
|
+
.filter((target)=>target.layout || target.site);
|
|
568
|
+
const sites = await Promise.all(due.map(async ({ origin, layout, site })=>{
|
|
569
|
+
const layoutProbes = layout ? RENDER_PROBES.map((spec)=>runProbe(`${origin}${spec.path(nonce())}`, spec, probeOptions)) : [];
|
|
545
570
|
const pageProbe = site ? [
|
|
546
571
|
runProbe(pageProbeUrl(renderOrigin, site, slot, renderSpellingNumber(runAt)), {
|
|
547
572
|
kind: 'page',
|