@aglyn/aglyn 1.0.0-beta.147 → 1.0.0-beta.150

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 (54) hide show
  1. package/package.json +10 -10
  2. package/src/lib/app-utils/binding-token-catalog.js +9 -2
  3. package/src/lib/app-utils/binding-token-catalog.js.map +1 -1
  4. package/src/lib/app-utils/collection-entries.d.ts +52 -3
  5. package/src/lib/app-utils/collection-entries.js +98 -9
  6. package/src/lib/app-utils/collection-entries.js.map +1 -1
  7. package/src/lib/app-utils/console-routes.d.ts +2 -34
  8. package/src/lib/app-utils/console-routes.js +29 -11
  9. package/src/lib/app-utils/console-routes.js.map +1 -1
  10. package/src/lib/app-utils/contexts.d.ts +1 -0
  11. package/src/lib/app-utils/contexts.js +1 -0
  12. package/src/lib/app-utils/contexts.js.map +1 -1
  13. package/src/lib/app-utils/docs-help.generated.d.ts +13 -1
  14. package/src/lib/app-utils/docs-help.generated.js +37 -0
  15. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  16. package/src/lib/app-utils/docs-index.generated.js +1 -1
  17. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  18. package/src/lib/app-utils/media-src.d.ts +93 -0
  19. package/src/lib/app-utils/media-src.js +114 -0
  20. package/src/lib/app-utils/media-src.js.map +1 -0
  21. package/src/lib/app-utils/notifications.d.ts +136 -2
  22. package/src/lib/app-utils/notifications.js +139 -3
  23. package/src/lib/app-utils/notifications.js.map +1 -1
  24. package/src/lib/app-utils/page-header-help-context.d.ts +83 -0
  25. package/src/lib/app-utils/page-header-help-context.js +56 -0
  26. package/src/lib/app-utils/page-header-help-context.js.map +1 -0
  27. package/src/lib/app-utils/release-flags.js +5 -4
  28. package/src/lib/app-utils/release-flags.js.map +1 -1
  29. package/src/lib/foundation/definitions/host-content-collections.d.ts +42 -40
  30. package/src/lib/foundation/definitions/host-content-collections.js +47 -109
  31. package/src/lib/foundation/definitions/host-content-collections.js.map +1 -1
  32. package/src/lib/plugin-manager/feature-plugins.d.ts +121 -22
  33. package/src/lib/plugin-manager/feature-plugins.js +41 -18
  34. package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
  35. package/src/lib/plugin-manager/first-party-plugins.generated.d.ts +13 -0
  36. package/src/lib/plugin-manager/first-party-plugins.generated.js +257 -0
  37. package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
  38. package/src/lib/plugin-manager/plugin-contact-capture.d.ts +16 -0
  39. package/src/lib/plugin-manager/plugin-contact-capture.js.map +1 -1
  40. package/src/lib/plugin-manager/plugin-host-collections.d.ts +27 -5
  41. package/src/lib/plugin-manager/plugin-host-collections.js +25 -16
  42. package/src/lib/plugin-manager/plugin-host-collections.js.map +1 -1
  43. package/src/lib/plugin-manager/plugin-media-publish.d.ts +88 -0
  44. package/src/lib/plugin-manager/plugin-media-publish.js +118 -0
  45. package/src/lib/plugin-manager/plugin-media-publish.js.map +1 -0
  46. package/src/lib/plugin-manager/plugin-org-capacity.d.ts +57 -0
  47. package/src/lib/plugin-manager/plugin-org-capacity.js +68 -0
  48. package/src/lib/plugin-manager/plugin-org-capacity.js.map +1 -0
  49. package/src/lib/plugin-manager/plugin-route-metadata.d.ts +141 -0
  50. package/src/lib/plugin-manager/plugin-route-metadata.js +140 -0
  51. package/src/lib/plugin-manager/plugin-route-metadata.js.map +1 -0
  52. package/src/lib/plugin-manager/record-captured-contact.d.ts +89 -0
  53. package/src/lib/plugin-manager/record-captured-contact.js +128 -0
  54. package/src/lib/plugin-manager/record-captured-contact.js.map +1 -0
@@ -15,7 +15,8 @@
15
15
  * limitations under the License.
16
16
  */
17
17
  /**
18
- * Which host subcollections the media usage scan reads (AGL-1867).
18
+ * The host subcollections CORE owns, and how the media usage scan reads them
19
+ * (AGL-1867, AGL-3080).
19
20
  *
20
21
  * ## Why this exists, and why it is shaped the way it is
21
22
  *
@@ -34,8 +35,8 @@
34
35
  * feared is only necessary if the default is NOT to scan. So the default here
35
36
  * is inverted:
36
37
  *
37
- * **Every host subcollection is scanned unless it is named below with a
38
- * reason it is not.**
38
+ * **Every host subcollection is scanned unless it is named with a reason it is
39
+ * not.**
39
40
  *
40
41
  * That turns the open question from "does this carry media?" — which needs a
41
42
  * judgement about a schema nobody wrote down — into "is scanning this wasteful
@@ -44,15 +45,27 @@
44
45
  * thought about is READ, so a plugin shipping a new media-bearing collection is
45
46
  * covered the day it lands rather than the day somebody remembers it.
46
47
  *
47
- * ## What keeps the list from rotting
48
+ * ## Core holds core's, and asks the plugins for theirs (AGL-3080)
49
+ *
50
+ * This file used to hold every collection in the product: which ones commerce
51
+ * writes, which ones marketing writes, and which console page each deep-links
52
+ * to. That is the platform keeping a map of its plugins' storage, and the map
53
+ * went stale exactly where a stale map is invisible — three of its deep links
54
+ * pointed at a hub that did not hold the document, and nothing could tell.
55
+ *
56
+ * So a plugin declares its own collections in `plugins.config.json` and the
57
+ * readers ask `plugin-manager/plugin-host-collections`. What is left here is
58
+ * core's own storage, which is the only part core is entitled to name.
59
+ *
60
+ * ## What keeps these lists from rotting
48
61
  *
49
62
  * `host-content-media-coverage.spec.ts` sweeps `apps/**` and `libs/**` for host
50
63
  * subcollection names — the same derived sweep
51
64
  * `host-subcollection-write-deny-coverage.spec.ts` uses, for the same reason —
52
- * and asserts {@link PLUGIN_CONTENT_COLLECTIONS} is EXACTLY that sweep minus
53
- * {@link CORE_CONTENT_COLLECTIONS} minus {@link MEDIA_SCAN_EXCLUDED}. A new
54
- * collection fails the build with a one-line decision to make, and the default
55
- * answer — add it to the scanned list — is also the safe one.
65
+ * and asserts the sweep is EXACTLY these three lists plus the plugins'
66
+ * declarations. A new collection fails the build with a one-line decision to
67
+ * make, and the default answer — declare it scanned, on the plugin that writes
68
+ * it — is also the safe one.
56
69
  *
57
70
  * ## The plugin boundary was NOT crossed to do this
58
71
  *
@@ -86,45 +99,31 @@
86
99
  */
87
100
  export declare const CORE_CONTENT_COLLECTIONS: readonly ["screens", "layouts", "components", "emailTemplates", "collections"];
88
101
  /**
89
- * Host subcollections deliberately NOT read by the media scan, and why.
90
- *
91
- * Every entry has to answer one of two questions: what would scanning it cost,
92
- * or what would scanning it get wrong. "It probably has no images in it" is not
93
- * an accepted reason — that is the guess this file is arranged to avoid making.
94
- */
95
- export declare const MEDIA_SCAN_EXCLUDED: Record<string, string>;
96
- /**
97
- * Host subcollections the media scan reads generically.
102
+ * Core's own host subcollections that the scan reads generically.
98
103
  *
99
104
  * Generically: no per-collection field list and no per-collection decoder.
100
105
  * Each document is flattened by `documentHaystack` — which decodes `nodes` and
101
- * `elements` through `decodeStoredNodes` first, so a plugin that adopts the
102
- * compressed storage form is covered without touching this file — and the
103
- * matching field's dotted path is recovered by walking the document. A field
104
- * list here would be the same staleness trap `referencingFieldPath` was written
105
- * to avoid one level down.
106
- *
107
- * MUST equal the repo-wide host subcollection sweep minus
108
- * {@link CORE_CONTENT_COLLECTIONS} minus {@link MEDIA_SCAN_EXCLUDED}. The guard
109
- * spec asserts exactly that, in both directions, so this list can neither miss
110
- * a new collection nor keep naming a deleted one.
106
+ * `elements` through `decodeStoredNodes` first — and the matching field's
107
+ * dotted path is recovered by walking the document. A field list here would be
108
+ * the same staleness trap `referencingFieldPath` was written to avoid one
109
+ * level down.
110
+ *
111
+ * Short because it is only core's: a blog author, the per-plugin settings
112
+ * document the plugin manager owns, and a site template. Everything else that
113
+ * is scanned generically belongs to a plugin and is declared by it.
111
114
  */
112
- export declare const PLUGIN_CONTENT_COLLECTIONS: readonly ["actions", "authors", "campaigns", "coupons", "discounts", "events", "experiments", "forms", "functions", "installs", "locations", "memberPosts", "overlays", "pluginSettings", "productCategories", "products", "redirects", "resources", "reviews", "services", "settings", "siteMembers", "suppliers", "templates", "variables", "webhooks", "workflows"];
113
- export type PluginContentCollection = (typeof PLUGIN_CONTENT_COLLECTIONS)[number];
115
+ export declare const CORE_GENERIC_SCAN_COLLECTIONS: readonly ["authors", "pluginSettings", "templates"];
114
116
  /**
115
- * Console page slug a plugin-content reference row deep-links to.
117
+ * Core's own host subcollections that the media scan deliberately does NOT
118
+ * read, and why.
116
119
  *
117
- * Optional on purpose, and deliberately not required for a collection to be
118
- * scanned. A row with no destination still renders — as text rather than a
119
- * link — and that is strictly better than the row not existing, which is the
120
- * bug this issue is about. Requiring a slug would have made the deep link a
121
- * precondition for coverage and quietly re-created the hole for every
122
- * collection nobody had got round to routing.
123
- *
124
- * Keyed by collection, valued with the `[pluginSlug]` segment of
125
- * `Route.HOST_PLUGIN`.
120
+ * Every entry has to answer one of two questions: what would scanning it cost,
121
+ * or what would scanning it get wrong. "It probably has no images in it" is not
122
+ * an accepted reason — that is the guess this file is arranged to avoid making.
123
+ * A plugin's exclusions carry the same burden and live in its own declaration,
124
+ * where `mediaScanReason` is required with `mediaScan: 'none'`.
126
125
  */
127
- export declare const PLUGIN_CONTENT_ROUTE_SLUG: Partial<Record<PluginContentCollection, string>>;
126
+ export declare const MEDIA_SCAN_EXCLUDED: Record<string, string>;
128
127
  /**
129
128
  * What to call one of these in a reference row.
130
129
  *
@@ -133,5 +132,8 @@ export declare const PLUGIN_CONTENT_ROUTE_SLUG: Partial<Record<PluginContentColl
133
132
  * `productCategories` reads as "Product category". Singularised with the two
134
133
  * rules English spells consistently enough to be worth automating; anything
135
134
  * else keeps its name, which is a cosmetic miss rather than a coverage one.
135
+ *
136
+ * Shared with `pluginHostCollectionLabel`, which prefers a plugin's declared
137
+ * label and falls back to exactly this.
136
138
  */
137
139
  export declare function hostContentCollectionLabel(collection: string): string;
@@ -14,7 +14,8 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */ /**
17
- * Which host subcollections the media usage scan reads (AGL-1867).
17
+ * The host subcollections CORE owns, and how the media usage scan reads them
18
+ * (AGL-1867, AGL-3080).
18
19
  *
19
20
  * ## Why this exists, and why it is shaped the way it is
20
21
  *
@@ -33,8 +34,8 @@
33
34
  * feared is only necessary if the default is NOT to scan. So the default here
34
35
  * is inverted:
35
36
  *
36
- * **Every host subcollection is scanned unless it is named below with a
37
- * reason it is not.**
37
+ * **Every host subcollection is scanned unless it is named with a reason it is
38
+ * not.**
38
39
  *
39
40
  * That turns the open question from "does this carry media?" — which needs a
40
41
  * judgement about a schema nobody wrote down — into "is scanning this wasteful
@@ -43,15 +44,27 @@
43
44
  * thought about is READ, so a plugin shipping a new media-bearing collection is
44
45
  * covered the day it lands rather than the day somebody remembers it.
45
46
  *
46
- * ## What keeps the list from rotting
47
+ * ## Core holds core's, and asks the plugins for theirs (AGL-3080)
48
+ *
49
+ * This file used to hold every collection in the product: which ones commerce
50
+ * writes, which ones marketing writes, and which console page each deep-links
51
+ * to. That is the platform keeping a map of its plugins' storage, and the map
52
+ * went stale exactly where a stale map is invisible — three of its deep links
53
+ * pointed at a hub that did not hold the document, and nothing could tell.
54
+ *
55
+ * So a plugin declares its own collections in `plugins.config.json` and the
56
+ * readers ask `plugin-manager/plugin-host-collections`. What is left here is
57
+ * core's own storage, which is the only part core is entitled to name.
58
+ *
59
+ * ## What keeps these lists from rotting
47
60
  *
48
61
  * `host-content-media-coverage.spec.ts` sweeps `apps/**` and `libs/**` for host
49
62
  * subcollection names — the same derived sweep
50
63
  * `host-subcollection-write-deny-coverage.spec.ts` uses, for the same reason —
51
- * and asserts {@link PLUGIN_CONTENT_COLLECTIONS} is EXACTLY that sweep minus
52
- * {@link CORE_CONTENT_COLLECTIONS} minus {@link MEDIA_SCAN_EXCLUDED}. A new
53
- * collection fails the build with a one-line decision to make, and the default
54
- * answer — add it to the scanned list — is also the safe one.
64
+ * and asserts the sweep is EXACTLY these three lists plus the plugins'
65
+ * declarations. A new collection fails the build with a one-line decision to
66
+ * make, and the default answer — declare it scanned, on the plugin that writes
67
+ * it — is also the safe one.
55
68
  *
56
69
  * ## The plugin boundary was NOT crossed to do this
57
70
  *
@@ -89,119 +102,41 @@
89
102
  'collections'
90
103
  ];
91
104
  /**
92
- * Host subcollections deliberately NOT read by the media scan, and why.
105
+ * Core's own host subcollections that the scan reads generically.
106
+ *
107
+ * Generically: no per-collection field list and no per-collection decoder.
108
+ * Each document is flattened by `documentHaystack` — which decodes `nodes` and
109
+ * `elements` through `decodeStoredNodes` first — and the matching field's
110
+ * dotted path is recovered by walking the document. A field list here would be
111
+ * the same staleness trap `referencingFieldPath` was written to avoid one
112
+ * level down.
113
+ *
114
+ * Short because it is only core's: a blog author, the per-plugin settings
115
+ * document the plugin manager owns, and a site template. Everything else that
116
+ * is scanned generically belongs to a plugin and is declared by it.
117
+ */ export const CORE_GENERIC_SCAN_COLLECTIONS = [
118
+ 'authors',
119
+ 'pluginSettings',
120
+ 'templates'
121
+ ];
122
+ /**
123
+ * Core's own host subcollections that the media scan deliberately does NOT
124
+ * read, and why.
93
125
  *
94
126
  * Every entry has to answer one of two questions: what would scanning it cost,
95
127
  * or what would scanning it get wrong. "It probably has no images in it" is not
96
128
  * an accepted reason — that is the guess this file is arranged to avoid making.
129
+ * A plugin's exclusions carry the same burden and live in its own declaration,
130
+ * where `mediaScanReason` is required with `mediaScan: 'none'`.
97
131
  */ export const MEDIA_SCAN_EXCLUDED = {
98
- // ── Scanning these would be WRONG, not merely expensive ────────────────
99
- media: 'The library itself. The asset under audit IS a document in here, and its ' + 'own record holds its own url, cdnPath and storage path — every needle ' + 'the scan carries. Reading this collection would report every asset as ' + 'referencing itself, which turns a deletion-safety control into noise ' + 'that nobody reads.',
132
+ media: 'The library itself. The asset under audit IS a document in here, and ' + 'its own record holds its own url, cdnPath and storage path — every ' + 'needle the scan carries. Reading this collection would report every ' + 'asset as referencing itself, which turns a deletion-safety control into ' + 'noise that nobody reads.',
100
133
  mediaFolders: 'Folder records: a name, a parent pointer and a scope. A folder never ' + 'holds an asset reference, and it is not a dependent either — deleting a ' + 'file does not break the folder it sat in.',
101
- emailCampaigns: 'The campaign CONTAINER — a name, a date window, the list ids it is ' + 'aimed at and a topic. Distinct from `campaigns`, which holds the sends ' + 'and IS scanned because a send carries the copy that went out. A ' + 'container carries no copy and no asset reference: the design a send ' + 'renders is a screen, and the screen is where the picker writes.',
102
- // ── Machine-written telemetry: unbounded, and no author picks into it ──
103
134
  analytics: 'Per-day traffic rollups written by the tenant collector. One document ' + 'per day per site forever, no author-editable field, and no surface that ' + 'can pick an asset into one.',
104
135
  screenAnalytics: 'Per-screen traffic history behind the Pro+ panel, written by the same ' + 'collector on the same unbounded per-day cadence.',
105
136
  counters: 'Quota and usage counters, server-owned (AGL-1367) and denied to client ' + 'writes outright. Numbers, not content.',
106
137
  activity: 'The site activity feed — an append-only audit log the console writes a ' + 'row into on every change. Unbounded by design, and a row that quotes an ' + 'asset id is a record that somebody touched it, not a place it is used.',
107
- // ── Transaction and ledger rows: unbounded, and a SNAPSHOT is not a use ─
108
- orders: 'Completed orders. Line items COPY a product\'s `imageUrl` at purchase ' + 'time, so these would match — and matching would be the wrong answer: an ' + 'order is an immutable record of what was sold, deleting the asset ' + 'changes nothing about it, and no author can edit one. A busy store also ' + 'holds more of these than the whole read budget.',
109
- carts: 'Live and abandoned carts, with the same copied line-item image and the ' + 'same reasoning, at higher volume — one document per shopper session.',
110
- checkouts: 'In-flight checkout sessions. Transient buyer state carrying the same ' + 'line-item snapshot as `carts`.',
111
- formSubmissions: 'Visitor form submissions. Unbounded, PII-heavy, and a file attached to ' + 'one is the visitor\'s upload — not a library asset an author picked, and ' + 'not something deleting a library asset would break.',
112
- leads: 'Captured leads, on the same footing as `formSubmissions`: visitor-' + 'submitted, unbounded, and never a place an author places an asset.',
113
- bookings: 'Booking records — a customer, a time and a service pointer. The service ' + 'holds the imagery and IS scanned; a booking is the transaction against ' + 'it.',
114
- campaignAttributions: 'One row per conversion — a form submission, a lead, a contact or a ' + 'booking — recording which campaign the visitor arrived from. Written ' + 'only by the Admin SDK, and the whole record is three sanitized UTM ' + 'strings capped at a hundred characters: there is no field an asset ' + 'could occupy and no surface that could pick one into it. Unbounded on ' + 'the same footing as the conversions it credits.',
115
- reservations: 'POS and booking holds. Short-lived transaction rows pointing at a ' + 'resource that is itself scanned.',
116
- stockHolds: 'Inventory holds taken during checkout. Machine-written, short-lived, ' + 'and a quantity rather than content.',
117
- subscriptions: 'Site membership subscriptions: a plan pointer, a status and Stripe ' + 'ids. Server-written from the billing webhook.',
118
- registers: 'POS register allocations — a count against the register add-on, read by ' + 'billing (AGL-1775). No content field at all.',
119
- giftCards: 'Gift card balances and redemption history. Money, not content.',
120
- licenseKeys: 'Digital-product license keys issued at fulfilment: a code, an order ' + 'pointer and a revocation flag.',
121
- inventoryAdjustments: 'The append-only stock adjustment ledger (AGL-2269). One row per manual ' + 'stock edit and per cancellation release, unbounded over a store\'s life.',
122
- inventoryReconciliation: 'Reconciliation runs comparing counted stock against recorded stock. ' + 'Machine-written totals.',
123
- restockAlerts: 'Back-in-stock requests: an email address and a product pointer, written ' + 'by visitors.',
124
- suppressions: 'Unsubscribes, bounces and spam complaints. Email addresses and a ' + 'reason.',
125
- stripeTaxRates: 'Cached Stripe tax rate ids, written by the tax sync. Vendor ids.',
126
138
  members: 'The ORG-facing member roster mirrored onto the site, server-owned ' + '(AGL-1367). A member\'s photo is their own account photo, uploaded ' + 'through /api/account/photo and stored outside any site\'s library, so it ' + 'is not an asset this scan can be asked about.'
127
139
  };
128
- /**
129
- * Host subcollections the media scan reads generically.
130
- *
131
- * Generically: no per-collection field list and no per-collection decoder.
132
- * Each document is flattened by `documentHaystack` — which decodes `nodes` and
133
- * `elements` through `decodeStoredNodes` first, so a plugin that adopts the
134
- * compressed storage form is covered without touching this file — and the
135
- * matching field's dotted path is recovered by walking the document. A field
136
- * list here would be the same staleness trap `referencingFieldPath` was written
137
- * to avoid one level down.
138
- *
139
- * MUST equal the repo-wide host subcollection sweep minus
140
- * {@link CORE_CONTENT_COLLECTIONS} minus {@link MEDIA_SCAN_EXCLUDED}. The guard
141
- * spec asserts exactly that, in both directions, so this list can neither miss
142
- * a new collection nor keep naming a deleted one.
143
- */ export const PLUGIN_CONTENT_COLLECTIONS = [
144
- 'actions',
145
- 'authors',
146
- 'campaigns',
147
- 'coupons',
148
- 'discounts',
149
- 'events',
150
- 'experiments',
151
- 'forms',
152
- 'functions',
153
- 'installs',
154
- 'locations',
155
- 'memberPosts',
156
- 'overlays',
157
- 'pluginSettings',
158
- 'productCategories',
159
- 'products',
160
- 'redirects',
161
- 'resources',
162
- 'reviews',
163
- 'services',
164
- 'settings',
165
- 'siteMembers',
166
- 'suppliers',
167
- 'templates',
168
- 'variables',
169
- 'webhooks',
170
- 'workflows'
171
- ];
172
- /**
173
- * Console page slug a plugin-content reference row deep-links to.
174
- *
175
- * Optional on purpose, and deliberately not required for a collection to be
176
- * scanned. A row with no destination still renders — as text rather than a
177
- * link — and that is strictly better than the row not existing, which is the
178
- * bug this issue is about. Requiring a slug would have made the deep link a
179
- * precondition for coverage and quietly re-created the hole for every
180
- * collection nobody had got round to routing.
181
- *
182
- * Keyed by collection, valued with the `[pluginSlug]` segment of
183
- * `Route.HOST_PLUGIN`.
184
- */ export const PLUGIN_CONTENT_ROUTE_SLUG = {
185
- products: 'products',
186
- productCategories: 'products',
187
- suppliers: 'products',
188
- locations: 'products',
189
- coupons: 'products',
190
- discounts: 'products',
191
- reviews: 'products',
192
- memberPosts: 'products',
193
- events: 'events',
194
- services: 'bookings',
195
- resources: 'bookings',
196
- redirects: 'redirects',
197
- campaigns: 'marketing',
198
- experiments: 'marketing',
199
- workflows: 'workflows',
200
- webhooks: 'workflows',
201
- functions: 'logic',
202
- variables: 'logic',
203
- actions: 'logic'
204
- };
205
140
  /**
206
141
  * What to call one of these in a reference row.
207
142
  *
@@ -210,6 +145,9 @@
210
145
  * `productCategories` reads as "Product category". Singularised with the two
211
146
  * rules English spells consistently enough to be worth automating; anything
212
147
  * else keeps its name, which is a cosmetic miss rather than a coverage one.
148
+ *
149
+ * Shared with `pluginHostCollectionLabel`, which prefers a plugin's declared
150
+ * label and falls back to exactly this.
213
151
  */ export function hostContentCollectionLabel(collection) {
214
152
  const singular = /ies$/.test(collection) ? collection.replace(/ies$/, 'y') : /(ss|s)$/.test(collection) ? collection.replace(/s$/, '') : collection;
215
153
  const spaced = singular.replace(/([a-z0-9])([A-Z])/g, '$1 $2').toLowerCase();
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/aglyn/src/lib/foundation/definitions/host-content-collections.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 * Which host subcollections the media usage scan reads (AGL-1867).\n *\n * ## Why this exists, and why it is shaped the way it is\n *\n * `scanMediaReferences` answers \"what uses this asset\" immediately before an\n * author deletes it, and every way it can be wrong points at \"unused\". AGL-1413\n * closed the screen/layout/component/host-field half. AGL-1867 closed emails.\n * What was left was plugin-owned content: a commerce product carries `imageUrl`\n * and `mediaUrls`, so a product photo used nowhere else reported as unused, and\n * the panel said so with a straight face.\n *\n * An earlier pass at this issue concluded the gap could not be closed by hand,\n * because closing it looked like classifying fifty-odd collection names as\n * media-bearing or not — \"a large list of guesses dressed as decisions, and the\n * first stale entry is where the next hole hides\". That reasoning was right\n * about the hazard and wrong about the shape of the fix. The classification it\n * feared is only necessary if the default is NOT to scan. So the default here\n * is inverted:\n *\n * **Every host subcollection is scanned unless it is named below with a\n * reason it is not.**\n *\n * That turns the open question from \"does this carry media?\" — which needs a\n * judgement about a schema nobody wrote down — into \"is scanning this wasteful\n * or wrong?\", which is answerable from the collection's own volume and who\n * writes it. It also puts the failure on the safe side: a collection nobody has\n * thought about is READ, so a plugin shipping a new media-bearing collection is\n * covered the day it lands rather than the day somebody remembers it.\n *\n * ## What keeps the list from rotting\n *\n * `host-content-media-coverage.spec.ts` sweeps `apps/**` and `libs/**` for host\n * subcollection names — the same derived sweep\n * `host-subcollection-write-deny-coverage.spec.ts` uses, for the same reason —\n * and asserts {@link PLUGIN_CONTENT_COLLECTIONS} is EXACTLY that sweep minus\n * {@link CORE_CONTENT_COLLECTIONS} minus {@link MEDIA_SCAN_EXCLUDED}. A new\n * collection fails the build with a one-line decision to make, and the default\n * answer — add it to the scanned list — is also the safe one.\n *\n * ## The plugin boundary was NOT crossed to do this\n *\n * Worth stating, because \"read plugin data into a first-party corpus\" sounds\n * like it should be. It is not, and the reason is that \"plugin\" means two\n * disjoint things:\n *\n * - a SANDBOXED marketplace plugin runs in an iframe on a separate origin and\n * has no Firestore access at all — `plugin-bridge.ts` is the whole protocol\n * and its guest verbs are `ready`, `resize`, `event`, `fetch-request` and\n * `error`, with `parseGuestMessage` rejecting anything else. It owns no\n * documents, so there is nothing here to reach into.\n * - a FIRST-PARTY feature plugin under `libs/plugins/*` is compiled into the\n * apps and writes ORDINARY host subcollections with the ordinary SDKs. Those\n * documents already sit under the same `hosts/{hostId}` rules as a screen,\n * are already read by first-party console pages, and are already covered by\n * the same scope check the scan runs before it reads anything.\n *\n * So the corpus grows to include documents the caller could already read\n * through the console, under an authorization decision that has not changed.\n * Nothing in the sandbox boundary is bypassed, because no sandboxed guest owns\n * any of this.\n */\n\n/**\n * Host subcollections the media scan reads through its own dedicated machinery.\n *\n * Not \"excluded\" — covered, by a pass that knows their shape. Screens, layouts,\n * components and email templates each hang a node tree off\n * `…/versions/{versionId}` and get the published-version and history passes;\n * `collections` is walked one level deeper, into `…/entries`.\n */\nexport const CORE_CONTENT_COLLECTIONS = [\n 'screens',\n 'layouts',\n 'components',\n 'emailTemplates',\n 'collections',\n] as const\n\n/**\n * Host subcollections deliberately NOT read by the media scan, and why.\n *\n * Every entry has to answer one of two questions: what would scanning it cost,\n * or what would scanning it get wrong. \"It probably has no images in it\" is not\n * an accepted reason — that is the guess this file is arranged to avoid making.\n */\nexport const MEDIA_SCAN_EXCLUDED: Record<string, string> = {\n // ── Scanning these would be WRONG, not merely expensive ────────────────\n media:\n 'The library itself. The asset under audit IS a document in here, and its ' +\n 'own record holds its own url, cdnPath and storage path — every needle ' +\n 'the scan carries. Reading this collection would report every asset as ' +\n 'referencing itself, which turns a deletion-safety control into noise ' +\n 'that nobody reads.',\n mediaFolders:\n 'Folder records: a name, a parent pointer and a scope. A folder never ' +\n 'holds an asset reference, and it is not a dependent either — deleting a ' +\n 'file does not break the folder it sat in.',\n emailCampaigns:\n 'The campaign CONTAINER — a name, a date window, the list ids it is ' +\n 'aimed at and a topic. Distinct from `campaigns`, which holds the sends ' +\n 'and IS scanned because a send carries the copy that went out. A ' +\n 'container carries no copy and no asset reference: the design a send ' +\n 'renders is a screen, and the screen is where the picker writes.',\n\n // ── Machine-written telemetry: unbounded, and no author picks into it ──\n analytics:\n 'Per-day traffic rollups written by the tenant collector. One document ' +\n 'per day per site forever, no author-editable field, and no surface that ' +\n 'can pick an asset into one.',\n screenAnalytics:\n 'Per-screen traffic history behind the Pro+ panel, written by the same ' +\n 'collector on the same unbounded per-day cadence.',\n counters:\n 'Quota and usage counters, server-owned (AGL-1367) and denied to client ' +\n 'writes outright. Numbers, not content.',\n activity:\n 'The site activity feed — an append-only audit log the console writes a ' +\n 'row into on every change. Unbounded by design, and a row that quotes an ' +\n 'asset id is a record that somebody touched it, not a place it is used.',\n\n // ── Transaction and ledger rows: unbounded, and a SNAPSHOT is not a use ─\n orders:\n 'Completed orders. Line items COPY a product\\'s `imageUrl` at purchase ' +\n 'time, so these would match — and matching would be the wrong answer: an ' +\n 'order is an immutable record of what was sold, deleting the asset ' +\n 'changes nothing about it, and no author can edit one. A busy store also ' +\n 'holds more of these than the whole read budget.',\n carts:\n 'Live and abandoned carts, with the same copied line-item image and the ' +\n 'same reasoning, at higher volume — one document per shopper session.',\n checkouts:\n 'In-flight checkout sessions. Transient buyer state carrying the same ' +\n 'line-item snapshot as `carts`.',\n formSubmissions:\n 'Visitor form submissions. Unbounded, PII-heavy, and a file attached to ' +\n 'one is the visitor\\'s upload — not a library asset an author picked, and ' +\n 'not something deleting a library asset would break.',\n leads:\n 'Captured leads, on the same footing as `formSubmissions`: visitor-' +\n 'submitted, unbounded, and never a place an author places an asset.',\n bookings:\n 'Booking records — a customer, a time and a service pointer. The service ' +\n 'holds the imagery and IS scanned; a booking is the transaction against ' +\n 'it.',\n campaignAttributions:\n 'One row per conversion — a form submission, a lead, a contact or a ' +\n 'booking — recording which campaign the visitor arrived from. Written ' +\n 'only by the Admin SDK, and the whole record is three sanitized UTM ' +\n 'strings capped at a hundred characters: there is no field an asset ' +\n 'could occupy and no surface that could pick one into it. Unbounded on ' +\n 'the same footing as the conversions it credits.',\n reservations:\n 'POS and booking holds. Short-lived transaction rows pointing at a ' +\n 'resource that is itself scanned.',\n stockHolds:\n 'Inventory holds taken during checkout. Machine-written, short-lived, ' +\n 'and a quantity rather than content.',\n subscriptions:\n 'Site membership subscriptions: a plan pointer, a status and Stripe ' +\n 'ids. Server-written from the billing webhook.',\n registers:\n 'POS register allocations — a count against the register add-on, read by ' +\n 'billing (AGL-1775). No content field at all.',\n giftCards:\n 'Gift card balances and redemption history. Money, not content.',\n licenseKeys:\n 'Digital-product license keys issued at fulfilment: a code, an order ' +\n 'pointer and a revocation flag.',\n inventoryAdjustments:\n 'The append-only stock adjustment ledger (AGL-2269). One row per manual ' +\n 'stock edit and per cancellation release, unbounded over a store\\'s life.',\n inventoryReconciliation:\n 'Reconciliation runs comparing counted stock against recorded stock. ' +\n 'Machine-written totals.',\n restockAlerts:\n 'Back-in-stock requests: an email address and a product pointer, written ' +\n 'by visitors.',\n suppressions:\n 'Unsubscribes, bounces and spam complaints. Email addresses and a ' +\n 'reason.',\n stripeTaxRates:\n 'Cached Stripe tax rate ids, written by the tax sync. Vendor ids.',\n members:\n 'The ORG-facing member roster mirrored onto the site, server-owned ' +\n '(AGL-1367). A member\\'s photo is their own account photo, uploaded ' +\n 'through /api/account/photo and stored outside any site\\'s library, so it ' +\n 'is not an asset this scan can be asked about.',\n}\n\n/**\n * Host subcollections the media scan reads generically.\n *\n * Generically: no per-collection field list and no per-collection decoder.\n * Each document is flattened by `documentHaystack` — which decodes `nodes` and\n * `elements` through `decodeStoredNodes` first, so a plugin that adopts the\n * compressed storage form is covered without touching this file — and the\n * matching field's dotted path is recovered by walking the document. A field\n * list here would be the same staleness trap `referencingFieldPath` was written\n * to avoid one level down.\n *\n * MUST equal the repo-wide host subcollection sweep minus\n * {@link CORE_CONTENT_COLLECTIONS} minus {@link MEDIA_SCAN_EXCLUDED}. The guard\n * spec asserts exactly that, in both directions, so this list can neither miss\n * a new collection nor keep naming a deleted one.\n */\nexport const PLUGIN_CONTENT_COLLECTIONS = [\n 'actions',\n 'authors',\n 'campaigns',\n 'coupons',\n 'discounts',\n 'events',\n 'experiments',\n 'forms',\n 'functions',\n 'installs',\n 'locations',\n 'memberPosts',\n 'overlays',\n 'pluginSettings',\n 'productCategories',\n 'products',\n 'redirects',\n 'resources',\n 'reviews',\n 'services',\n 'settings',\n 'siteMembers',\n 'suppliers',\n 'templates',\n 'variables',\n 'webhooks',\n 'workflows',\n] as const\n\nexport type PluginContentCollection =\n (typeof PLUGIN_CONTENT_COLLECTIONS)[number]\n\n/**\n * Console page slug a plugin-content reference row deep-links to.\n *\n * Optional on purpose, and deliberately not required for a collection to be\n * scanned. A row with no destination still renders — as text rather than a\n * link — and that is strictly better than the row not existing, which is the\n * bug this issue is about. Requiring a slug would have made the deep link a\n * precondition for coverage and quietly re-created the hole for every\n * collection nobody had got round to routing.\n *\n * Keyed by collection, valued with the `[pluginSlug]` segment of\n * `Route.HOST_PLUGIN`.\n */\nexport const PLUGIN_CONTENT_ROUTE_SLUG: Partial<\n Record<PluginContentCollection, string>\n> = {\n products: 'products',\n productCategories: 'products',\n suppliers: 'products',\n locations: 'products',\n coupons: 'products',\n discounts: 'products',\n reviews: 'products',\n memberPosts: 'products',\n events: 'events',\n services: 'bookings',\n resources: 'bookings',\n redirects: 'redirects',\n campaigns: 'marketing',\n experiments: 'marketing',\n workflows: 'workflows',\n webhooks: 'workflows',\n functions: 'logic',\n variables: 'logic',\n actions: 'logic',\n}\n\n/**\n * What to call one of these in a reference row.\n *\n * Derived from the collection name rather than declared, so a newly scanned\n * collection gets a readable label without a second list to keep in step —\n * `productCategories` reads as \"Product category\". Singularised with the two\n * rules English spells consistently enough to be worth automating; anything\n * else keeps its name, which is a cosmetic miss rather than a coverage one.\n */\nexport function hostContentCollectionLabel(collection: string): string {\n const singular = /ies$/.test(collection)\n ? collection.replace(/ies$/, 'y')\n : /(ss|s)$/.test(collection)\n ? collection.replace(/s$/, '')\n : collection\n const spaced = singular.replace(/([a-z0-9])([A-Z])/g, '$1 $2').toLowerCase()\n return spaced.charAt(0).toUpperCase() + spaced.slice(1)\n}\n"],"names":["CORE_CONTENT_COLLECTIONS","MEDIA_SCAN_EXCLUDED","media","mediaFolders","emailCampaigns","analytics","screenAnalytics","counters","activity","orders","carts","checkouts","formSubmissions","leads","bookings","campaignAttributions","reservations","stockHolds","subscriptions","registers","giftCards","licenseKeys","inventoryAdjustments","inventoryReconciliation","restockAlerts","suppressions","stripeTaxRates","members","PLUGIN_CONTENT_COLLECTIONS","PLUGIN_CONTENT_ROUTE_SLUG","products","productCategories","suppliers","locations","coupons","discounts","reviews","memberPosts","events","services","resources","redirects","campaigns","experiments","workflows","webhooks","functions","variables","actions","hostContentCollectionLabel","collection","singular","test","replace","spaced","toLowerCase","charAt","toUpperCase","slice"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6DC,GAED;;;;;;;CAOC,GACD,OAAO,MAAMA,2BAA2B;IACtC;IACA;IACA;IACA;IACA;CACD,CAAS;AAEV;;;;;;CAMC,GACD,OAAO,MAAMC,sBAA8C;IACzD,0EAA0E;IAC1EC,OACE,8EACA,2EACA,2EACA,0EACA;IACFC,cACE,0EACA,6EACA;IACFC,gBACE,wEACA,4EACA,qEACA,yEACA;IAEF,0EAA0E;IAC1EC,WACE,2EACA,6EACA;IACFC,iBACE,2EACA;IACFC,UACE,4EACA;IACFC,UACE,4EACA,6EACA;IAEF,2EAA2E;IAC3EC,QACE,2EACA,6EACA,uEACA,6EACA;IACFC,OACE,4EACA;IACFC,WACE,0EACA;IACFC,iBACE,4EACA,8EACA;IACFC,OACE,uEACA;IACFC,UACE,6EACA,4EACA;IACFC,sBACE,wEACA,0EACA,wEACA,wEACA,2EACA;IACFC,cACE,uEACA;IACFC,YACE,0EACA;IACFC,eACE,wEACA;IACFC,WACE,6EACA;IACFC,WACE;IACFC,aACE,yEACA;IACFC,sBACE,4EACA;IACFC,yBACE,yEACA;IACFC,eACE,6EACA;IACFC,cACE,sEACA;IACFC,gBACE;IACFC,SACE,uEACA,wEACA,8EACA;AACJ,EAAC;AAED;;;;;;;;;;;;;;;CAeC,GACD,OAAO,MAAMC,6BAA6B;IACxC;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,CAAS;AAKV;;;;;;;;;;;;CAYC,GACD,OAAO,MAAMC,4BAET;IACFC,UAAU;IACVC,mBAAmB;IACnBC,WAAW;IACXC,WAAW;IACXC,SAAS;IACTC,WAAW;IACXC,SAAS;IACTC,aAAa;IACbC,QAAQ;IACRC,UAAU;IACVC,WAAW;IACXC,WAAW;IACXC,WAAW;IACXC,aAAa;IACbC,WAAW;IACXC,UAAU;IACVC,WAAW;IACXC,WAAW;IACXC,SAAS;AACX,EAAC;AAED;;;;;;;;CAQC,GACD,OAAO,SAASC,2BAA2BC,UAAkB;IAC3D,MAAMC,WAAW,OAAOC,IAAI,CAACF,cACzBA,WAAWG,OAAO,CAAC,QAAQ,OAC3B,UAAUD,IAAI,CAACF,cACbA,WAAWG,OAAO,CAAC,MAAM,MACzBH;IACN,MAAMI,SAASH,SAASE,OAAO,CAAC,sBAAsB,SAASE,WAAW;IAC1E,OAAOD,OAAOE,MAAM,CAAC,GAAGC,WAAW,KAAKH,OAAOI,KAAK,CAAC;AACvD"}
1
+ {"version":3,"sources":["../../../../../../../libs/aglyn/src/lib/foundation/definitions/host-content-collections.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 * The host subcollections CORE owns, and how the media usage scan reads them\n * (AGL-1867, AGL-3080).\n *\n * ## Why this exists, and why it is shaped the way it is\n *\n * `scanMediaReferences` answers \"what uses this asset\" immediately before an\n * author deletes it, and every way it can be wrong points at \"unused\". AGL-1413\n * closed the screen/layout/component/host-field half. AGL-1867 closed emails.\n * What was left was plugin-owned content: a commerce product carries `imageUrl`\n * and `mediaUrls`, so a product photo used nowhere else reported as unused, and\n * the panel said so with a straight face.\n *\n * An earlier pass at this issue concluded the gap could not be closed by hand,\n * because closing it looked like classifying fifty-odd collection names as\n * media-bearing or not — \"a large list of guesses dressed as decisions, and the\n * first stale entry is where the next hole hides\". That reasoning was right\n * about the hazard and wrong about the shape of the fix. The classification it\n * feared is only necessary if the default is NOT to scan. So the default here\n * is inverted:\n *\n * **Every host subcollection is scanned unless it is named with a reason it is\n * not.**\n *\n * That turns the open question from \"does this carry media?\" — which needs a\n * judgement about a schema nobody wrote down — into \"is scanning this wasteful\n * or wrong?\", which is answerable from the collection's own volume and who\n * writes it. It also puts the failure on the safe side: a collection nobody has\n * thought about is READ, so a plugin shipping a new media-bearing collection is\n * covered the day it lands rather than the day somebody remembers it.\n *\n * ## Core holds core's, and asks the plugins for theirs (AGL-3080)\n *\n * This file used to hold every collection in the product: which ones commerce\n * writes, which ones marketing writes, and which console page each deep-links\n * to. That is the platform keeping a map of its plugins' storage, and the map\n * went stale exactly where a stale map is invisible — three of its deep links\n * pointed at a hub that did not hold the document, and nothing could tell.\n *\n * So a plugin declares its own collections in `plugins.config.json` and the\n * readers ask `plugin-manager/plugin-host-collections`. What is left here is\n * core's own storage, which is the only part core is entitled to name.\n *\n * ## What keeps these lists from rotting\n *\n * `host-content-media-coverage.spec.ts` sweeps `apps/**` and `libs/**` for host\n * subcollection names — the same derived sweep\n * `host-subcollection-write-deny-coverage.spec.ts` uses, for the same reason —\n * and asserts the sweep is EXACTLY these three lists plus the plugins'\n * declarations. A new collection fails the build with a one-line decision to\n * make, and the default answer — declare it scanned, on the plugin that writes\n * it — is also the safe one.\n *\n * ## The plugin boundary was NOT crossed to do this\n *\n * Worth stating, because \"read plugin data into a first-party corpus\" sounds\n * like it should be. It is not, and the reason is that \"plugin\" means two\n * disjoint things:\n *\n * - a SANDBOXED marketplace plugin runs in an iframe on a separate origin and\n * has no Firestore access at all — `plugin-bridge.ts` is the whole protocol\n * and its guest verbs are `ready`, `resize`, `event`, `fetch-request` and\n * `error`, with `parseGuestMessage` rejecting anything else. It owns no\n * documents, so there is nothing here to reach into.\n * - a FIRST-PARTY feature plugin under `libs/plugins/*` is compiled into the\n * apps and writes ORDINARY host subcollections with the ordinary SDKs. Those\n * documents already sit under the same `hosts/{hostId}` rules as a screen,\n * are already read by first-party console pages, and are already covered by\n * the same scope check the scan runs before it reads anything.\n *\n * So the corpus grows to include documents the caller could already read\n * through the console, under an authorization decision that has not changed.\n * Nothing in the sandbox boundary is bypassed, because no sandboxed guest owns\n * any of this.\n */\n\n/**\n * Host subcollections the media scan reads through its own dedicated machinery.\n *\n * Not \"excluded\" — covered, by a pass that knows their shape. Screens, layouts,\n * components and email templates each hang a node tree off\n * `…/versions/{versionId}` and get the published-version and history passes;\n * `collections` is walked one level deeper, into `…/entries`.\n */\nexport const CORE_CONTENT_COLLECTIONS = [\n 'screens',\n 'layouts',\n 'components',\n 'emailTemplates',\n 'collections',\n] as const\n\n/**\n * Core's own host subcollections that the scan reads generically.\n *\n * Generically: no per-collection field list and no per-collection decoder.\n * Each document is flattened by `documentHaystack` — which decodes `nodes` and\n * `elements` through `decodeStoredNodes` first — and the matching field's\n * dotted path is recovered by walking the document. A field list here would be\n * the same staleness trap `referencingFieldPath` was written to avoid one\n * level down.\n *\n * Short because it is only core's: a blog author, the per-plugin settings\n * document the plugin manager owns, and a site template. Everything else that\n * is scanned generically belongs to a plugin and is declared by it.\n */\nexport const CORE_GENERIC_SCAN_COLLECTIONS = [\n 'authors',\n 'pluginSettings',\n 'templates',\n] as const\n\n/**\n * Core's own host subcollections that the media scan deliberately does NOT\n * read, and why.\n *\n * Every entry has to answer one of two questions: what would scanning it cost,\n * or what would scanning it get wrong. \"It probably has no images in it\" is not\n * an accepted reason — that is the guess this file is arranged to avoid making.\n * A plugin's exclusions carry the same burden and live in its own declaration,\n * where `mediaScanReason` is required with `mediaScan: 'none'`.\n */\nexport const MEDIA_SCAN_EXCLUDED: Record<string, string> = {\n media:\n 'The library itself. The asset under audit IS a document in here, and ' +\n 'its own record holds its own url, cdnPath and storage path — every ' +\n 'needle the scan carries. Reading this collection would report every ' +\n 'asset as referencing itself, which turns a deletion-safety control into ' +\n 'noise that nobody reads.',\n mediaFolders:\n 'Folder records: a name, a parent pointer and a scope. A folder never ' +\n 'holds an asset reference, and it is not a dependent either — deleting a ' +\n 'file does not break the folder it sat in.',\n analytics:\n 'Per-day traffic rollups written by the tenant collector. One document ' +\n 'per day per site forever, no author-editable field, and no surface that ' +\n 'can pick an asset into one.',\n screenAnalytics:\n 'Per-screen traffic history behind the Pro+ panel, written by the same ' +\n 'collector on the same unbounded per-day cadence.',\n counters:\n 'Quota and usage counters, server-owned (AGL-1367) and denied to client ' +\n 'writes outright. Numbers, not content.',\n activity:\n 'The site activity feed — an append-only audit log the console writes a ' +\n 'row into on every change. Unbounded by design, and a row that quotes an ' +\n 'asset id is a record that somebody touched it, not a place it is used.',\n members:\n 'The ORG-facing member roster mirrored onto the site, server-owned ' +\n '(AGL-1367). A member\\'s photo is their own account photo, uploaded ' +\n 'through /api/account/photo and stored outside any site\\'s library, so it ' +\n 'is not an asset this scan can be asked about.',\n}\n\n/**\n * What to call one of these in a reference row.\n *\n * Derived from the collection name rather than declared, so a newly scanned\n * collection gets a readable label without a second list to keep in step —\n * `productCategories` reads as \"Product category\". Singularised with the two\n * rules English spells consistently enough to be worth automating; anything\n * else keeps its name, which is a cosmetic miss rather than a coverage one.\n *\n * Shared with `pluginHostCollectionLabel`, which prefers a plugin's declared\n * label and falls back to exactly this.\n */\nexport function hostContentCollectionLabel(collection: string): string {\n const singular = /ies$/.test(collection)\n ? collection.replace(/ies$/, 'y')\n : /(ss|s)$/.test(collection)\n ? collection.replace(/s$/, '')\n : collection\n const spaced = singular.replace(/([a-z0-9])([A-Z])/g, '$1 $2').toLowerCase()\n return spaced.charAt(0).toUpperCase() + spaced.slice(1)\n}\n"],"names":["CORE_CONTENT_COLLECTIONS","CORE_GENERIC_SCAN_COLLECTIONS","MEDIA_SCAN_EXCLUDED","media","mediaFolders","analytics","screenAnalytics","counters","activity","members","hostContentCollectionLabel","collection","singular","test","replace","spaced","toLowerCase","charAt","toUpperCase","slice"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0EC,GAED;;;;;;;CAOC,GACD,OAAO,MAAMA,2BAA2B;IACtC;IACA;IACA;IACA;IACA;CACD,CAAS;AAEV;;;;;;;;;;;;;CAaC,GACD,OAAO,MAAMC,gCAAgC;IAC3C;IACA;IACA;CACD,CAAS;AAEV;;;;;;;;;CASC,GACD,OAAO,MAAMC,sBAA8C;IACzDC,OACE,0EACA,wEACA,yEACA,6EACA;IACFC,cACE,0EACA,6EACA;IACFC,WACE,2EACA,6EACA;IACFC,iBACE,2EACA;IACFC,UACE,4EACA;IACFC,UACE,4EACA,6EACA;IACFC,SACE,uEACA,wEACA,8EACA;AACJ,EAAC;AAED;;;;;;;;;;;CAWC,GACD,OAAO,SAASC,2BAA2BC,UAAkB;IAC3D,MAAMC,WAAW,OAAOC,IAAI,CAACF,cACzBA,WAAWG,OAAO,CAAC,QAAQ,OAC3B,UAAUD,IAAI,CAACF,cACbA,WAAWG,OAAO,CAAC,MAAM,MACzBH;IACN,MAAMI,SAASH,SAASE,OAAO,CAAC,sBAAsB,SAASE,WAAW;IAC1E,OAAOD,OAAOE,MAAM,CAAC,GAAGC,WAAW,KAAKH,OAAOI,KAAK,CAAC;AACvD"}
@@ -84,6 +84,17 @@ export interface ConsolePluginOrgHost {
84
84
  */
85
85
  export interface ConsolePluginOrgMount {
86
86
  orgId: string;
87
+ /**
88
+ * The organization's URL slug (AGL-3080) — the segment its console pages
89
+ * hang under, and what a surface needs to link to one of its siblings.
90
+ *
91
+ * Carried because plugins were reconstructing it: from `hostsPath` by
92
+ * splitting a string, or by resolving a site's org through an async lookup
93
+ * that can come back empty and leave a link unbuilt (AGL-867). The shell
94
+ * has it synchronously; every plugin paying for it again, differently, is
95
+ * the cost of not handing it over.
96
+ */
97
+ orgSlug: string;
87
98
  hosts: readonly ConsolePluginOrgHost[];
88
99
  hostsReady: boolean;
89
100
  /**
@@ -94,6 +105,27 @@ export interface ConsolePluginOrgMount {
94
105
  * cannot import the console's route table.
95
106
  */
96
107
  hostsPath: string;
108
+ /**
109
+ * Where this organization manages its PLAN — the console's billing page
110
+ * for the org (AGL-3080).
111
+ *
112
+ * Here for the same reason `hostsPath` is: a plugin cannot import the
113
+ * console's route table, and a surface that has to say "on a paid plan the
114
+ * cut is lower" is useless without somewhere to send the person who just
115
+ * read it. The shell's own upgrade notice covers an ENTITLEMENT refusal,
116
+ * where the surface never renders; this covers the case the surface renders
117
+ * fine and the plan is still the answer — a marketplace publisher seeing
118
+ * the free-plan fee on a listing they are about to price.
119
+ *
120
+ * OPTIONAL, and a plugin must branch on it rather than assume it: a
121
+ * deployment that bills nobody has no such page, and a self-hoster who
122
+ * removed it should not get a plugin's link into a 404. The shell's own
123
+ * upgrade notice makes the same allowance. `resolveOrgMount` supplies it
124
+ * for every mount this console builds.
125
+ *
126
+ * A plugin renders a link to it or does not, and never parses it.
127
+ */
128
+ billingPath?: string;
97
129
  }
98
130
  /**
99
131
  * Props every plugin-contributed console page receives from the shell's
@@ -113,6 +145,26 @@ export interface ConsolePluginPageProps {
113
145
  hostId: string | null;
114
146
  /** Present only at an org-level mount — see {@link ConsolePluginOrgMount}. */
115
147
  orgMount?: ConsolePluginOrgMount;
148
+ /**
149
+ * Every workspace the SIGNED-IN PERSON belongs to, as the org switcher
150
+ * already names them (AGL-3080) — not the mounted org's siblings, and
151
+ * nothing about what any of them contain.
152
+ *
153
+ * For a surface whose subject crosses workspaces. The marketplace's
154
+ * licences panel is the case: a purchase licenses one organization, so
155
+ * "I bought this once — which workspace did the licence land in?" is a
156
+ * question about the BUYER, and the answer is a name the reader already
157
+ * sees in the switcher. Resolving it from ids would be a read per row of
158
+ * documents the shell is already holding.
159
+ *
160
+ * Absent when the shell has not resolved them, and a surface must name the
161
+ * id rather than wait: this is a label, and a row with a raw id in it is
162
+ * worse than nothing only if the row does not appear at all.
163
+ */
164
+ viewerOrgs?: readonly {
165
+ id: string;
166
+ name: string;
167
+ }[];
116
168
  /** True when the org holds the extension's `featureFlag` entitlement. */
117
169
  entitled: boolean;
118
170
  /**
@@ -495,29 +547,34 @@ export declare const CONSOLE_WIDGET_SLOTS: {
495
547
  readonly orgData: "orgData";
496
548
  /** Besigner functions (ƒx) panel. Props: hostId. */
497
549
  readonly besignerFunctions: "besignerFunctions";
498
- /** Marketplace listing detail body. Props: hostId, listingId, permissions. */
499
- readonly marketplaceListing: "marketplaceListing";
500
- /**
501
- * Above the whole Marketplace subtree: what THIS DEPLOYMENT cannot do
502
- * (AGL-2019, moved out of the console app by AGL-3080). No props — the
503
- * widget reads {@link DeploymentCapabilities} from the context the org's
504
- * server layout provides, and draws nothing when the deployment is
505
- * configured.
506
- *
507
- * A zone rather than a component the app holds, because the sentence is
508
- * the marketplace's: it names what still works without a Stripe platform
509
- * (browsing, free installs) and that is the plugin's knowledge, not the
510
- * shell's. The app supplies only the fact.
511
- */
512
- readonly marketplaceCapability: "marketplaceCapability";
513
550
  /**
514
- * Org marketplace browse body (AGL-772). Props: hostId (acting site),
515
- * permissions, orgScoped. The single org-scope place to browse + install,
516
- * replacing the per-site marketplace tab.
517
- */
518
- readonly orgMarketplace: "orgMarketplace";
519
- /** Plugins & add-ons hub installs section. Props: hostId. */
520
- readonly orgAddons: "orgAddons";
551
+ * Wherever a console page offers to publish something it holds (AGL-3080).
552
+ * Props: {@link ConsoleArtifactPublishZoneProps}.
553
+ *
554
+ * A layouts page, a components page and the org publish panel each held a
555
+ * dialog that posted to `marketplace/publish-layout` and read the
556
+ * marketplace's price floor. The console knows it has a layout; where a
557
+ * layout can be PUBLISHED, what a listing costs at the least, and what to
558
+ * call the thing in the copy all belong to whatever sells it.
559
+ *
560
+ * ── The page says what it has, never where it goes ───────────────────────
561
+ *
562
+ * The zone is handed a {@link ConsolePublishableArtifact} in the console's
563
+ * own vocabulary — a kind, the site or org it belongs to, the document id —
564
+ * and the widget decides the endpoint, the noun and the form. A kind the
565
+ * widget does not publish is one it draws nothing for, which is the same
566
+ * answer as no widget at all.
567
+ *
568
+ * ── An offer with nowhere to go must not be made ─────────────────────────
569
+ *
570
+ * The control that OPENS this — a menu item, a button — belongs to the page
571
+ * and is not a widget, because it is one entry of a list the page builds.
572
+ * So a page offering it asks `useSlotWidgets` whether this zone has a
573
+ * widget at all and leaves the entry out when it does not. Drawing it
574
+ * anyway would be a menu item that opens nothing on a workspace with no
575
+ * marketplace.
576
+ */
577
+ readonly hostArtifactPublish: "hostArtifactPublish";
521
578
  /** Bottom of the host dashboard. Props: hostId, org. (AGL-433) */
522
579
  readonly dashboardFooter: "dashboardFooter";
523
580
  /** Org settings page, below the tabbed cards. Props: orgId, org. */
@@ -865,6 +922,48 @@ export interface ConsoleHostFirstRunZoneProps {
865
922
  */
866
923
  startBlank: () => void;
867
924
  }
925
+ /**
926
+ * Something a console page holds that somebody may want to publish
927
+ * (AGL-3080).
928
+ *
929
+ * The console's own vocabulary and nothing else: a `kind` naming what the
930
+ * thing IS, the scope that holds it, and the document. Where it can be
931
+ * published to, what a listing of it is called and what it may cost are the
932
+ * publishing plugin's, which is the whole point of handing this over rather
933
+ * than building a request.
934
+ *
935
+ * `kind` is an open string for the reason a plugin's zone ids are: core
936
+ * listing every publishable noun would be core holding the catalog again. A
937
+ * widget that does not publish a kind draws nothing for it.
938
+ */
939
+ export interface ConsolePublishableArtifact {
940
+ /** What the thing is: `layout`, `component`, `theme`, `site`, … */
941
+ kind: string;
942
+ /** The site it belongs to, where the kind belongs to one. */
943
+ hostId?: string | null;
944
+ /** The organization, for a kind held by the org rather than a site. */
945
+ orgId?: string | null;
946
+ /**
947
+ * The document, in the holding surface's terms. Absent for a kind that IS
948
+ * the site or the org — a theme, a whole site template — where the scope
949
+ * above already names it.
950
+ */
951
+ artifactId?: string | null;
952
+ /** Seeds the listing's name; the person may change it. */
953
+ displayName?: string;
954
+ description?: string;
955
+ }
956
+ /** What the `hostArtifactPublish` zone hands each widget (AGL-3080). */
957
+ export interface ConsoleArtifactPublishZoneProps {
958
+ /**
959
+ * What the page asked to publish, or `null` while nothing is open. The
960
+ * page owns the open/closed state because the control that opens this is
961
+ * the page's own.
962
+ */
963
+ artifact: ConsolePublishableArtifact | null;
964
+ /** Closes it, however it was closed — cancelled, published, or refused. */
965
+ onClose: () => void;
966
+ }
868
967
  /** What the `hostTheme` zone hands each widget (AGL-2938). */
869
968
  export interface ConsoleHostThemeZoneProps {
870
969
  hostId: string;