@aglyn/aglyn 1.0.0-beta.271 → 1.0.0-beta.273
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 +11 -11
- package/src/lib/app-utils/account-preferences.d.ts +50 -0
- package/src/lib/app-utils/account-preferences.js +96 -0
- package/src/lib/app-utils/account-preferences.js.map +1 -1
- package/src/lib/app-utils/analytics-events.d.ts +3 -2
- package/src/lib/app-utils/analytics-events.js.map +1 -1
- package/src/lib/app-utils/client-build.d.ts +4 -1
- package/src/lib/app-utils/client-build.js.map +1 -1
- package/src/lib/app-utils/docs-help-sections.generated.js +6 -1
- package/src/lib/app-utils/docs-help-sections.generated.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +7 -1
- package/src/lib/app-utils/docs-help.generated.js +22 -0
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +15 -8
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/plan-card-meters.d.ts +96 -0
- package/src/lib/app-utils/plan-card-meters.js +197 -0
- package/src/lib/app-utils/plan-card-meters.js.map +1 -0
- package/src/lib/app-utils/release-flags.d.ts +1 -1
- package/src/lib/app-utils/release-flags.js +12 -0
- package/src/lib/app-utils/release-flags.js.map +1 -1
- package/src/lib/app-utils/site-care.d.ts +25 -2
- package/src/lib/app-utils/site-care.js +28 -1
- package/src/lib/app-utils/site-care.js.map +1 -1
- package/src/lib/foundation/definitions/platform.types.d.ts +20 -0
- package/src/lib/foundation/definitions/platform.types.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.js +16 -0
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
- package/src/lib/plugin-manager/plugin-media-ingest.d.ts +18 -1
- package/src/lib/plugin-manager/plugin-media-ingest.js.map +1 -1
- package/src/lib/plugin-manager/plugin-resource-drafts.d.ts +30 -0
- package/src/lib/plugin-manager/plugin-resource-drafts.js.map +1 -1
- package/src/lib/plugin-manager/plugin-site-resources.d.ts +74 -0
- package/src/lib/plugin-manager/plugin-site-resources.js +135 -0
- 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 {
|
|
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"}
|
|
@@ -142,6 +142,28 @@ export type PluginDraftWrite = (PluginDraftRecord & {
|
|
|
142
142
|
}) | (PluginDraftRefusal & {
|
|
143
143
|
ok: false;
|
|
144
144
|
});
|
|
145
|
+
/**
|
|
146
|
+
* A change to the records a draft was seeded with (AGL-3988): each record by
|
|
147
|
+
* its place among them, each value by the field's id the writer's facts
|
|
148
|
+
* report. Values not named are kept. A record whose `where` values are no
|
|
149
|
+
* longer what it holds — a member changed it since — is left as it is.
|
|
150
|
+
*/
|
|
151
|
+
export interface PluginDraftRecordsRevision extends PluginDraftContext {
|
|
152
|
+
/** The draft, by the id it was written under. */
|
|
153
|
+
id: string;
|
|
154
|
+
records: ReadonlyArray<{
|
|
155
|
+
/** Its place among the records the draft was seeded with, from 0. */
|
|
156
|
+
order: number;
|
|
157
|
+
values: Readonly<Record<string, unknown>>;
|
|
158
|
+
where?: Readonly<Record<string, unknown>>;
|
|
159
|
+
}>;
|
|
160
|
+
}
|
|
161
|
+
export type PluginDraftRecordsRevise = {
|
|
162
|
+
ok: true;
|
|
163
|
+
changed: number;
|
|
164
|
+
} | (PluginDraftRefusal & {
|
|
165
|
+
ok: false;
|
|
166
|
+
});
|
|
145
167
|
export interface PluginResourceDraftWriter {
|
|
146
168
|
/** Whether this member may create one on this site now; `null` admits. */
|
|
147
169
|
refusal(context: PluginDraftContext): Promise<PluginDraftRefusal | null>;
|
|
@@ -154,6 +176,14 @@ export interface PluginResourceDraftWriter {
|
|
|
154
176
|
}): Promise<PluginDraftRecord | null>;
|
|
155
177
|
/** Makes the draft; a refusal is returned, never thrown. */
|
|
156
178
|
write(request: PluginDraftRequest): Promise<PluginDraftWrite>;
|
|
179
|
+
/**
|
|
180
|
+
* Changes values of the records the draft was seeded with (AGL-3988), for a
|
|
181
|
+
* resource whose drafts are seeded with records: the AI site start's late
|
|
182
|
+
* photo pass gives a list's covers photos once its pages are built. Held
|
|
183
|
+
* to the same member rules as `write`; a refusal is returned, never thrown.
|
|
184
|
+
* Absent on a writer whose drafts have no records.
|
|
185
|
+
*/
|
|
186
|
+
reviseRecords?(request: PluginDraftRecordsRevision): Promise<PluginDraftRecordsRevise>;
|
|
157
187
|
}
|
|
158
188
|
export declare const PLUGIN_RESOURCE_DRAFTS: import("./plugin-services").PluginServiceContract<PluginResourceDraftWriter>;
|
|
159
189
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-resource-drafts.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 { ReusableComponentTree } from '../app-utils/compose-reusable-components'\nimport { getRegisteringPluginId } from '../app-utils/registering-plugin'\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/**\n * Drafts one plugin writes for another (AGL-2912).\n *\n * A resource belongs to the plugin that models it: the document's shape, the\n * allowance a create counts against, who may create one, and what makes one\n * well-formed. A different plugin that produces such a resource — an importer\n * turning a file into pages, a generator turning a brief into an email, a\n * migration bringing campaigns over from another tool — must not write the\n * document itself, because it would restate the owner's rules and drift from\n * them the day the owner changes one. Nor may it import the owner, which the\n * package map forbids.\n *\n * So the owner registers a WRITER for the resource from its server surfaces,\n * and a caller asks for one by resource name. Every rule stays the owner's:\n *\n * - `refusal` answers, before the caller spends anything, whether this\n * member may create one on this site now: their role, the site's room;\n * - `check` answers, synchronously and without I/O, whether content is\n * well-formed, so a caller can hold what it produces to the owner's rules\n * before it keeps it;\n * - `read` reports the draft already written under an id, so a caller that\n * runs again finds its draft rather than making a second;\n * - `write` makes the draft, meets the allowance inside its own write, and\n * never publishes, schedules or sends anything.\n *\n * Built on the typed service registry (`plugin-services`): one contract, every\n * writer registered under its resource name as the service's key.\n *\n * ## The registry carries no authorization of the CALLER\n *\n * A writer is a pre-authorized operation. It applies the owner's rules to the\n * member the draft is for, and knows nothing about whether the caller may act\n * for that member. A caller establishes, in its own terms and before it asks:\n * who is asking, that the org owns the site, and that the owning plugin is\n * switched on for that site.\n *\n * ## One writer per resource\n *\n * A resource has one owner. A second plugin registering a writer for a\n * resource another plugin already writes is refused, naming both, and the\n * incumbent keeps serving; the same plugin registering again — from its second\n * server surface, or on a hot reload — replaces its own.\n */\n\n/** Who a draft is for, on which site, and when. */\nexport interface PluginDraftContext {\n orgId: string\n hostId: string\n /** The member the draft is made for: the owner applies its rules to them and records them as the author. */\n uid: string\n /** The org document the caller already read, for the owner's entitlement and allowance reads. */\n org: Readonly<Record<string, unknown>> | null\n now: Date\n /**\n * The act the draft is one part of, where there is one: an AI build's job\n * (AGL-3755). Drafts of one act share what must not repeat across them (a\n * cover photo), and a writer that fetches something for the draft reads\n * what the act says the business is.\n */\n act?: PluginDraftAct\n}\n\n/** The act a draft is one part of ({@link PluginDraftContext.act}). */\nexport interface PluginDraftAct {\n /** Stable across the act's retries. */\n id: string\n /** What the act says the business is, in its own words: \"a sourdough bakery in Austin\". */\n business?: string\n}\n\n/** Why a draft cannot be made. */\nexport interface PluginDraftRefusal {\n status: 400 | 403 | 404 | 409\n /** Customer-safe: a caller shows it as it stands. */\n error: string\n}\n\n/** A draft that exists. */\nexport interface PluginDraftRecord {\n id: string\n name: string\n /** The version the draft opens on, where the resource has versions. */\n versionId: string | null\n /** What the owner reports about the draft beyond its identity, in the shape its writer documents. */\n facts: Readonly<Record<string, unknown>>\n /**\n * For a resource its writer makes live when the member may (an event the\n * publishing role publishes, AGL-3755): whether this one went live. `false`\n * means it was kept as a draft, and `notice` says why. Absent: the\n * capability's own `live` stands.\n */\n live?: boolean\n /** Customer-safe: a sentence a caller shows beside the record, such as why it stayed a draft. */\n notice?: string\n}\n\nexport interface PluginDraftRequest extends PluginDraftContext {\n /** The draft's id, chosen by the caller: one per act, so a write asked again finds its draft. */\n id: string\n /** The name asked for; the owner may number it to keep it unique. */\n name: string\n /** The resource's content, in the shape the owner's writer documents and validates. */\n content: Readonly<Record<string, unknown>>\n}\n\nexport type PluginDraftCheck =\n | { ok: true; facts: Readonly<Record<string, unknown>> }\n | { ok: false; problems: string[] }\n\n/** What a check is told beside the content. */\nexport interface PluginDraftCheckContext {\n hostId: string\n /**\n * The site's published reusable components the content places, keyed by id\n * — what `loadReferencedComponents` reads (AGL-3287).\n *\n * `check` does no I/O, so a caller whose content may place a component loads\n * the definitions first and hands them in; a writer whose resource renders\n * placed components composes them before judging, so what passes is what\n * would render. Absent means none were loaded, and the content is judged as\n * written.\n */\n components?: Readonly<Record<string, ReusableComponentTree | undefined>>\n}\n\nexport type PluginDraftWrite =\n | (PluginDraftRecord & { ok: true; replayed: boolean })\n | (PluginDraftRefusal & { ok: false })\n\nexport interface PluginResourceDraftWriter {\n /** Whether this member may create one on this site now; `null` admits. */\n refusal(context: PluginDraftContext): Promise<PluginDraftRefusal | null>\n /** Whether content is well-formed, with what the owner derived from it. Pure. */\n check(\n content: Readonly<Record<string, unknown>>,\n context: PluginDraftCheckContext,\n ): PluginDraftCheck\n /** The draft written under `id`, or `null`. */\n read(context: { hostId: string; id: string }): Promise<PluginDraftRecord | null>\n /** Makes the draft; a refusal is returned, never thrown. */\n write(request: PluginDraftRequest): Promise<PluginDraftWrite>\n}\n\nexport const PLUGIN_RESOURCE_DRAFTS =\n definePluginServiceContract<PluginResourceDraftWriter>('core.resource-drafts', {\n multiple: true,\n })\n\n/**\n * Registers the writer for a resource. The owner is the loader's marker when\n * a register fn is running, else `options.pluginId`; with neither the\n * registration throws. A writer for a resource another plugin writes throws\n * naming both.\n */\nexport function registerPluginResourceDraftWriter(\n resource: string,\n writer: PluginResourceDraftWriter,\n options?: { pluginId?: string },\n): void {\n const key = resource.trim()\n if (!key) throw new Error('a resource draft writer needs a resource name')\n const pluginId = (getRegisteringPluginId() ?? options?.pluginId ?? '').trim()\n const incumbent = resolvePluginServices(PLUGIN_RESOURCE_DRAFTS).find(\n (entry) => entry.key === key,\n )\n if (incumbent && pluginId && incumbent.pluginId !== pluginId) {\n throw new Error(\n `resource \"${key}\" already has a draft writer from \"${incumbent.pluginId}\"; ` +\n `refused \"${pluginId}\"`,\n )\n }\n registerPluginService(PLUGIN_RESOURCE_DRAFTS, writer, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n key,\n })\n}\n\nexport interface ResolvedPluginDraftWriter {\n /** The plugin that owns the resource. */\n pluginId: string\n writer: PluginResourceDraftWriter\n}\n\n/** The writer for a resource, with its owner, or `null` when no plugin writes it. */\nexport function pluginResourceDraftWriter(resource: string): ResolvedPluginDraftWriter | null {\n const key = resource.trim()\n const entry = resolvePluginServices(PLUGIN_RESOURCE_DRAFTS).find((one) => one.key === key)\n return entry ? { pluginId: entry.pluginId, writer: entry.impl } : null\n}\n"],"names":["getRegisteringPluginId","definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_RESOURCE_DRAFTS","multiple","registerPluginResourceDraftWriter","resource","writer","options","key","trim","Error","pluginId","incumbent","find","entry","pluginResourceDraftWriter","one","impl"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAGD,SAASA,sBAAsB,QAAQ,qCAAiC;AACxE,SACEC,2BAA2B,EAC3BC,qBAAqB,EACrBC,qBAAqB,QAChB,uBAAmB;AAiJ1B,OAAO,MAAMC,yBACXH,4BAAuD,wBAAwB;IAC7EI,UAAU;AACZ,GAAE;AAEJ;;;;;CAKC,GACD,OAAO,SAASC,kCACdC,QAAgB,EAChBC,MAAiC,EACjCC,OAA+B;QAIbT,MAAAA;IAFlB,MAAMU,MAAMH,SAASI,IAAI;IACzB,IAAI,CAACD,KAAK,MAAM,IAAIE,MAAM;IAC1B,MAAMC,WAAW,EAACb,QAAAA,0BAAAA,oCAAAA,0BAA4BS,2BAAAA,QAASI,QAAQ,YAA7Cb,OAAiD,IAAIW,IAAI;IAC3E,MAAMG,YAAYX,sBAAsBC,wBAAwBW,IAAI,CAClE,CAACC,QAAUA,MAAMN,GAAG,KAAKA;IAE3B,IAAII,aAAaD,YAAYC,UAAUD,QAAQ,KAAKA,UAAU;QAC5D,MAAM,IAAID,MACR,CAAC,UAAU,EAAEF,IAAI,mCAAmC,EAAEI,UAAUD,QAAQ,CAAC,GAAG,CAAC,GAC3E,CAAC,SAAS,EAAEA,SAAS,CAAC,CAAC;IAE7B;IACAX,sBAAsBE,wBAAwBI,QAAQ,aAChDC,CAAAA,2BAAAA,QAASI,QAAQ,IAAG;QAAEA,UAAUJ,QAAQI,QAAQ;IAAC,IAAI,CAAC;QAC1DH;;AAEJ;AAQA,mFAAmF,GACnF,OAAO,SAASO,0BAA0BV,QAAgB;IACxD,MAAMG,MAAMH,SAASI,IAAI;IACzB,MAAMK,QAAQb,sBAAsBC,wBAAwBW,IAAI,CAAC,CAACG,MAAQA,IAAIR,GAAG,KAAKA;IACtF,OAAOM,QAAQ;QAAEH,UAAUG,MAAMH,QAAQ;QAAEL,QAAQQ,MAAMG,IAAI;IAAC,IAAI;AACpE"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-resource-drafts.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 { ReusableComponentTree } from '../app-utils/compose-reusable-components'\nimport { getRegisteringPluginId } from '../app-utils/registering-plugin'\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/**\n * Drafts one plugin writes for another (AGL-2912).\n *\n * A resource belongs to the plugin that models it: the document's shape, the\n * allowance a create counts against, who may create one, and what makes one\n * well-formed. A different plugin that produces such a resource — an importer\n * turning a file into pages, a generator turning a brief into an email, a\n * migration bringing campaigns over from another tool — must not write the\n * document itself, because it would restate the owner's rules and drift from\n * them the day the owner changes one. Nor may it import the owner, which the\n * package map forbids.\n *\n * So the owner registers a WRITER for the resource from its server surfaces,\n * and a caller asks for one by resource name. Every rule stays the owner's:\n *\n * - `refusal` answers, before the caller spends anything, whether this\n * member may create one on this site now: their role, the site's room;\n * - `check` answers, synchronously and without I/O, whether content is\n * well-formed, so a caller can hold what it produces to the owner's rules\n * before it keeps it;\n * - `read` reports the draft already written under an id, so a caller that\n * runs again finds its draft rather than making a second;\n * - `write` makes the draft, meets the allowance inside its own write, and\n * never publishes, schedules or sends anything.\n *\n * Built on the typed service registry (`plugin-services`): one contract, every\n * writer registered under its resource name as the service's key.\n *\n * ## The registry carries no authorization of the CALLER\n *\n * A writer is a pre-authorized operation. It applies the owner's rules to the\n * member the draft is for, and knows nothing about whether the caller may act\n * for that member. A caller establishes, in its own terms and before it asks:\n * who is asking, that the org owns the site, and that the owning plugin is\n * switched on for that site.\n *\n * ## One writer per resource\n *\n * A resource has one owner. A second plugin registering a writer for a\n * resource another plugin already writes is refused, naming both, and the\n * incumbent keeps serving; the same plugin registering again — from its second\n * server surface, or on a hot reload — replaces its own.\n */\n\n/** Who a draft is for, on which site, and when. */\nexport interface PluginDraftContext {\n orgId: string\n hostId: string\n /** The member the draft is made for: the owner applies its rules to them and records them as the author. */\n uid: string\n /** The org document the caller already read, for the owner's entitlement and allowance reads. */\n org: Readonly<Record<string, unknown>> | null\n now: Date\n /**\n * The act the draft is one part of, where there is one: an AI build's job\n * (AGL-3755). Drafts of one act share what must not repeat across them (a\n * cover photo), and a writer that fetches something for the draft reads\n * what the act says the business is.\n */\n act?: PluginDraftAct\n}\n\n/** The act a draft is one part of ({@link PluginDraftContext.act}). */\nexport interface PluginDraftAct {\n /** Stable across the act's retries. */\n id: string\n /** What the act says the business is, in its own words: \"a sourdough bakery in Austin\". */\n business?: string\n}\n\n/** Why a draft cannot be made. */\nexport interface PluginDraftRefusal {\n status: 400 | 403 | 404 | 409\n /** Customer-safe: a caller shows it as it stands. */\n error: string\n}\n\n/** A draft that exists. */\nexport interface PluginDraftRecord {\n id: string\n name: string\n /** The version the draft opens on, where the resource has versions. */\n versionId: string | null\n /** What the owner reports about the draft beyond its identity, in the shape its writer documents. */\n facts: Readonly<Record<string, unknown>>\n /**\n * For a resource its writer makes live when the member may (an event the\n * publishing role publishes, AGL-3755): whether this one went live. `false`\n * means it was kept as a draft, and `notice` says why. Absent: the\n * capability's own `live` stands.\n */\n live?: boolean\n /** Customer-safe: a sentence a caller shows beside the record, such as why it stayed a draft. */\n notice?: string\n}\n\nexport interface PluginDraftRequest extends PluginDraftContext {\n /** The draft's id, chosen by the caller: one per act, so a write asked again finds its draft. */\n id: string\n /** The name asked for; the owner may number it to keep it unique. */\n name: string\n /** The resource's content, in the shape the owner's writer documents and validates. */\n content: Readonly<Record<string, unknown>>\n}\n\nexport type PluginDraftCheck =\n | { ok: true; facts: Readonly<Record<string, unknown>> }\n | { ok: false; problems: string[] }\n\n/** What a check is told beside the content. */\nexport interface PluginDraftCheckContext {\n hostId: string\n /**\n * The site's published reusable components the content places, keyed by id\n * — what `loadReferencedComponents` reads (AGL-3287).\n *\n * `check` does no I/O, so a caller whose content may place a component loads\n * the definitions first and hands them in; a writer whose resource renders\n * placed components composes them before judging, so what passes is what\n * would render. Absent means none were loaded, and the content is judged as\n * written.\n */\n components?: Readonly<Record<string, ReusableComponentTree | undefined>>\n}\n\nexport type PluginDraftWrite =\n | (PluginDraftRecord & { ok: true; replayed: boolean })\n | (PluginDraftRefusal & { ok: false })\n\n/**\n * A change to the records a draft was seeded with (AGL-3988): each record by\n * its place among them, each value by the field's id the writer's facts\n * report. Values not named are kept. A record whose `where` values are no\n * longer what it holds — a member changed it since — is left as it is.\n */\nexport interface PluginDraftRecordsRevision extends PluginDraftContext {\n /** The draft, by the id it was written under. */\n id: string\n records: ReadonlyArray<{\n /** Its place among the records the draft was seeded with, from 0. */\n order: number\n values: Readonly<Record<string, unknown>>\n where?: Readonly<Record<string, unknown>>\n }>\n}\n\nexport type PluginDraftRecordsRevise =\n | { ok: true; changed: number }\n | (PluginDraftRefusal & { ok: false })\n\nexport interface PluginResourceDraftWriter {\n /** Whether this member may create one on this site now; `null` admits. */\n refusal(context: PluginDraftContext): Promise<PluginDraftRefusal | null>\n /** Whether content is well-formed, with what the owner derived from it. Pure. */\n check(\n content: Readonly<Record<string, unknown>>,\n context: PluginDraftCheckContext,\n ): PluginDraftCheck\n /** The draft written under `id`, or `null`. */\n read(context: { hostId: string; id: string }): Promise<PluginDraftRecord | null>\n /** Makes the draft; a refusal is returned, never thrown. */\n write(request: PluginDraftRequest): Promise<PluginDraftWrite>\n /**\n * Changes values of the records the draft was seeded with (AGL-3988), for a\n * resource whose drafts are seeded with records: the AI site start's late\n * photo pass gives a list's covers photos once its pages are built. Held\n * to the same member rules as `write`; a refusal is returned, never thrown.\n * Absent on a writer whose drafts have no records.\n */\n reviseRecords?(request: PluginDraftRecordsRevision): Promise<PluginDraftRecordsRevise>\n}\n\nexport const PLUGIN_RESOURCE_DRAFTS =\n definePluginServiceContract<PluginResourceDraftWriter>('core.resource-drafts', {\n multiple: true,\n })\n\n/**\n * Registers the writer for a resource. The owner is the loader's marker when\n * a register fn is running, else `options.pluginId`; with neither the\n * registration throws. A writer for a resource another plugin writes throws\n * naming both.\n */\nexport function registerPluginResourceDraftWriter(\n resource: string,\n writer: PluginResourceDraftWriter,\n options?: { pluginId?: string },\n): void {\n const key = resource.trim()\n if (!key) throw new Error('a resource draft writer needs a resource name')\n const pluginId = (getRegisteringPluginId() ?? options?.pluginId ?? '').trim()\n const incumbent = resolvePluginServices(PLUGIN_RESOURCE_DRAFTS).find(\n (entry) => entry.key === key,\n )\n if (incumbent && pluginId && incumbent.pluginId !== pluginId) {\n throw new Error(\n `resource \"${key}\" already has a draft writer from \"${incumbent.pluginId}\"; ` +\n `refused \"${pluginId}\"`,\n )\n }\n registerPluginService(PLUGIN_RESOURCE_DRAFTS, writer, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n key,\n })\n}\n\nexport interface ResolvedPluginDraftWriter {\n /** The plugin that owns the resource. */\n pluginId: string\n writer: PluginResourceDraftWriter\n}\n\n/** The writer for a resource, with its owner, or `null` when no plugin writes it. */\nexport function pluginResourceDraftWriter(resource: string): ResolvedPluginDraftWriter | null {\n const key = resource.trim()\n const entry = resolvePluginServices(PLUGIN_RESOURCE_DRAFTS).find((one) => one.key === key)\n return entry ? { pluginId: entry.pluginId, writer: entry.impl } : null\n}\n"],"names":["getRegisteringPluginId","definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_RESOURCE_DRAFTS","multiple","registerPluginResourceDraftWriter","resource","writer","options","key","trim","Error","pluginId","incumbent","find","entry","pluginResourceDraftWriter","one","impl"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAGD,SAASA,sBAAsB,QAAQ,qCAAiC;AACxE,SACEC,2BAA2B,EAC3BC,qBAAqB,EACrBC,qBAAqB,QAChB,uBAAmB;AA8K1B,OAAO,MAAMC,yBACXH,4BAAuD,wBAAwB;IAC7EI,UAAU;AACZ,GAAE;AAEJ;;;;;CAKC,GACD,OAAO,SAASC,kCACdC,QAAgB,EAChBC,MAAiC,EACjCC,OAA+B;QAIbT,MAAAA;IAFlB,MAAMU,MAAMH,SAASI,IAAI;IACzB,IAAI,CAACD,KAAK,MAAM,IAAIE,MAAM;IAC1B,MAAMC,WAAW,EAACb,QAAAA,0BAAAA,oCAAAA,0BAA4BS,2BAAAA,QAASI,QAAQ,YAA7Cb,OAAiD,IAAIW,IAAI;IAC3E,MAAMG,YAAYX,sBAAsBC,wBAAwBW,IAAI,CAClE,CAACC,QAAUA,MAAMN,GAAG,KAAKA;IAE3B,IAAII,aAAaD,YAAYC,UAAUD,QAAQ,KAAKA,UAAU;QAC5D,MAAM,IAAID,MACR,CAAC,UAAU,EAAEF,IAAI,mCAAmC,EAAEI,UAAUD,QAAQ,CAAC,GAAG,CAAC,GAC3E,CAAC,SAAS,EAAEA,SAAS,CAAC,CAAC;IAE7B;IACAX,sBAAsBE,wBAAwBI,QAAQ,aAChDC,CAAAA,2BAAAA,QAASI,QAAQ,IAAG;QAAEA,UAAUJ,QAAQI,QAAQ;IAAC,IAAI,CAAC;QAC1DH;;AAEJ;AAQA,mFAAmF,GACnF,OAAO,SAASO,0BAA0BV,QAAgB;IACxD,MAAMG,MAAMH,SAASI,IAAI;IACzB,MAAMK,QAAQb,sBAAsBC,wBAAwBW,IAAI,CAAC,CAACG,MAAQA,IAAIR,GAAG,KAAKA;IACtF,OAAOM,QAAQ;QAAEH,UAAUG,MAAMH,QAAQ;QAAEL,QAAQQ,MAAMG,IAAI;IAAC,IAAI;AACpE"}
|
|
@@ -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"}
|