@aglyn/aglyn 1.0.0-beta.264 → 1.0.0-beta.268

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 (45) hide show
  1. package/package.json +12 -12
  2. package/src/lib/app-utils/account-acquisition.d.ts +13 -0
  3. package/src/lib/app-utils/account-acquisition.js +84 -0
  4. package/src/lib/app-utils/account-acquisition.js.map +1 -1
  5. package/src/lib/app-utils/analytics-events.d.ts +70 -0
  6. package/src/lib/app-utils/analytics-events.js +8 -0
  7. package/src/lib/app-utils/analytics-events.js.map +1 -1
  8. package/src/lib/app-utils/docs-help-sections.generated.js +4 -1
  9. package/src/lib/app-utils/docs-help-sections.generated.js.map +1 -1
  10. package/src/lib/app-utils/docs-help.generated.d.ts +2 -2
  11. package/src/lib/app-utils/docs-help.generated.js +8 -1
  12. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  13. package/src/lib/app-utils/docs-index.generated.js +31 -10
  14. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  15. package/src/lib/app-utils/expand-repeatables.js +14 -7
  16. package/src/lib/app-utils/expand-repeatables.js.map +1 -1
  17. package/src/lib/app-utils/health-report.d.ts +120 -8
  18. package/src/lib/app-utils/health-report.js +138 -3
  19. package/src/lib/app-utils/health-report.js.map +1 -1
  20. package/src/lib/app-utils/live-document-count.client.d.ts +28 -0
  21. package/src/lib/app-utils/live-document-count.client.js +35 -0
  22. package/src/lib/app-utils/live-document-count.client.js.map +1 -0
  23. package/src/lib/app-utils/live-document-count.d.ts +91 -0
  24. package/src/lib/app-utils/live-document-count.js +81 -0
  25. package/src/lib/app-utils/live-document-count.js.map +1 -0
  26. package/src/lib/app-utils/notification-sounds.d.ts +49 -0
  27. package/src/lib/app-utils/notification-sounds.js +54 -0
  28. package/src/lib/app-utils/notification-sounds.js.map +1 -0
  29. package/src/lib/app-utils/notifications.d.ts +2 -0
  30. package/src/lib/app-utils/notifications.js +38 -10
  31. package/src/lib/app-utils/notifications.js.map +1 -1
  32. package/src/lib/app-utils/screen-analytics-aggregate.d.ts +10 -0
  33. package/src/lib/app-utils/screen-analytics-aggregate.js +11 -0
  34. package/src/lib/app-utils/screen-analytics-aggregate.js.map +1 -1
  35. package/src/lib/app-utils/upload-inspection.d.ts +9 -0
  36. package/src/lib/app-utils/upload-inspection.js +27 -0
  37. package/src/lib/app-utils/upload-inspection.js.map +1 -1
  38. package/src/lib/plugin-manager/first-party-plugins.generated.js +6 -0
  39. package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
  40. package/src/lib/plugin-manager/plugin-activity-actions.d.ts +14 -0
  41. package/src/lib/plugin-manager/plugin-activity-actions.js +30 -3
  42. package/src/lib/plugin-manager/plugin-activity-actions.js.map +1 -1
  43. package/src/lib/plugin-manager/plugin-site-erase.d.ts +81 -0
  44. package/src/lib/plugin-manager/plugin-site-erase.js +56 -0
  45. package/src/lib/plugin-manager/plugin-site-erase.js.map +1 -0
@@ -127,6 +127,14 @@ export interface PluginActivityRegistration {
127
127
  * type needs no entry — the feed reads its noun off the type itself.
128
128
  */
129
129
  targetTypes?: Readonly<Record<string, string>>;
130
+ /**
131
+ * Words for the plugin's rows that store a SENTENCE rather than a code
132
+ * (AGL-3892): rows its writers filed before codes existed, or still file as
133
+ * prose (`Action ran on formSubmission`). Answers what a person reads for
134
+ * such an action, or `undefined` for one that is not the plugin's. Pure and
135
+ * cheap: every feed row asks every plugin that declares one.
136
+ */
137
+ describeAction?: (action: string) => string | undefined;
130
138
  }
131
139
  /**
132
140
  * The most codes one group may hold. A group's chip asks `action is any of`
@@ -169,6 +177,12 @@ export declare function pluginStaffAuditCollection(collection: string): {
169
177
  export declare function isPluginJobTargetType(type: unknown): boolean;
170
178
  /** The readable label for a declared code; `undefined` for any other action. */
171
179
  export declare function pluginActivityActionLabel(action: unknown): string | undefined;
180
+ /**
181
+ * What a plugin's `describeAction` says of a stored sentence (AGL-3892), or
182
+ * `undefined` when no plugin claims it. A describer that throws is skipped,
183
+ * so one plugin's bug never blanks another's rows.
184
+ */
185
+ export declare function pluginActivityDescribedAction(action: unknown): string | undefined;
172
186
  /** The group a stored action's code belongs to, or `undefined`. */
173
187
  export declare function pluginActivityGroupForAction(action: unknown): PluginActivityGroup | undefined;
174
188
  /**
@@ -28,7 +28,8 @@
28
28
  * Declared at module scope from both the client barrel and the `/server`
29
29
  * entry (the AGL-428 pattern), with an explicit `pluginId`: module scope
30
30
  * runs before the loader's ownership marker is set.
31
- */ /** A namespaced type as the log stores it: `pluginId:noun`, both parts plain. */ const PLUGIN_TARGET_TYPE = /^([a-z][a-z0-9-]*):([A-Za-z][A-Za-z0-9]*)$/;
31
+ */ var _globalScope_PLUGIN_ACTIVITY_ACTIONS_KEY;
32
+ /** A namespaced type as the log stores it: `pluginId:noun`, both parts plain. */ const PLUGIN_TARGET_TYPE = /^([a-z][a-z0-9-]*):([A-Za-z][A-Za-z0-9]*)$/;
32
33
  /** Whether a stored target type is a plugin's namespaced one. */ export function isPluginActivityTargetType(type) {
33
34
  return typeof type === 'string' && PLUGIN_TARGET_TYPE.test(type);
34
35
  }
@@ -40,7 +41,15 @@
40
41
  if (typeof type !== 'string') return undefined;
41
42
  return (_PLUGIN_TARGET_TYPE_exec = PLUGIN_TARGET_TYPE.exec(type)) == null ? void 0 : _PLUGIN_TARGET_TYPE_exec[2];
42
43
  }
43
- const registrations = new Map();
44
+ /**
45
+ * The registrations live on `globalThis` under `Symbol.for`: each app's boot
46
+ * (`instrumentation.ts`) declares them and its routes read them, and Next
47
+ * compiles those apart, so a module-scoped map would be one copy per module
48
+ * graph: boot would fill its copy and a route would read an empty one
49
+ * (AGL-3412, AGL-3868).
50
+ */ const PLUGIN_ACTIVITY_ACTIONS_KEY = Symbol.for('@aglyn/aglyn:plugin-activity-actions');
51
+ const globalScope = globalThis;
52
+ const registrations = (_globalScope_PLUGIN_ACTIVITY_ACTIONS_KEY = globalScope[PLUGIN_ACTIVITY_ACTIONS_KEY]) != null ? _globalScope_PLUGIN_ACTIVITY_ACTIONS_KEY : globalScope[PLUGIN_ACTIVITY_ACTIONS_KEY] = new Map();
44
53
  /**
45
54
  * The most codes one group may hold. A group's chip asks `action is any of`
46
55
  * its codes, which the feeds' queries serve as one Firestore `in` — capped at
@@ -158,6 +167,23 @@ function ownerOfActivityAction(key) {
158
167
  const match = registration.actions.find((entry)=>entry.key === action);
159
168
  if (match) return match.label;
160
169
  }
170
+ return pluginActivityDescribedAction(action);
171
+ }
172
+ /**
173
+ * What a plugin's `describeAction` says of a stored sentence (AGL-3892), or
174
+ * `undefined` when no plugin claims it. A describer that throws is skipped,
175
+ * so one plugin's bug never blanks another's rows.
176
+ */ export function pluginActivityDescribedAction(action) {
177
+ if (typeof action !== 'string' || !action.trim()) return undefined;
178
+ for (const registration of registrations.values()){
179
+ if (!registration.describeAction) continue;
180
+ try {
181
+ const words = registration.describeAction(action);
182
+ if (words) return words;
183
+ } catch (unused) {
184
+ // A describer that throws describes nothing.
185
+ }
186
+ }
161
187
  return undefined;
162
188
  }
163
189
  /** The group a stored action's code belongs to, or `undefined`. */ export function pluginActivityGroupForAction(action) {
@@ -173,7 +199,8 @@ function ownerOfActivityAction(key) {
173
199
  * The filter chips the feeds offer: one per registered group, each with the
174
200
  * codes its chip keeps. What an `action isAnyOf …` request sends.
175
201
  */ export function listPluginActivityFilters() {
176
- return listPluginActivityRegistrations().map((entry)=>({
202
+ // A plugin that only describes prose rows (no codes) has no chip: its chip could keep nothing.
203
+ return listPluginActivityRegistrations().filter((entry)=>entry.actions.length > 0).map((entry)=>({
177
204
  group: entry.group,
178
205
  actions: entry.actions.map((action)=>action.key)
179
206
  }));
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-activity-actions.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 * Plugin-declared activity actions (AGL-2940).\n *\n * Most rows in `orgs/{orgId}/activity` and `hosts/{hostId}/activity` store\n * a prose action written at the call site. A plugin whose rows are read by\n * more than a person — a filter chip, a staff facet, an actor table — stores\n * a CODE (`ai.job.output`) and declares here what the code means: the label\n * a reader sees, the scope it is written at, and the group its filter chip\n * belongs to. The activity renderers read this catalog for labels and\n * filters, so a second plugin with coded rows gets a chip and a facet group\n * by registering, with no edit to the renderers.\n *\n * Declared at module scope from both the client barrel and the `/server`\n * entry (the AGL-428 pattern), with an explicit `pluginId`: module scope\n * runs before the loader's ownership marker is set.\n */\n\nexport type PluginActivityScope = 'org' | 'host' | 'staff'\n\n/**\n * A resource a PLUGIN files an org activity row under (AGL-2978): the\n * plugin's id, a colon and the plugin's own noun — `acme-mail:mailbox`.\n *\n * Core's activity targets are a closed list of core's own resources. A\n * plugin's resource is not one of them and is never added to that list; the\n * org log's target type accepts any namespaced type instead, and the feed\n * reads the noun after the colon for a reader. The namespace keeps two\n * plugins' nouns from meaning one thing.\n */\nexport type PluginActivityTargetType = `${string}:${string}`\n\n/** A namespaced type as the log stores it: `pluginId:noun`, both parts plain. */\nconst PLUGIN_TARGET_TYPE = /^([a-z][a-z0-9-]*):([A-Za-z][A-Za-z0-9]*)$/\n\n/** Whether a stored target type is a plugin's namespaced one. */\nexport function isPluginActivityTargetType(type: unknown): type is PluginActivityTargetType {\n return typeof type === 'string' && PLUGIN_TARGET_TYPE.test(type)\n}\n\n/**\n * The noun a plugin's namespaced type names — `acme-mail:mailbox` →\n * `mailbox` — or `undefined` for any other type.\n */\nexport function pluginActivityTargetNoun(type: unknown): string | undefined {\n if (typeof type !== 'string') return undefined\n return PLUGIN_TARGET_TYPE.exec(type)?.[2]\n}\n\nexport interface PluginActivityAction {\n /** The stored action code — dotted, stable, never a sentence. */\n key: string\n /** What a person reads for the code in the feed and the actor table. */\n label: string\n /**\n * The same act as a sentence about its target (AGL-3660): `{target}` is\n * replaced by the target's noun and name — `Created {target} with Aglyn AI`\n * reads `Created page Home with Aglyn AI`. Absent, a row reads `label`.\n */\n sentence?: string\n /** Which log(s) the writers put the code in. */\n scope: PluginActivityScope | readonly PluginActivityScope[]\n /** The feed target types rows carrying this code file their output under. */\n resourceTypes?: readonly string[]\n}\n\n/**\n * The filter a plugin's coded rows share: one chip in the org feed and the\n * actor table, one facet group on the staff audit page.\n */\nexport interface PluginActivityGroup {\n /** Stable group id — the facet's value (`ai`). */\n id: string\n /** The chip and the facet option (`AI`). */\n label: string\n /**\n * Staff audit actions that belong to the group without carrying one of the\n * registered codes — rows written by core on the plugin's behalf, matched\n * by prefix (`billing.assistOverage.`).\n */\n staffAuditPrefixes?: readonly string[]\n /**\n * Staff audit actions a plugin's staff doors write when staff READ\n * something rather than change it (AGL-2939): the audit log files them as\n * an access. Matched exactly.\n */\n staffAuditAccessActions?: readonly string[]\n /**\n * What a person reads for each staff audit code the plugin's doors write\n * (AGL-3660) — the audit trail shows these words and keeps the code for\n * the row's tooltip and details. Matched exactly.\n */\n staffAuditLabels?: Readonly<Record<string, string>>\n /**\n * A staff audit row's words drawn from what the act left (AGL-3660): the\n * plugin reads its own `after` (what a job made, from what) and answers\n * the sentence and the outcome. `undefined` falls back to the label.\n */\n staffAuditDescribe?: (entry: {\n action: string\n after: Readonly<Record<string, unknown>>\n }) => { action?: string; result?: string } | undefined\n /**\n * The plugin's collections as an audit target path walks them, named for\n * a reader (AGL-3660). `job` marks the collection whose id is the job an\n * activity row links to.\n */\n staffAuditCollections?: Readonly<Record<string, { noun: string; job?: boolean }>>\n /** Activity target types whose id is such a job (AGL-3660). */\n jobTargetTypes?: readonly string[]\n}\n\nexport interface PluginActivityRegistration {\n pluginId: string\n group: PluginActivityGroup\n actions: readonly PluginActivityAction[]\n /**\n * Target types the plugin's rows are filed under that are NOT namespaced,\n * with the noun a reader sees for each (AGL-3080): rows written before\n * `pluginId:noun` existed, whose stored type cannot change. A namespaced\n * type needs no entry — the feed reads its noun off the type itself.\n */\n targetTypes?: Readonly<Record<string, string>>\n}\n\nconst registrations = new Map<string, PluginActivityRegistration>()\n\n/**\n * The most codes one group may hold. A group's chip asks `action is any of`\n * its codes, which the feeds' queries serve as one Firestore `in` — capped at\n * thirty values — so a larger group would be a chip the list always refuses.\n */\nexport const PLUGIN_ACTIVITY_GROUP_MAX_ACTIONS = 30\n\n/**\n * Idempotent per plugin — re-registration replaces that plugin's catalog. A\n * code already declared by a DIFFERENT plugin is refused for the whole\n * registration: two owners for one code would give the row two labels. A\n * group of more than `PLUGIN_ACTIVITY_GROUP_MAX_ACTIONS` codes is refused\n * too, since its chip could never be applied.\n */\nexport function registerPluginActivityActions(\n registration: PluginActivityRegistration,\n): void {\n const pluginId = registration.pluginId.trim()\n if (!pluginId) throw new Error('plugin activity actions need a pluginId')\n if (registration.actions.length > PLUGIN_ACTIVITY_GROUP_MAX_ACTIONS) {\n throw new Error(\n `activity group \"${registration.group.id}\" declares ` +\n `${registration.actions.length} actions; its filter chip can ask for at most ` +\n `${PLUGIN_ACTIVITY_GROUP_MAX_ACTIONS}, refused \"${pluginId}\"`,\n )\n }\n for (const action of registration.actions) {\n const owner = ownerOfActivityAction(action.key)\n if (owner && owner !== pluginId) {\n throw new Error(\n `activity action \"${action.key}\" is already declared by \"${owner}\"; ` +\n `refused \"${pluginId}\"`,\n )\n }\n }\n for (const type of Object.keys(registration.targetTypes ?? {})) {\n const owner = [...registrations.values()].find(\n (entry) => entry.pluginId !== pluginId && entry.targetTypes?.[type] !== undefined,\n )?.pluginId\n if (owner) {\n throw new Error(\n `activity target type \"${type}\" is already declared by \"${owner}\"; ` +\n `refused \"${pluginId}\"`,\n )\n }\n }\n registrations.set(pluginId, registration)\n}\n\n/**\n * The noun a plugin declared for one of its un-namespaced target types, or\n * `undefined` when no plugin declared the type.\n */\nexport function pluginActivityTargetLabel(type: unknown): string | undefined {\n if (typeof type !== 'string') return undefined\n for (const registration of registrations.values()) {\n const label = registration.targetTypes?.[type]\n if (label) return label\n }\n return undefined\n}\n\nfunction ownerOfActivityAction(key: string): string | undefined {\n for (const registration of registrations.values()) {\n if (registration.actions.some((action) => action.key === key)) {\n return registration.pluginId\n }\n }\n return undefined\n}\n\n/** Every registration, in registration order. */\nexport function listPluginActivityRegistrations(): PluginActivityRegistration[] {\n return [...registrations.values()]\n}\n\n/** Every declared action across plugins. */\nexport function listPluginActivityActions(): PluginActivityAction[] {\n return listPluginActivityRegistrations().flatMap((entry) => [...entry.actions])\n}\n\n/** A declared code's sentence template (`{target}` placeholder), if it has one. */\nexport function pluginActivityActionSentence(action: unknown): string | undefined {\n if (typeof action !== 'string') return undefined\n for (const registration of registrations.values()) {\n const match = registration.actions.find((entry) => entry.key === action)\n if (match) return match.sentence\n }\n return undefined\n}\n\n/** What a plugin declared a staff audit code reads as; `undefined` otherwise. */\nexport function pluginStaffAuditActionLabel(action: unknown): string | undefined {\n if (typeof action !== 'string') return undefined\n for (const registration of registrations.values()) {\n const label = registration.group.staffAuditLabels?.[action]\n if (label) return label\n }\n return undefined\n}\n\n/** A plugin's words for one staff audit row, from its `after`; `undefined` if none. */\nexport function pluginStaffAuditDescribe(\n action: string,\n after: Readonly<Record<string, unknown>>,\n): { action?: string; result?: string } | undefined {\n for (const registration of registrations.values()) {\n const described = registration.group.staffAuditDescribe?.({ action, after })\n if (described) return described\n }\n return undefined\n}\n\n/** How a plugin names one of its collections in an audit path, if it does. */\nexport function pluginStaffAuditCollection(\n collection: string,\n): { noun: string; job?: boolean } | undefined {\n for (const registration of registrations.values()) {\n const named = registration.group.staffAuditCollections?.[collection]\n if (named) return named\n }\n return undefined\n}\n\n/** Whether an activity target type names a plugin's job. */\nexport function isPluginJobTargetType(type: unknown): boolean {\n if (typeof type !== 'string') return false\n for (const registration of registrations.values()) {\n if (registration.group.jobTargetTypes?.includes(type)) return true\n }\n return false\n}\n\n/** The readable label for a declared code; `undefined` for any other action. */\nexport function pluginActivityActionLabel(action: unknown): string | undefined {\n if (typeof action !== 'string') return undefined\n for (const registration of registrations.values()) {\n const match = registration.actions.find((entry) => entry.key === action)\n if (match) return match.label\n }\n return undefined\n}\n\n/** The group a stored action's code belongs to, or `undefined`. */\nexport function pluginActivityGroupForAction(\n action: unknown,\n): PluginActivityGroup | undefined {\n if (typeof action !== 'string') return undefined\n for (const registration of registrations.values()) {\n if (registration.actions.some((entry) => entry.key === action)) {\n return registration.group\n }\n }\n return undefined\n}\n\n/**\n * The filter chips the feeds offer: one per registered group, each with the\n * codes its chip keeps. What an `action isAnyOf …` request sends.\n */\nexport function listPluginActivityFilters(): Array<{\n group: PluginActivityGroup\n actions: string[]\n}> {\n return listPluginActivityRegistrations().map((entry) => ({\n group: entry.group,\n actions: entry.actions.map((action) => action.key),\n }))\n}\n\n/**\n * The staff audit facet group for an action: a registered group when the\n * action is one of its codes or starts with one of its prefixes, else the\n * action's leading namespace (`billing`, `org`, `plugins`).\n */\nexport function pluginStaffAuditActionGroup(action: unknown): string {\n const text = typeof action === 'string' ? action.trim() : ''\n if (!text) return ''\n const byCode = pluginActivityGroupForAction(text)\n if (byCode) return byCode.id\n for (const registration of registrations.values()) {\n const prefixes = registration.group.staffAuditPrefixes ?? []\n if (prefixes.some((prefix) => text.startsWith(prefix))) {\n return registration.group.id\n }\n }\n const dot = text.indexOf('.')\n return dot > 0 ? text.slice(0, dot) : text\n}\n\n/** Whether a staff audit action is a read some plugin declared (AGL-2939). */\nexport function isPluginStaffAuditAccess(action: unknown): boolean {\n if (typeof action !== 'string' || !action) return false\n for (const registration of registrations.values()) {\n if (registration.group.staffAuditAccessActions?.includes(action)) return true\n }\n return false\n}\n\n/** How a facet group reads in the menu: a registered label, else the id. */\nexport function pluginStaffAuditActionGroupLabel(group: string): string {\n for (const registration of registrations.values()) {\n if (registration.group.id === group) return registration.group.label\n }\n return group\n}\n\n/** Test seam: forget every registration. */\nexport function resetPluginActivityActionsForTests(): void {\n registrations.clear()\n}\n"],"names":["PLUGIN_TARGET_TYPE","isPluginActivityTargetType","type","test","pluginActivityTargetNoun","undefined","exec","registrations","Map","PLUGIN_ACTIVITY_GROUP_MAX_ACTIONS","registerPluginActivityActions","registration","pluginId","trim","Error","actions","length","group","id","action","owner","ownerOfActivityAction","key","Object","keys","targetTypes","values","find","entry","set","pluginActivityTargetLabel","label","some","listPluginActivityRegistrations","listPluginActivityActions","flatMap","pluginActivityActionSentence","match","sentence","pluginStaffAuditActionLabel","staffAuditLabels","pluginStaffAuditDescribe","after","described","staffAuditDescribe","pluginStaffAuditCollection","collection","named","staffAuditCollections","isPluginJobTargetType","jobTargetTypes","includes","pluginActivityActionLabel","pluginActivityGroupForAction","listPluginActivityFilters","map","pluginStaffAuditActionGroup","text","byCode","prefixes","staffAuditPrefixes","prefix","startsWith","dot","indexOf","slice","isPluginStaffAuditAccess","staffAuditAccessActions","pluginStaffAuditActionGroupLabel","resetPluginActivityActionsForTests","clear"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-activity-actions.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 * Plugin-declared activity actions (AGL-2940).\n *\n * Most rows in `orgs/{orgId}/activity` and `hosts/{hostId}/activity` store\n * a prose action written at the call site. A plugin whose rows are read by\n * more than a person — a filter chip, a staff facet, an actor table — stores\n * a CODE (`ai.job.output`) and declares here what the code means: the label\n * a reader sees, the scope it is written at, and the group its filter chip\n * belongs to. The activity renderers read this catalog for labels and\n * filters, so a second plugin with coded rows gets a chip and a facet group\n * by registering, with no edit to the renderers.\n *\n * Declared at module scope from both the client barrel and the `/server`\n * entry (the AGL-428 pattern), with an explicit `pluginId`: module scope\n * runs before the loader's ownership marker is set.\n */\n\nexport type PluginActivityScope = 'org' | 'host' | 'staff'\n\n/**\n * A resource a PLUGIN files an org activity row under (AGL-2978): the\n * plugin's id, a colon and the plugin's own noun — `acme-mail:mailbox`.\n *\n * Core's activity targets are a closed list of core's own resources. A\n * plugin's resource is not one of them and is never added to that list; the\n * org log's target type accepts any namespaced type instead, and the feed\n * reads the noun after the colon for a reader. The namespace keeps two\n * plugins' nouns from meaning one thing.\n */\nexport type PluginActivityTargetType = `${string}:${string}`\n\n/** A namespaced type as the log stores it: `pluginId:noun`, both parts plain. */\nconst PLUGIN_TARGET_TYPE = /^([a-z][a-z0-9-]*):([A-Za-z][A-Za-z0-9]*)$/\n\n/** Whether a stored target type is a plugin's namespaced one. */\nexport function isPluginActivityTargetType(type: unknown): type is PluginActivityTargetType {\n return typeof type === 'string' && PLUGIN_TARGET_TYPE.test(type)\n}\n\n/**\n * The noun a plugin's namespaced type names — `acme-mail:mailbox` →\n * `mailbox` — or `undefined` for any other type.\n */\nexport function pluginActivityTargetNoun(type: unknown): string | undefined {\n if (typeof type !== 'string') return undefined\n return PLUGIN_TARGET_TYPE.exec(type)?.[2]\n}\n\nexport interface PluginActivityAction {\n /** The stored action code — dotted, stable, never a sentence. */\n key: string\n /** What a person reads for the code in the feed and the actor table. */\n label: string\n /**\n * The same act as a sentence about its target (AGL-3660): `{target}` is\n * replaced by the target's noun and name — `Created {target} with Aglyn AI`\n * reads `Created page Home with Aglyn AI`. Absent, a row reads `label`.\n */\n sentence?: string\n /** Which log(s) the writers put the code in. */\n scope: PluginActivityScope | readonly PluginActivityScope[]\n /** The feed target types rows carrying this code file their output under. */\n resourceTypes?: readonly string[]\n}\n\n/**\n * The filter a plugin's coded rows share: one chip in the org feed and the\n * actor table, one facet group on the staff audit page.\n */\nexport interface PluginActivityGroup {\n /** Stable group id — the facet's value (`ai`). */\n id: string\n /** The chip and the facet option (`AI`). */\n label: string\n /**\n * Staff audit actions that belong to the group without carrying one of the\n * registered codes — rows written by core on the plugin's behalf, matched\n * by prefix (`billing.assistOverage.`).\n */\n staffAuditPrefixes?: readonly string[]\n /**\n * Staff audit actions a plugin's staff doors write when staff READ\n * something rather than change it (AGL-2939): the audit log files them as\n * an access. Matched exactly.\n */\n staffAuditAccessActions?: readonly string[]\n /**\n * What a person reads for each staff audit code the plugin's doors write\n * (AGL-3660) — the audit trail shows these words and keeps the code for\n * the row's tooltip and details. Matched exactly.\n */\n staffAuditLabels?: Readonly<Record<string, string>>\n /**\n * A staff audit row's words drawn from what the act left (AGL-3660): the\n * plugin reads its own `after` (what a job made, from what) and answers\n * the sentence and the outcome. `undefined` falls back to the label.\n */\n staffAuditDescribe?: (entry: {\n action: string\n after: Readonly<Record<string, unknown>>\n }) => { action?: string; result?: string } | undefined\n /**\n * The plugin's collections as an audit target path walks them, named for\n * a reader (AGL-3660). `job` marks the collection whose id is the job an\n * activity row links to.\n */\n staffAuditCollections?: Readonly<Record<string, { noun: string; job?: boolean }>>\n /** Activity target types whose id is such a job (AGL-3660). */\n jobTargetTypes?: readonly string[]\n}\n\nexport interface PluginActivityRegistration {\n pluginId: string\n group: PluginActivityGroup\n actions: readonly PluginActivityAction[]\n /**\n * Target types the plugin's rows are filed under that are NOT namespaced,\n * with the noun a reader sees for each (AGL-3080): rows written before\n * `pluginId:noun` existed, whose stored type cannot change. A namespaced\n * type needs no entry — the feed reads its noun off the type itself.\n */\n targetTypes?: Readonly<Record<string, string>>\n /**\n * Words for the plugin's rows that store a SENTENCE rather than a code\n * (AGL-3892): rows its writers filed before codes existed, or still file as\n * prose (`Action ran on formSubmission`). Answers what a person reads for\n * such an action, or `undefined` for one that is not the plugin's. Pure and\n * cheap: every feed row asks every plugin that declares one.\n */\n describeAction?: (action: string) => string | undefined\n}\n\n/**\n * The registrations live on `globalThis` under `Symbol.for`: each app's boot\n * (`instrumentation.ts`) declares them and its routes read them, and Next\n * compiles those apart, so a module-scoped map would be one copy per module\n * graph: boot would fill its copy and a route would read an empty one\n * (AGL-3412, AGL-3868).\n */\nconst PLUGIN_ACTIVITY_ACTIONS_KEY = Symbol.for('@aglyn/aglyn:plugin-activity-actions')\n\nconst globalScope = globalThis as typeof globalThis & {\n [PLUGIN_ACTIVITY_ACTIONS_KEY]?: Map<string, PluginActivityRegistration>\n}\n\nconst registrations: Map<string, PluginActivityRegistration> =\n globalScope[PLUGIN_ACTIVITY_ACTIONS_KEY] ??\n (globalScope[PLUGIN_ACTIVITY_ACTIONS_KEY] = new Map<string, PluginActivityRegistration>())\n\n/**\n * The most codes one group may hold. A group's chip asks `action is any of`\n * its codes, which the feeds' queries serve as one Firestore `in` — capped at\n * thirty values — so a larger group would be a chip the list always refuses.\n */\nexport const PLUGIN_ACTIVITY_GROUP_MAX_ACTIONS = 30\n\n/**\n * Idempotent per plugin — re-registration replaces that plugin's catalog. A\n * code already declared by a DIFFERENT plugin is refused for the whole\n * registration: two owners for one code would give the row two labels. A\n * group of more than `PLUGIN_ACTIVITY_GROUP_MAX_ACTIONS` codes is refused\n * too, since its chip could never be applied.\n */\nexport function registerPluginActivityActions(\n registration: PluginActivityRegistration,\n): void {\n const pluginId = registration.pluginId.trim()\n if (!pluginId) throw new Error('plugin activity actions need a pluginId')\n if (registration.actions.length > PLUGIN_ACTIVITY_GROUP_MAX_ACTIONS) {\n throw new Error(\n `activity group \"${registration.group.id}\" declares ` +\n `${registration.actions.length} actions; its filter chip can ask for at most ` +\n `${PLUGIN_ACTIVITY_GROUP_MAX_ACTIONS}, refused \"${pluginId}\"`,\n )\n }\n for (const action of registration.actions) {\n const owner = ownerOfActivityAction(action.key)\n if (owner && owner !== pluginId) {\n throw new Error(\n `activity action \"${action.key}\" is already declared by \"${owner}\"; ` +\n `refused \"${pluginId}\"`,\n )\n }\n }\n for (const type of Object.keys(registration.targetTypes ?? {})) {\n const owner = [...registrations.values()].find(\n (entry) => entry.pluginId !== pluginId && entry.targetTypes?.[type] !== undefined,\n )?.pluginId\n if (owner) {\n throw new Error(\n `activity target type \"${type}\" is already declared by \"${owner}\"; ` +\n `refused \"${pluginId}\"`,\n )\n }\n }\n registrations.set(pluginId, registration)\n}\n\n/**\n * The noun a plugin declared for one of its un-namespaced target types, or\n * `undefined` when no plugin declared the type.\n */\nexport function pluginActivityTargetLabel(type: unknown): string | undefined {\n if (typeof type !== 'string') return undefined\n for (const registration of registrations.values()) {\n const label = registration.targetTypes?.[type]\n if (label) return label\n }\n return undefined\n}\n\nfunction ownerOfActivityAction(key: string): string | undefined {\n for (const registration of registrations.values()) {\n if (registration.actions.some((action) => action.key === key)) {\n return registration.pluginId\n }\n }\n return undefined\n}\n\n/** Every registration, in registration order. */\nexport function listPluginActivityRegistrations(): PluginActivityRegistration[] {\n return [...registrations.values()]\n}\n\n/** Every declared action across plugins. */\nexport function listPluginActivityActions(): PluginActivityAction[] {\n return listPluginActivityRegistrations().flatMap((entry) => [...entry.actions])\n}\n\n/** A declared code's sentence template (`{target}` placeholder), if it has one. */\nexport function pluginActivityActionSentence(action: unknown): string | undefined {\n if (typeof action !== 'string') return undefined\n for (const registration of registrations.values()) {\n const match = registration.actions.find((entry) => entry.key === action)\n if (match) return match.sentence\n }\n return undefined\n}\n\n/** What a plugin declared a staff audit code reads as; `undefined` otherwise. */\nexport function pluginStaffAuditActionLabel(action: unknown): string | undefined {\n if (typeof action !== 'string') return undefined\n for (const registration of registrations.values()) {\n const label = registration.group.staffAuditLabels?.[action]\n if (label) return label\n }\n return undefined\n}\n\n/** A plugin's words for one staff audit row, from its `after`; `undefined` if none. */\nexport function pluginStaffAuditDescribe(\n action: string,\n after: Readonly<Record<string, unknown>>,\n): { action?: string; result?: string } | undefined {\n for (const registration of registrations.values()) {\n const described = registration.group.staffAuditDescribe?.({ action, after })\n if (described) return described\n }\n return undefined\n}\n\n/** How a plugin names one of its collections in an audit path, if it does. */\nexport function pluginStaffAuditCollection(\n collection: string,\n): { noun: string; job?: boolean } | undefined {\n for (const registration of registrations.values()) {\n const named = registration.group.staffAuditCollections?.[collection]\n if (named) return named\n }\n return undefined\n}\n\n/** Whether an activity target type names a plugin's job. */\nexport function isPluginJobTargetType(type: unknown): boolean {\n if (typeof type !== 'string') return false\n for (const registration of registrations.values()) {\n if (registration.group.jobTargetTypes?.includes(type)) return true\n }\n return false\n}\n\n/** The readable label for a declared code; `undefined` for any other action. */\nexport function pluginActivityActionLabel(action: unknown): string | undefined {\n if (typeof action !== 'string') return undefined\n for (const registration of registrations.values()) {\n const match = registration.actions.find((entry) => entry.key === action)\n if (match) return match.label\n }\n return pluginActivityDescribedAction(action)\n}\n\n/**\n * What a plugin's `describeAction` says of a stored sentence (AGL-3892), or\n * `undefined` when no plugin claims it. A describer that throws is skipped,\n * so one plugin's bug never blanks another's rows.\n */\nexport function pluginActivityDescribedAction(action: unknown): string | undefined {\n if (typeof action !== 'string' || !action.trim()) return undefined\n for (const registration of registrations.values()) {\n if (!registration.describeAction) continue\n try {\n const words = registration.describeAction(action)\n if (words) return words\n } catch {\n // A describer that throws describes nothing.\n }\n }\n return undefined\n}\n\n/** The group a stored action's code belongs to, or `undefined`. */\nexport function pluginActivityGroupForAction(\n action: unknown,\n): PluginActivityGroup | undefined {\n if (typeof action !== 'string') return undefined\n for (const registration of registrations.values()) {\n if (registration.actions.some((entry) => entry.key === action)) {\n return registration.group\n }\n }\n return undefined\n}\n\n/**\n * The filter chips the feeds offer: one per registered group, each with the\n * codes its chip keeps. What an `action isAnyOf …` request sends.\n */\nexport function listPluginActivityFilters(): Array<{\n group: PluginActivityGroup\n actions: string[]\n}> {\n // A plugin that only describes prose rows (no codes) has no chip: its chip could keep nothing.\n return listPluginActivityRegistrations()\n .filter((entry) => entry.actions.length > 0)\n .map((entry) => ({\n group: entry.group,\n actions: entry.actions.map((action) => action.key),\n }))\n}\n\n/**\n * The staff audit facet group for an action: a registered group when the\n * action is one of its codes or starts with one of its prefixes, else the\n * action's leading namespace (`billing`, `org`, `plugins`).\n */\nexport function pluginStaffAuditActionGroup(action: unknown): string {\n const text = typeof action === 'string' ? action.trim() : ''\n if (!text) return ''\n const byCode = pluginActivityGroupForAction(text)\n if (byCode) return byCode.id\n for (const registration of registrations.values()) {\n const prefixes = registration.group.staffAuditPrefixes ?? []\n if (prefixes.some((prefix) => text.startsWith(prefix))) {\n return registration.group.id\n }\n }\n const dot = text.indexOf('.')\n return dot > 0 ? text.slice(0, dot) : text\n}\n\n/** Whether a staff audit action is a read some plugin declared (AGL-2939). */\nexport function isPluginStaffAuditAccess(action: unknown): boolean {\n if (typeof action !== 'string' || !action) return false\n for (const registration of registrations.values()) {\n if (registration.group.staffAuditAccessActions?.includes(action)) return true\n }\n return false\n}\n\n/** How a facet group reads in the menu: a registered label, else the id. */\nexport function pluginStaffAuditActionGroupLabel(group: string): string {\n for (const registration of registrations.values()) {\n if (registration.group.id === group) return registration.group.label\n }\n return group\n}\n\n/** Test seam: forget every registration. */\nexport function resetPluginActivityActionsForTests(): void {\n registrations.clear()\n}\n"],"names":["globalScope","PLUGIN_TARGET_TYPE","isPluginActivityTargetType","type","test","pluginActivityTargetNoun","undefined","exec","PLUGIN_ACTIVITY_ACTIONS_KEY","Symbol","for","globalThis","registrations","Map","PLUGIN_ACTIVITY_GROUP_MAX_ACTIONS","registerPluginActivityActions","registration","pluginId","trim","Error","actions","length","group","id","action","owner","ownerOfActivityAction","key","Object","keys","targetTypes","values","find","entry","set","pluginActivityTargetLabel","label","some","listPluginActivityRegistrations","listPluginActivityActions","flatMap","pluginActivityActionSentence","match","sentence","pluginStaffAuditActionLabel","staffAuditLabels","pluginStaffAuditDescribe","after","described","staffAuditDescribe","pluginStaffAuditCollection","collection","named","staffAuditCollections","isPluginJobTargetType","jobTargetTypes","includes","pluginActivityActionLabel","pluginActivityDescribedAction","describeAction","words","pluginActivityGroupForAction","listPluginActivityFilters","filter","map","pluginStaffAuditActionGroup","text","byCode","prefixes","staffAuditPrefixes","prefix","startsWith","dot","indexOf","slice","isPluginStaffAuditAccess","staffAuditAccessActions","pluginStaffAuditActionGroupLabel","resetPluginActivityActionsForTests","clear"],"mappings":"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"}
@@ -0,0 +1,81 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * A SERVER PROCESS DELETES A SITE THE WAY ITS ADMIN DOES (AGL-3936).
19
+ *
20
+ * Deleting a site is more than its documents: the erase itself
21
+ * (`eraseHost`: Storage, the routing index, the org's `hosts` map and its
22
+ * pooled seats, every member's projection, the Firestore tree), the site's
23
+ * sending domain, which only the console holds the credentials to release,
24
+ * the tenant's cached pages and names, and the record of it in the
25
+ * workspace's feed and the staff audit. The console's delete route does all
26
+ * of it in that order. A plugin with no request behind it — the AI beat
27
+ * removing the empty site a cancelled guided start left — has no route to
28
+ * call, and a partial delete of its own would strand whichever half it left
29
+ * out. So the console registers its one implementation at boot
30
+ * (`instrumentation.ts`), the same direction as `core.site-cache`, and a
31
+ * plugin names the site and why.
32
+ *
33
+ * Authorization is the caller's. The implementation re-reads the site, and
34
+ * refuses one in a consent group as the delete route does.
35
+ *
36
+ * A process with no implementation answers `{ erased: false, reason:
37
+ * 'unavailable' }` from {@link erasePluginSite}, never a silent success: a
38
+ * caller keeps whatever told it to delete, and tries again later.
39
+ */
40
+ export interface PluginSiteEraseRequest {
41
+ hostId: string;
42
+ /**
43
+ * What the workspace's feed says happened, in its own words: "Removed an
44
+ * empty site". The feed names the site beside it.
45
+ */
46
+ activity: string;
47
+ /** Why, for the staff audit and the log: `'ai-start-empty'`. */
48
+ reason: string;
49
+ /**
50
+ * The workspace the caller believes owns the site. A site that now belongs
51
+ * to another workspace is not erased (`reason: 'moved'`).
52
+ */
53
+ orgId: string;
54
+ }
55
+ export type PluginSiteEraseResult = {
56
+ erased: true;
57
+ } | {
58
+ erased: false;
59
+ /**
60
+ * `missing`: the site is already gone. `moved`: it belongs to another
61
+ * workspace now. `consent-group`: its consent group still needs it.
62
+ * `unavailable`: nothing is installed, or the erase failed.
63
+ */
64
+ reason: 'missing' | 'moved' | 'consent-group' | 'unavailable';
65
+ };
66
+ export interface PluginSiteEraser {
67
+ erase(request: PluginSiteEraseRequest): Promise<PluginSiteEraseResult>;
68
+ }
69
+ /** One implementation: the app that holds the delete route's credentials. */
70
+ export declare const PLUGIN_SITE_ERASER: import("./plugin-services").PluginServiceContract<PluginSiteEraser>;
71
+ export declare function registerPluginSiteEraser(eraser: PluginSiteEraser, options?: {
72
+ pluginId?: string;
73
+ }): void;
74
+ /** Whether an implementation is installed. */
75
+ export declare function hasPluginSiteEraser(): boolean;
76
+ /**
77
+ * Deletes one site through the app's implementation. Never throws: nothing
78
+ * installed, or an implementation that threw, answers `unavailable` with
79
+ * the reason logged.
80
+ */
81
+ export declare function erasePluginSite(request: PluginSiteEraseRequest): Promise<PluginSiteEraseResult>;
@@ -0,0 +1,56 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { definePluginServiceContract, registerPluginService, resolvePluginService } from "./plugin-services.js";
18
+ /** One implementation: the app that holds the delete route's credentials. */ export const PLUGIN_SITE_ERASER = definePluginServiceContract('core.site-erase', {
19
+ multiple: false
20
+ });
21
+ export function registerPluginSiteEraser(eraser, options) {
22
+ if (typeof (eraser == null ? void 0 : eraser.erase) !== 'function') {
23
+ throw new Error('a site eraser needs an erase function');
24
+ }
25
+ registerPluginService(PLUGIN_SITE_ERASER, eraser, _extends({}, (options == null ? void 0 : options.pluginId) ? {
26
+ pluginId: options.pluginId
27
+ } : {}));
28
+ }
29
+ /** Whether an implementation is installed. */ export function hasPluginSiteEraser() {
30
+ return Boolean(resolvePluginService(PLUGIN_SITE_ERASER));
31
+ }
32
+ /**
33
+ * Deletes one site through the app's implementation. Never throws: nothing
34
+ * installed, or an implementation that threw, answers `unavailable` with
35
+ * the reason logged.
36
+ */ export async function erasePluginSite(request) {
37
+ const eraser = resolvePluginService(PLUGIN_SITE_ERASER);
38
+ if (!eraser) {
39
+ console.error(`[site-erase] no implementation is installed, so site ${request.hostId} ` + `was not deleted (${request.reason}). The app registers this at boot.`);
40
+ return {
41
+ erased: false,
42
+ reason: 'unavailable'
43
+ };
44
+ }
45
+ try {
46
+ return await eraser.erase(request);
47
+ } catch (error) {
48
+ console.error(`[site-erase] deleting site ${request.hostId} failed (${request.reason})`, error);
49
+ return {
50
+ erased: false,
51
+ reason: 'unavailable'
52
+ };
53
+ }
54
+ }
55
+
56
+ //# sourceMappingURL=plugin-site-erase.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-site-erase.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 definePluginServiceContract,\n registerPluginService,\n resolvePluginService,\n} from './plugin-services'\n\n/**\n * A SERVER PROCESS DELETES A SITE THE WAY ITS ADMIN DOES (AGL-3936).\n *\n * Deleting a site is more than its documents: the erase itself\n * (`eraseHost`: Storage, the routing index, the org's `hosts` map and its\n * pooled seats, every member's projection, the Firestore tree), the site's\n * sending domain, which only the console holds the credentials to release,\n * the tenant's cached pages and names, and the record of it in the\n * workspace's feed and the staff audit. The console's delete route does all\n * of it in that order. A plugin with no request behind it — the AI beat\n * removing the empty site a cancelled guided start left — has no route to\n * call, and a partial delete of its own would strand whichever half it left\n * out. So the console registers its one implementation at boot\n * (`instrumentation.ts`), the same direction as `core.site-cache`, and a\n * plugin names the site and why.\n *\n * Authorization is the caller's. The implementation re-reads the site, and\n * refuses one in a consent group as the delete route does.\n *\n * A process with no implementation answers `{ erased: false, reason:\n * 'unavailable' }` from {@link erasePluginSite}, never a silent success: a\n * caller keeps whatever told it to delete, and tries again later.\n */\n\nexport interface PluginSiteEraseRequest {\n hostId: string\n /**\n * What the workspace's feed says happened, in its own words: \"Removed an\n * empty site\". The feed names the site beside it.\n */\n activity: string\n /** Why, for the staff audit and the log: `'ai-start-empty'`. */\n reason: string\n /**\n * The workspace the caller believes owns the site. A site that now belongs\n * to another workspace is not erased (`reason: 'moved'`).\n */\n orgId: string\n}\n\nexport type PluginSiteEraseResult =\n | { erased: true }\n | {\n erased: false\n /**\n * `missing`: the site is already gone. `moved`: it belongs to another\n * workspace now. `consent-group`: its consent group still needs it.\n * `unavailable`: nothing is installed, or the erase failed.\n */\n reason: 'missing' | 'moved' | 'consent-group' | 'unavailable'\n }\n\nexport interface PluginSiteEraser {\n erase(request: PluginSiteEraseRequest): Promise<PluginSiteEraseResult>\n}\n\n/** One implementation: the app that holds the delete route's credentials. */\nexport const PLUGIN_SITE_ERASER = definePluginServiceContract<PluginSiteEraser>(\n 'core.site-erase',\n { multiple: false },\n)\n\nexport function registerPluginSiteEraser(\n eraser: PluginSiteEraser,\n options?: { pluginId?: string },\n): void {\n if (typeof eraser?.erase !== 'function') {\n throw new Error('a site eraser needs an erase function')\n }\n registerPluginService(PLUGIN_SITE_ERASER, eraser, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\n/** Whether an implementation is installed. */\nexport function hasPluginSiteEraser(): boolean {\n return Boolean(resolvePluginService(PLUGIN_SITE_ERASER))\n}\n\n/**\n * Deletes one site through the app's implementation. Never throws: nothing\n * installed, or an implementation that threw, answers `unavailable` with\n * the reason logged.\n */\nexport async function erasePluginSite(\n request: PluginSiteEraseRequest,\n): Promise<PluginSiteEraseResult> {\n const eraser = resolvePluginService(PLUGIN_SITE_ERASER)\n if (!eraser) {\n console.error(\n `[site-erase] no implementation is installed, so site ${request.hostId} ` +\n `was not deleted (${request.reason}). The app registers this at boot.`,\n )\n return { erased: false, reason: 'unavailable' }\n }\n try {\n return await eraser.erase(request)\n } catch (error) {\n console.error(`[site-erase] deleting site ${request.hostId} failed (${request.reason})`, error)\n return { erased: false, reason: 'unavailable' }\n }\n}\n"],"names":["definePluginServiceContract","registerPluginService","resolvePluginService","PLUGIN_SITE_ERASER","multiple","registerPluginSiteEraser","eraser","options","erase","Error","pluginId","hasPluginSiteEraser","Boolean","erasePluginSite","request","console","error","hostId","reason","erased"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,2BAA2B,EAC3BC,qBAAqB,EACrBC,oBAAoB,QACf,uBAAmB;AA0D1B,2EAA2E,GAC3E,OAAO,MAAMC,qBAAqBH,4BAChC,mBACA;IAAEI,UAAU;AAAM,GACnB;AAED,OAAO,SAASC,yBACdC,MAAwB,EACxBC,OAA+B;IAE/B,IAAI,QAAOD,0BAAAA,OAAQE,KAAK,MAAK,YAAY;QACvC,MAAM,IAAIC,MAAM;IAClB;IACAR,sBAAsBE,oBAAoBG,QAAQ,aAC5CC,CAAAA,2BAAAA,QAASG,QAAQ,IAAG;QAAEA,UAAUH,QAAQG,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAEA,4CAA4C,GAC5C,OAAO,SAASC;IACd,OAAOC,QAAQV,qBAAqBC;AACtC;AAEA;;;;CAIC,GACD,OAAO,eAAeU,gBACpBC,OAA+B;IAE/B,MAAMR,SAASJ,qBAAqBC;IACpC,IAAI,CAACG,QAAQ;QACXS,QAAQC,KAAK,CACX,CAAC,qDAAqD,EAAEF,QAAQG,MAAM,CAAC,CAAC,CAAC,GACvE,CAAC,iBAAiB,EAAEH,QAAQI,MAAM,CAAC,kCAAkC,CAAC;QAE1E,OAAO;YAAEC,QAAQ;YAAOD,QAAQ;QAAc;IAChD;IACA,IAAI;QACF,OAAO,MAAMZ,OAAOE,KAAK,CAACM;IAC5B,EAAE,OAAOE,OAAO;QACdD,QAAQC,KAAK,CAAC,CAAC,2BAA2B,EAAEF,QAAQG,MAAM,CAAC,SAAS,EAAEH,QAAQI,MAAM,CAAC,CAAC,CAAC,EAAEF;QACzF,OAAO;YAAEG,QAAQ;YAAOD,QAAQ;QAAc;IAChD;AACF"}