@aglyn/aglyn 1.0.0-beta.271 → 1.0.0-beta.272

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 (33) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/account-preferences.d.ts +50 -0
  3. package/src/lib/app-utils/account-preferences.js +96 -0
  4. package/src/lib/app-utils/account-preferences.js.map +1 -1
  5. package/src/lib/app-utils/analytics-events.d.ts +3 -2
  6. package/src/lib/app-utils/analytics-events.js.map +1 -1
  7. package/src/lib/app-utils/client-build.d.ts +4 -1
  8. package/src/lib/app-utils/client-build.js.map +1 -1
  9. package/src/lib/app-utils/docs-help-sections.generated.js +6 -1
  10. package/src/lib/app-utils/docs-help-sections.generated.js.map +1 -1
  11. package/src/lib/app-utils/docs-help.generated.d.ts +7 -1
  12. package/src/lib/app-utils/docs-help.generated.js +22 -0
  13. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  14. package/src/lib/app-utils/docs-index.generated.js +6 -6
  15. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  16. package/src/lib/app-utils/plan-card-meters.d.ts +96 -0
  17. package/src/lib/app-utils/plan-card-meters.js +197 -0
  18. package/src/lib/app-utils/plan-card-meters.js.map +1 -0
  19. package/src/lib/app-utils/release-flags.d.ts +1 -1
  20. package/src/lib/app-utils/release-flags.js +12 -0
  21. package/src/lib/app-utils/release-flags.js.map +1 -1
  22. package/src/lib/app-utils/site-care.d.ts +25 -2
  23. package/src/lib/app-utils/site-care.js +28 -1
  24. package/src/lib/app-utils/site-care.js.map +1 -1
  25. package/src/lib/foundation/definitions/platform.types.d.ts +20 -0
  26. package/src/lib/foundation/definitions/platform.types.js.map +1 -1
  27. package/src/lib/plugin-manager/first-party-plugins.generated.js +16 -0
  28. package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
  29. package/src/lib/plugin-manager/plugin-media-ingest.d.ts +18 -1
  30. package/src/lib/plugin-manager/plugin-media-ingest.js.map +1 -1
  31. package/src/lib/plugin-manager/plugin-site-resources.d.ts +74 -0
  32. package/src/lib/plugin-manager/plugin-site-resources.js +135 -0
  33. package/src/lib/plugin-manager/plugin-site-resources.js.map +1 -0
@@ -14,7 +14,7 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */
17
- import type { AglynHostMediaStockSource } from '../foundation/definitions/platform.types';
17
+ import type { AglynHostMediaImportSource, AglynHostMediaStockSource } from '../foundation/definitions/platform.types';
18
18
  /**
19
19
  * A SERVER PROCESS STORES A PICTURE IN A SITE'S MEDIA LIBRARY (AGL-3660).
20
20
  *
@@ -62,6 +62,12 @@ export interface PluginMediaIngestRequest {
62
62
  * the asset, which is what the AI reads its owner's pictures by.
63
63
  */
64
64
  ownerPhotoLinkId?: string;
65
+ /**
66
+ * The website a site import brought the picture in from (AGL-3976),
67
+ * stamped as `importedFrom` on the asset; {@link PluginMediaIngest.findImportedPhoto}
68
+ * reads it back by its key, so one picture is stored once per site.
69
+ */
70
+ importedFrom?: Omit<AglynHostMediaImportSource, 'importedAt'>;
65
71
  }
66
72
  /** A stored asset, as a page names it. */
67
73
  export interface PluginMediaAsset {
@@ -102,6 +108,17 @@ export interface PluginMediaIngest {
102
108
  hostId: string;
103
109
  limit?: number;
104
110
  }): Promise<PluginMediaAsset[]>;
111
+ /**
112
+ * The site's live, public asset a site import already brought in under this
113
+ * source key (`import:<hash>`, AGL-3976), so the same picture is never
114
+ * stored twice. Optional: an implementation registered before imports
115
+ * existed answers nothing, and an import then stores no picture at all
116
+ * rather than risk a second copy.
117
+ */
118
+ findImportedPhoto?(input: {
119
+ hostId: string;
120
+ sourceKey: string;
121
+ }): Promise<PluginMediaAsset | null>;
105
122
  }
106
123
  /** One implementation: the app that holds the media ingress. */
107
124
  export declare const PLUGIN_MEDIA_INGEST: import("./plugin-services").PluginServiceContract<PluginMediaIngest>;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-media-ingest.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 type { AglynHostMediaStockSource } from '../foundation/definitions/platform.types'\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginService,\n} from './plugin-services'\n\n/**\n * A SERVER PROCESS STORES A PICTURE IN A SITE'S MEDIA LIBRARY (AGL-3660).\n *\n * The media library takes bytes through the console's own ingress — the\n * upload routes and the v1 API — which check the caller, the site's\n * lockdown, the bytes' structure, the takedown list, the plan and the\n * storage band before anything is written. A plugin that holds a picture on\n * the server with no browser request behind it (an AI job copying a stock\n * photo) has no route to post to. This is that route's door for it: the\n * console registers the one implementation at boot (`instrumentation.ts`),\n * the same direction as `core.site-cache`, and it applies the ingress checks\n * as the member named, so a plugin never writes a media document or a\n * Storage object itself.\n *\n * Images only: a server-held video or document has no caller yet.\n *\n * A process with no implementation answers `null` from\n * {@link pluginMediaIngest}, and a caller keeps its own fallback.\n */\n\nexport interface PluginMediaIngestRequest {\n hostId: string\n /**\n * The member the asset is stored as. Their access to the site, the site's\n * lockdown and the workspace's plan and storage band are checked as for\n * an upload they made themselves.\n */\n uid: string\n fileName: string\n /** A raster image type: `image/jpeg`, `image/png`, `image/webp` or `image/gif`. */\n contentType: string\n bytes: Uint8Array\n alt?: string\n description?: string\n /** Where a photo copied from a stock library came from (`stockPhoto` on the asset). */\n stockPhoto?: Omit<AglynHostMediaStockSource, 'importedAt'>\n /**\n * The library folder the asset is filed in (AGL-3957); absent or null,\n * the library's root. An id the library does not hold files the asset at\n * the root, as an upload naming a stale folder does.\n */\n folderId?: string | null\n /**\n * The asset is the business's own photo, sent through a site photo link\n * (AGL-3957) — the id of the link it came through. Stamps `ownerPhoto` on\n * the asset, which is what the AI reads its owner's pictures by.\n */\n ownerPhotoLinkId?: string\n}\n\n/** A stored asset, as a page names it. */\nexport interface PluginMediaAsset {\n mediaId: string\n /** What an `image` node's `src` holds: the asset's media reference, else its URL. */\n src: string\n width?: number\n height?: number\n /** What the asset's own alt text says it shows, where a member wrote one. */\n alt?: string\n}\n\nexport type PluginMediaIngestResult =\n | ({ ok: true } & PluginMediaAsset)\n | { ok: false; status: number; reason: string }\n\nexport interface PluginMediaIngest {\n /** Stores one picture in the site's own library. */\n ingest(request: PluginMediaIngestRequest): Promise<PluginMediaIngestResult>\n /**\n * The site's live, public asset copied from a stock photo with this source\n * key (`{provider}:{id}`), so a photo is never stored twice in one library.\n */\n findStockPhoto(input: { hostId: string; sourceKey: string }): Promise<PluginMediaAsset | null>\n /**\n * The site's live, public photos of the business itself (AGL-3957): what\n * came through a site photo link, newest first, at most `limit`. A photo\n * of the owner's own work is placed before any stock photo is searched\n * for. Optional: an implementation registered before the link existed\n * answers nothing, and a caller treats that as an empty library.\n */\n listOwnerPhotos?(input: { hostId: string; limit?: number }): Promise<PluginMediaAsset[]>\n}\n\n/** One implementation: the app that holds the media ingress. */\nexport const PLUGIN_MEDIA_INGEST = definePluginServiceContract<PluginMediaIngest>(\n 'core.media-ingest',\n { multiple: false },\n)\n\nexport function registerPluginMediaIngest(\n ingest: PluginMediaIngest,\n options?: { pluginId?: string },\n): void {\n if (typeof ingest?.ingest !== 'function' || typeof ingest?.findStockPhoto !== 'function') {\n throw new Error('a media ingest needs ingest and findStockPhoto')\n }\n registerPluginService(PLUGIN_MEDIA_INGEST, ingest, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\n/** The registered ingest, or `null` in a process that registered none. */\nexport function pluginMediaIngest(): PluginMediaIngest | null {\n return resolvePluginService(PLUGIN_MEDIA_INGEST) ?? null\n}\n"],"names":["definePluginServiceContract","registerPluginService","resolvePluginService","PLUGIN_MEDIA_INGEST","multiple","registerPluginMediaIngest","ingest","options","findStockPhoto","Error","pluginId","pluginMediaIngest"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAGD,SACEA,2BAA2B,EAC3BC,qBAAqB,EACrBC,oBAAoB,QACf,uBAAmB;AAqF1B,8DAA8D,GAC9D,OAAO,MAAMC,sBAAsBH,4BACjC,qBACA;IAAEI,UAAU;AAAM,GACnB;AAED,OAAO,SAASC,0BACdC,MAAyB,EACzBC,OAA+B;IAE/B,IAAI,QAAOD,0BAAAA,OAAQA,MAAM,MAAK,cAAc,QAAOA,0BAAAA,OAAQE,cAAc,MAAK,YAAY;QACxF,MAAM,IAAIC,MAAM;IAClB;IACAR,sBAAsBE,qBAAqBG,QAAQ,aAC7CC,CAAAA,2BAAAA,QAASG,QAAQ,IAAG;QAAEA,UAAUH,QAAQG,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAEA,wEAAwE,GACxE,OAAO,SAASC;QACPT;IAAP,QAAOA,wBAAAA,qBAAqBC,gCAArBD,wBAA6C;AACtD"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-media-ingest.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 type {\n AglynHostMediaImportSource,\n AglynHostMediaStockSource,\n} from '../foundation/definitions/platform.types'\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginService,\n} from './plugin-services'\n\n/**\n * A SERVER PROCESS STORES A PICTURE IN A SITE'S MEDIA LIBRARY (AGL-3660).\n *\n * The media library takes bytes through the console's own ingress — the\n * upload routes and the v1 API — which check the caller, the site's\n * lockdown, the bytes' structure, the takedown list, the plan and the\n * storage band before anything is written. A plugin that holds a picture on\n * the server with no browser request behind it (an AI job copying a stock\n * photo) has no route to post to. This is that route's door for it: the\n * console registers the one implementation at boot (`instrumentation.ts`),\n * the same direction as `core.site-cache`, and it applies the ingress checks\n * as the member named, so a plugin never writes a media document or a\n * Storage object itself.\n *\n * Images only: a server-held video or document has no caller yet.\n *\n * A process with no implementation answers `null` from\n * {@link pluginMediaIngest}, and a caller keeps its own fallback.\n */\n\nexport interface PluginMediaIngestRequest {\n hostId: string\n /**\n * The member the asset is stored as. Their access to the site, the site's\n * lockdown and the workspace's plan and storage band are checked as for\n * an upload they made themselves.\n */\n uid: string\n fileName: string\n /** A raster image type: `image/jpeg`, `image/png`, `image/webp` or `image/gif`. */\n contentType: string\n bytes: Uint8Array\n alt?: string\n description?: string\n /** Where a photo copied from a stock library came from (`stockPhoto` on the asset). */\n stockPhoto?: Omit<AglynHostMediaStockSource, 'importedAt'>\n /**\n * The library folder the asset is filed in (AGL-3957); absent or null,\n * the library's root. An id the library does not hold files the asset at\n * the root, as an upload naming a stale folder does.\n */\n folderId?: string | null\n /**\n * The asset is the business's own photo, sent through a site photo link\n * (AGL-3957) — the id of the link it came through. Stamps `ownerPhoto` on\n * the asset, which is what the AI reads its owner's pictures by.\n */\n ownerPhotoLinkId?: string\n /**\n * The website a site import brought the picture in from (AGL-3976),\n * stamped as `importedFrom` on the asset; {@link PluginMediaIngest.findImportedPhoto}\n * reads it back by its key, so one picture is stored once per site.\n */\n importedFrom?: Omit<AglynHostMediaImportSource, 'importedAt'>\n}\n\n/** A stored asset, as a page names it. */\nexport interface PluginMediaAsset {\n mediaId: string\n /** What an `image` node's `src` holds: the asset's media reference, else its URL. */\n src: string\n width?: number\n height?: number\n /** What the asset's own alt text says it shows, where a member wrote one. */\n alt?: string\n}\n\nexport type PluginMediaIngestResult =\n | ({ ok: true } & PluginMediaAsset)\n | { ok: false; status: number; reason: string }\n\nexport interface PluginMediaIngest {\n /** Stores one picture in the site's own library. */\n ingest(request: PluginMediaIngestRequest): Promise<PluginMediaIngestResult>\n /**\n * The site's live, public asset copied from a stock photo with this source\n * key (`{provider}:{id}`), so a photo is never stored twice in one library.\n */\n findStockPhoto(input: { hostId: string; sourceKey: string }): Promise<PluginMediaAsset | null>\n /**\n * The site's live, public photos of the business itself (AGL-3957): what\n * came through a site photo link, newest first, at most `limit`. A photo\n * of the owner's own work is placed before any stock photo is searched\n * for. Optional: an implementation registered before the link existed\n * answers nothing, and a caller treats that as an empty library.\n */\n listOwnerPhotos?(input: { hostId: string; limit?: number }): Promise<PluginMediaAsset[]>\n /**\n * The site's live, public asset a site import already brought in under this\n * source key (`import:<hash>`, AGL-3976), so the same picture is never\n * stored twice. Optional: an implementation registered before imports\n * existed answers nothing, and an import then stores no picture at all\n * rather than risk a second copy.\n */\n findImportedPhoto?(input: { hostId: string; sourceKey: string }): Promise<PluginMediaAsset | null>\n}\n\n/** One implementation: the app that holds the media ingress. */\nexport const PLUGIN_MEDIA_INGEST = definePluginServiceContract<PluginMediaIngest>(\n 'core.media-ingest',\n { multiple: false },\n)\n\nexport function registerPluginMediaIngest(\n ingest: PluginMediaIngest,\n options?: { pluginId?: string },\n): void {\n if (typeof ingest?.ingest !== 'function' || typeof ingest?.findStockPhoto !== 'function') {\n throw new Error('a media ingest needs ingest and findStockPhoto')\n }\n registerPluginService(PLUGIN_MEDIA_INGEST, ingest, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\n/** The registered ingest, or `null` in a process that registered none. */\nexport function pluginMediaIngest(): PluginMediaIngest | null {\n return resolvePluginService(PLUGIN_MEDIA_INGEST) ?? null\n}\n"],"names":["definePluginServiceContract","registerPluginService","resolvePluginService","PLUGIN_MEDIA_INGEST","multiple","registerPluginMediaIngest","ingest","options","findStockPhoto","Error","pluginId","pluginMediaIngest"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAMD,SACEA,2BAA2B,EAC3BC,qBAAqB,EACrBC,oBAAoB,QACf,uBAAmB;AAmG1B,8DAA8D,GAC9D,OAAO,MAAMC,sBAAsBH,4BACjC,qBACA;IAAEI,UAAU;AAAM,GACnB;AAED,OAAO,SAASC,0BACdC,MAAyB,EACzBC,OAA+B;IAE/B,IAAI,QAAOD,0BAAAA,OAAQA,MAAM,MAAK,cAAc,QAAOA,0BAAAA,OAAQE,cAAc,MAAK,YAAY;QACxF,MAAM,IAAIC,MAAM;IAClB;IACAR,sBAAsBE,qBAAqBG,QAAQ,aAC7CC,CAAAA,2BAAAA,QAASG,QAAQ,IAAG;QAAEA,UAAUH,QAAQG,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAEA,wEAAwE,GACxE,OAAO,SAASC;QACPT;IAAP,QAAOA,wBAAAA,qBAAqBC,gCAArBD,wBAA6C;AACtD"}
@@ -0,0 +1,74 @@
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
+ /** The site a preview or an erase is about, and the workspace that owns it. */
18
+ export interface PluginSiteResourceRequest {
19
+ orgId: string;
20
+ hostId: string;
21
+ }
22
+ /** One resource that goes with the site. */
23
+ export interface PluginSiteResourceGoing {
24
+ id: string;
25
+ /** Its name, as the plugin's own page shows it. */
26
+ name: string;
27
+ /** How many items it holds; `null` where they could not be counted. */
28
+ items: number | null;
29
+ }
30
+ /** What deleting the site does to one plugin's resources. */
31
+ export interface PluginSiteResourcePreview {
32
+ /** What one resource is called, and several: `['dataset', 'datasets']`. */
33
+ noun: readonly [string, string];
34
+ /** What one of its items is called, and several: `['record', 'records']`. */
35
+ itemNoun: readonly [string, string];
36
+ /** Shared with this site alone: erased with every item. */
37
+ going: PluginSiteResourceGoing[];
38
+ /** Shared with this site and others: they stay, and lose this site. */
39
+ staying: number;
40
+ }
41
+ export interface PluginSiteResourceEraser {
42
+ /** What deleting the site would take. Reads only. */
43
+ preview(request: PluginSiteResourceRequest): Promise<PluginSiteResourcePreview>;
44
+ /** Erases the site's own resources; answers how many it erased. */
45
+ erase(request: PluginSiteResourceRequest): Promise<number>;
46
+ }
47
+ /**
48
+ * Registers a plugin's site-resource eraser. Owner = the loader's marker
49
+ * inside a register fn, else `options.pluginId`; an eraser with neither
50
+ * throws. One per plugin: registering again replaces the plugin's earlier
51
+ * eraser in place, so a module evaluated twice does not erase twice.
52
+ */
53
+ export declare function registerPluginSiteResourceEraser(eraser: PluginSiteResourceEraser, options?: {
54
+ pluginId?: string;
55
+ }): void;
56
+ /** One plugin's preview, or `null` where its preview failed. */
57
+ export interface PluginSiteResourcePreviewRow {
58
+ pluginId: string;
59
+ preview: PluginSiteResourcePreview | null;
60
+ }
61
+ /**
62
+ * Every plugin's preview, in registration order. Never throws: a preview that
63
+ * fails is logged and answered as `null` for its plugin.
64
+ */
65
+ export declare function previewPluginSiteResources(request: PluginSiteResourceRequest): Promise<PluginSiteResourcePreviewRow[]>;
66
+ /**
67
+ * Runs every eraser, in registration order, and answers how many resources
68
+ * each erased by plugin id, or `null` for one that threw. Never throws.
69
+ */
70
+ export declare function erasePluginSiteResources(request: PluginSiteResourceRequest): Promise<Record<string, number | null>>;
71
+ /** The plugins with a site-resource eraser, in the order they run. */
72
+ export declare function listPluginSiteResourceErasers(): string[];
73
+ /** Test seam: forget every eraser. */
74
+ export declare function resetPluginSiteResourceErasersForTests(): void;
@@ -0,0 +1,135 @@
1
+ var _globalScope_ERASERS_KEY;
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
+ */ /**
18
+ * A PLUGIN'S SHARE OF DELETING A SITE (AGL-4003).
19
+ *
20
+ * Deleting a site (`eraseHost`) destroys what the platform holds for it: its
21
+ * document tree, its Storage, its routing, and the organization documents a
22
+ * plugin declares as the site's (`orgCollections` with a `siteField`). Some
23
+ * plugin resources belong to the ORGANIZATION and are shared with sites by a
24
+ * scope list instead — a dataset shared with one site, or with several. Which
25
+ * of those are the site's own, and what becomes of the rest, is the plugin's
26
+ * to say, so the plugin registers a site-resource eraser here:
27
+ *
28
+ * - `preview` lists what deleting the site would take — the resources shared
29
+ * with this site alone, each with how many items it holds — and how many it
30
+ * shares with other sites, which stay. The Delete site card shows it before
31
+ * anyone confirms, so it must change nothing.
32
+ * - `erase` erases the resources shared with this site alone, items and all,
33
+ * and takes the site off the sharing of the ones it shares with others. It
34
+ * answers how many it erased.
35
+ *
36
+ * Erasers run in registration order and are ISOLATED, like every trailing
37
+ * cleanup of a site delete: a throw is logged against its plugin and does not
38
+ * stop the next one or the delete. A preview that throws is reported as
39
+ * `null` for its plugin, so the card can say the list is incomplete rather
40
+ * than show an empty one.
41
+ *
42
+ * Registered from a plugin's `serverDeclarations`, so the eraser is in place
43
+ * in every server process that can delete a site.
44
+ */ import { getRegisteringPluginId } from "../app-utils/registering-plugin.js";
45
+ import { runPluginDeclarationsRepair } from "./plugin-declarations-repair.js";
46
+ /**
47
+ * One list per process, on `globalThis` (AGL-3464): the app registers its
48
+ * plugins' declarations from `instrumentation.ts`, which Next compiles apart
49
+ * from the route that deletes a site, and a module-scoped list is filled in
50
+ * one copy and read empty in the other.
51
+ */ const ERASERS_KEY = Symbol.for('@aglyn/aglyn:plugin-site-resource-erasers');
52
+ const globalScope = globalThis;
53
+ const registrations = (_globalScope_ERASERS_KEY = globalScope[ERASERS_KEY]) != null ? _globalScope_ERASERS_KEY : globalScope[ERASERS_KEY] = [];
54
+ /**
55
+ * Registers a plugin's site-resource eraser. Owner = the loader's marker
56
+ * inside a register fn, else `options.pluginId`; an eraser with neither
57
+ * throws. One per plugin: registering again replaces the plugin's earlier
58
+ * eraser in place, so a module evaluated twice does not erase twice.
59
+ */ export function registerPluginSiteResourceEraser(eraser, options) {
60
+ var _ref, _getRegisteringPluginId;
61
+ const pluginId = ((_ref = (_getRegisteringPluginId = getRegisteringPluginId()) != null ? _getRegisteringPluginId : options == null ? void 0 : options.pluginId) != null ? _ref : '').trim();
62
+ if (!pluginId) {
63
+ throw new Error('a plugin site-resource eraser was registered with no owner: pass ' + '{ pluginId } when registering outside a plugin register fn');
64
+ }
65
+ if (typeof (eraser == null ? void 0 : eraser.preview) !== 'function' || typeof (eraser == null ? void 0 : eraser.erase) !== 'function') {
66
+ throw new Error('a site-resource eraser needs a preview and an erase function');
67
+ }
68
+ const index = registrations.findIndex((entry)=>entry.pluginId === pluginId);
69
+ if (index >= 0) registrations[index] = {
70
+ pluginId,
71
+ eraser
72
+ };
73
+ else registrations.push({
74
+ pluginId,
75
+ eraser
76
+ });
77
+ }
78
+ /**
79
+ * The erasers in place, after running the app's boot step once when none is:
80
+ * a boot whose declarations failed must not read as a workspace with nothing
81
+ * shared with the site.
82
+ */ async function registered() {
83
+ if (!registrations.length) {
84
+ await runPluginDeclarationsRepair().catch((error)=>{
85
+ console.error('[plugins] the declarations repair failed before a site delete', error);
86
+ });
87
+ }
88
+ return [
89
+ ...registrations
90
+ ];
91
+ }
92
+ /**
93
+ * Every plugin's preview, in registration order. Never throws: a preview that
94
+ * fails is logged and answered as `null` for its plugin.
95
+ */ export async function previewPluginSiteResources(request) {
96
+ const rows = [];
97
+ for (const { pluginId, eraser } of (await registered())){
98
+ try {
99
+ rows.push({
100
+ pluginId,
101
+ preview: await eraser.preview(request)
102
+ });
103
+ } catch (error) {
104
+ console.error(`[plugins] ${pluginId} could not list what goes with site ${request.hostId}`, error);
105
+ rows.push({
106
+ pluginId,
107
+ preview: null
108
+ });
109
+ }
110
+ }
111
+ return rows;
112
+ }
113
+ /**
114
+ * Runs every eraser, in registration order, and answers how many resources
115
+ * each erased by plugin id, or `null` for one that threw. Never throws.
116
+ */ export async function erasePluginSiteResources(request) {
117
+ const erased = {};
118
+ for (const { pluginId, eraser } of (await registered())){
119
+ try {
120
+ erased[pluginId] = await eraser.erase(request);
121
+ } catch (error) {
122
+ erased[pluginId] = null;
123
+ console.error(`[plugins] ${pluginId} failed to erase site ${request.hostId}'s resources`, error);
124
+ }
125
+ }
126
+ return erased;
127
+ }
128
+ /** The plugins with a site-resource eraser, in the order they run. */ export function listPluginSiteResourceErasers() {
129
+ return registrations.map((entry)=>entry.pluginId);
130
+ }
131
+ /** Test seam: forget every eraser. */ export function resetPluginSiteResourceErasersForTests() {
132
+ registrations.length = 0;
133
+ }
134
+
135
+ //# sourceMappingURL=plugin-site-resources.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-site-resources.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 * A PLUGIN'S SHARE OF DELETING A SITE (AGL-4003).\n *\n * Deleting a site (`eraseHost`) destroys what the platform holds for it: its\n * document tree, its Storage, its routing, and the organization documents a\n * plugin declares as the site's (`orgCollections` with a `siteField`). Some\n * plugin resources belong to the ORGANIZATION and are shared with sites by a\n * scope list instead — a dataset shared with one site, or with several. Which\n * of those are the site's own, and what becomes of the rest, is the plugin's\n * to say, so the plugin registers a site-resource eraser here:\n *\n * - `preview` lists what deleting the site would take — the resources shared\n * with this site alone, each with how many items it holds — and how many it\n * shares with other sites, which stay. The Delete site card shows it before\n * anyone confirms, so it must change nothing.\n * - `erase` erases the resources shared with this site alone, items and all,\n * and takes the site off the sharing of the ones it shares with others. It\n * answers how many it erased.\n *\n * Erasers run in registration order and are ISOLATED, like every trailing\n * cleanup of a site delete: a throw is logged against its plugin and does not\n * stop the next one or the delete. A preview that throws is reported as\n * `null` for its plugin, so the card can say the list is incomplete rather\n * than show an empty one.\n *\n * Registered from a plugin's `serverDeclarations`, so the eraser is in place\n * in every server process that can delete a site.\n */\n\nimport { getRegisteringPluginId } from '../app-utils/registering-plugin'\nimport { runPluginDeclarationsRepair } from './plugin-declarations-repair'\n\n/** The site a preview or an erase is about, and the workspace that owns it. */\nexport interface PluginSiteResourceRequest {\n orgId: string\n hostId: string\n}\n\n/** One resource that goes with the site. */\nexport interface PluginSiteResourceGoing {\n id: string\n /** Its name, as the plugin's own page shows it. */\n name: string\n /** How many items it holds; `null` where they could not be counted. */\n items: number | null\n}\n\n/** What deleting the site does to one plugin's resources. */\nexport interface PluginSiteResourcePreview {\n /** What one resource is called, and several: `['dataset', 'datasets']`. */\n noun: readonly [string, string]\n /** What one of its items is called, and several: `['record', 'records']`. */\n itemNoun: readonly [string, string]\n /** Shared with this site alone: erased with every item. */\n going: PluginSiteResourceGoing[]\n /** Shared with this site and others: they stay, and lose this site. */\n staying: number\n}\n\nexport interface PluginSiteResourceEraser {\n /** What deleting the site would take. Reads only. */\n preview(request: PluginSiteResourceRequest): Promise<PluginSiteResourcePreview>\n /** Erases the site's own resources; answers how many it erased. */\n erase(request: PluginSiteResourceRequest): Promise<number>\n}\n\ninterface Registration {\n pluginId: string\n eraser: PluginSiteResourceEraser\n}\n\n/**\n * One list per process, on `globalThis` (AGL-3464): the app registers its\n * plugins' declarations from `instrumentation.ts`, which Next compiles apart\n * from the route that deletes a site, and a module-scoped list is filled in\n * one copy and read empty in the other.\n */\nconst ERASERS_KEY = Symbol.for('@aglyn/aglyn:plugin-site-resource-erasers')\n\nconst globalScope = globalThis as typeof globalThis & {\n [ERASERS_KEY]?: Registration[]\n}\n\nconst registrations: Registration[] =\n globalScope[ERASERS_KEY] ?? (globalScope[ERASERS_KEY] = [])\n\n/**\n * Registers a plugin's site-resource eraser. Owner = the loader's marker\n * inside a register fn, else `options.pluginId`; an eraser with neither\n * throws. One per plugin: registering again replaces the plugin's earlier\n * eraser in place, so a module evaluated twice does not erase twice.\n */\nexport function registerPluginSiteResourceEraser(\n eraser: PluginSiteResourceEraser,\n options?: { pluginId?: string },\n): void {\n const pluginId = (getRegisteringPluginId() ?? options?.pluginId ?? '').trim()\n if (!pluginId) {\n throw new Error(\n 'a plugin site-resource eraser was registered with no owner: pass ' +\n '{ pluginId } when registering outside a plugin register fn',\n )\n }\n if (typeof eraser?.preview !== 'function' || typeof eraser?.erase !== 'function') {\n throw new Error('a site-resource eraser needs a preview and an erase function')\n }\n const index = registrations.findIndex((entry) => entry.pluginId === pluginId)\n if (index >= 0) registrations[index] = { pluginId, eraser }\n else registrations.push({ pluginId, eraser })\n}\n\n/**\n * The erasers in place, after running the app's boot step once when none is:\n * a boot whose declarations failed must not read as a workspace with nothing\n * shared with the site.\n */\nasync function registered(): Promise<Registration[]> {\n if (!registrations.length) {\n await runPluginDeclarationsRepair().catch((error: unknown) => {\n console.error('[plugins] the declarations repair failed before a site delete', error)\n })\n }\n return [...registrations]\n}\n\n/** One plugin's preview, or `null` where its preview failed. */\nexport interface PluginSiteResourcePreviewRow {\n pluginId: string\n preview: PluginSiteResourcePreview | null\n}\n\n/**\n * Every plugin's preview, in registration order. Never throws: a preview that\n * fails is logged and answered as `null` for its plugin.\n */\nexport async function previewPluginSiteResources(\n request: PluginSiteResourceRequest,\n): Promise<PluginSiteResourcePreviewRow[]> {\n const rows: PluginSiteResourcePreviewRow[] = []\n for (const { pluginId, eraser } of await registered()) {\n try {\n rows.push({ pluginId, preview: await eraser.preview(request) })\n } catch (error) {\n console.error(\n `[plugins] ${pluginId} could not list what goes with site ${request.hostId}`,\n error,\n )\n rows.push({ pluginId, preview: null })\n }\n }\n return rows\n}\n\n/**\n * Runs every eraser, in registration order, and answers how many resources\n * each erased by plugin id, or `null` for one that threw. Never throws.\n */\nexport async function erasePluginSiteResources(\n request: PluginSiteResourceRequest,\n): Promise<Record<string, number | null>> {\n const erased: Record<string, number | null> = {}\n for (const { pluginId, eraser } of await registered()) {\n try {\n erased[pluginId] = await eraser.erase(request)\n } catch (error) {\n erased[pluginId] = null\n console.error(`[plugins] ${pluginId} failed to erase site ${request.hostId}'s resources`, error)\n }\n }\n return erased\n}\n\n/** The plugins with a site-resource eraser, in the order they run. */\nexport function listPluginSiteResourceErasers(): string[] {\n return registrations.map((entry) => entry.pluginId)\n}\n\n/** Test seam: forget every eraser. */\nexport function resetPluginSiteResourceErasersForTests(): void {\n registrations.length = 0\n}\n"],"names":["globalScope","getRegisteringPluginId","runPluginDeclarationsRepair","ERASERS_KEY","Symbol","for","globalThis","registrations","registerPluginSiteResourceEraser","eraser","options","pluginId","trim","Error","preview","erase","index","findIndex","entry","push","registered","length","catch","error","console","previewPluginSiteResources","request","rows","hostId","erasePluginSiteResources","erased","listPluginSiteResourceErasers","map","resetPluginSiteResourceErasersForTests"],"mappings":"IAqGEA;AArGF;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2BC,GAED,SAASC,sBAAsB,QAAQ,qCAAiC;AACxE,SAASC,2BAA2B,QAAQ,kCAA8B;AAyC1E;;;;;CAKC,GACD,MAAMC,cAAcC,OAAOC,GAAG,CAAC;AAE/B,MAAML,cAAcM;AAIpB,MAAMC,iBACJP,2BAAAA,WAAW,CAACG,YAAY,YAAxBH,2BAA6BA,WAAW,CAACG,YAAY,GAAG,EAAE;AAE5D;;;;;CAKC,GACD,OAAO,SAASK,iCACdC,MAAgC,EAChCC,OAA+B;QAEbT,MAAAA;IAAlB,MAAMU,WAAW,EAACV,QAAAA,0BAAAA,oCAAAA,0BAA4BS,2BAAAA,QAASC,QAAQ,YAA7CV,OAAiD,IAAIW,IAAI;IAC3E,IAAI,CAACD,UAAU;QACb,MAAM,IAAIE,MACR,sEACE;IAEN;IACA,IAAI,QAAOJ,0BAAAA,OAAQK,OAAO,MAAK,cAAc,QAAOL,0BAAAA,OAAQM,KAAK,MAAK,YAAY;QAChF,MAAM,IAAIF,MAAM;IAClB;IACA,MAAMG,QAAQT,cAAcU,SAAS,CAAC,CAACC,QAAUA,MAAMP,QAAQ,KAAKA;IACpE,IAAIK,SAAS,GAAGT,aAAa,CAACS,MAAM,GAAG;QAAEL;QAAUF;IAAO;SACrDF,cAAcY,IAAI,CAAC;QAAER;QAAUF;IAAO;AAC7C;AAEA;;;;CAIC,GACD,eAAeW;IACb,IAAI,CAACb,cAAcc,MAAM,EAAE;QACzB,MAAMnB,8BAA8BoB,KAAK,CAAC,CAACC;YACzCC,QAAQD,KAAK,CAAC,iEAAiEA;QACjF;IACF;IACA,OAAO;WAAIhB;KAAc;AAC3B;AAQA;;;CAGC,GACD,OAAO,eAAekB,2BACpBC,OAAkC;IAElC,MAAMC,OAAuC,EAAE;IAC/C,KAAK,MAAM,EAAEhB,QAAQ,EAAEF,MAAM,EAAE,IAAI,CAAA,MAAMW,YAAW,EAAG;QACrD,IAAI;YACFO,KAAKR,IAAI,CAAC;gBAAER;gBAAUG,SAAS,MAAML,OAAOK,OAAO,CAACY;YAAS;QAC/D,EAAE,OAAOH,OAAO;YACdC,QAAQD,KAAK,CACX,CAAC,UAAU,EAAEZ,SAAS,oCAAoC,EAAEe,QAAQE,MAAM,EAAE,EAC5EL;YAEFI,KAAKR,IAAI,CAAC;gBAAER;gBAAUG,SAAS;YAAK;QACtC;IACF;IACA,OAAOa;AACT;AAEA;;;CAGC,GACD,OAAO,eAAeE,yBACpBH,OAAkC;IAElC,MAAMI,SAAwC,CAAC;IAC/C,KAAK,MAAM,EAAEnB,QAAQ,EAAEF,MAAM,EAAE,IAAI,CAAA,MAAMW,YAAW,EAAG;QACrD,IAAI;YACFU,MAAM,CAACnB,SAAS,GAAG,MAAMF,OAAOM,KAAK,CAACW;QACxC,EAAE,OAAOH,OAAO;YACdO,MAAM,CAACnB,SAAS,GAAG;YACnBa,QAAQD,KAAK,CAAC,CAAC,UAAU,EAAEZ,SAAS,sBAAsB,EAAEe,QAAQE,MAAM,CAAC,YAAY,CAAC,EAAEL;QAC5F;IACF;IACA,OAAOO;AACT;AAEA,oEAAoE,GACpE,OAAO,SAASC;IACd,OAAOxB,cAAcyB,GAAG,CAAC,CAACd,QAAUA,MAAMP,QAAQ;AACpD;AAEA,oCAAoC,GACpC,OAAO,SAASsB;IACd1B,cAAcc,MAAM,GAAG;AACzB"}