@aglyn/plugins-marketplace 1.0.0-beta.221 → 1.0.0-beta.223

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/README.md CHANGED
@@ -37,6 +37,10 @@ It also registers a `rating` custom field type (`registerCustomFieldType`) with
37
37
 
38
38
  The server entry also registers the server side of the `rating` field type, so its validators run on write paths where no client loaded the plugin.
39
39
 
40
+ From its server declarations it also answers the platform's kill switch for another plugin (`registerPluginRevocationReader`, `plugin-revocations`): a listing's revocation, which the campaign send path reads before it mails a design installed from that listing.
41
+
42
+ A listing type whose copies live in another plugin's storage is that plugin's to read and write: `install-dataset-schema`, `publish-dataset-schema` and `update-artifact` keep the listing, its gates, the purchase, the provenance stamp and the tally, and ask the type's owner for the rest through `@aglyn/aglyn/plugin-manager/plugin-artifact-types` (the data plugin keeps `datasetSchema`). With no owner in the deployment the doors refuse (`501`, or `503` when the declared owner did not start) before anything is written.
43
+
40
44
  **Exports from `.`**
41
45
 
42
46
  - `registerMarketplaceConsole`, `BUNDLE_ID` (`'marketplace'`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/plugins-marketplace",
3
- "version": "1.0.0-beta.221",
3
+ "version": "1.0.0-beta.223",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,19 +25,19 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.221",
29
- "@aglyn/aglyn-markdown-editor": "1.0.0-beta.221",
30
- "@aglyn/shared-data-enums": "1.0.0-beta.221",
31
- "@aglyn/shared-data-mdi": "1.0.0-beta.221",
32
- "@aglyn/shared-ui-jsx": "1.0.0-beta.221",
33
- "@aglyn/shared-ui-next": "1.0.0-beta.221",
34
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.221",
35
- "@aglyn/shared-util-email": "1.0.0-beta.221",
36
- "@aglyn/shared-util-http": "1.0.0-beta.221",
37
- "@aglyn/shared-util-timestamp": "1.0.0-beta.221",
38
- "@aglyn/tenant-data-admin": "1.0.0-beta.221",
39
- "@aglyn/tenant-feature-instance": "1.0.0-beta.221",
40
- "@aglyn/tenant-runtime": "1.0.0-beta.221",
28
+ "@aglyn/aglyn": "1.0.0-beta.223",
29
+ "@aglyn/aglyn-markdown-editor": "1.0.0-beta.223",
30
+ "@aglyn/shared-data-enums": "1.0.0-beta.223",
31
+ "@aglyn/shared-data-mdi": "1.0.0-beta.223",
32
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.223",
33
+ "@aglyn/shared-ui-next": "1.0.0-beta.223",
34
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.223",
35
+ "@aglyn/shared-util-email": "1.0.0-beta.223",
36
+ "@aglyn/shared-util-http": "1.0.0-beta.223",
37
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.223",
38
+ "@aglyn/tenant-data-admin": "1.0.0-beta.223",
39
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.223",
40
+ "@aglyn/tenant-runtime": "1.0.0-beta.223",
41
41
  "@swc/helpers": "0.5.23"
42
42
  },
43
43
  "peerDependencies": {
@@ -18,8 +18,8 @@
18
18
  * The marketplace plugin's server declarations: the light registrations core
19
19
  * reads at boot, before any surface loads.
20
20
  *
21
- * The `rating` field type's validator (AGL-434), and the marketplace's share
22
- * of a workspace erasure.
21
+ * The `rating` field type's validator (AGL-434), the marketplace's share of
22
+ * a workspace erasure, and its kill switch as another plugin reads it.
23
23
  *
24
24
  * The validator: A dataset field of this type
25
25
  * can be written by a path that never loads the marketplace — a visitor's
@@ -17,14 +17,15 @@
17
17
  // the registry and nothing else.
18
18
  import { registerCustomFieldType } from "@aglyn/aglyn/plugin-manager/custom-fields";
19
19
  import { listPluginOrgErasers, registerPluginOrgEraser } from "@aglyn/aglyn/plugin-manager/plugin-org-erasure";
20
+ import { registerPluginRevocationReader } from "@aglyn/aglyn/plugin-manager/plugin-revocations";
20
21
  import { BUNDLE_ID } from "./constants/bundle-common.js";
21
22
  import { RATING_FIELD } from "./model/rating-field.js";
22
23
  /**
23
24
  * The marketplace plugin's server declarations: the light registrations core
24
25
  * reads at boot, before any surface loads.
25
26
  *
26
- * The `rating` field type's validator (AGL-434), and the marketplace's share
27
- * of a workspace erasure.
27
+ * The `rating` field type's validator (AGL-434), the marketplace's share of
28
+ * a workspace erasure, and its kill switch as another plugin reads it.
28
29
  *
29
30
  * The validator: A dataset field of this type
30
31
  * can be written by a path that never loads the marketplace — a visitor's
@@ -48,6 +49,18 @@ import { RATING_FIELD } from "./model/rating-field.js";
48
49
  pluginId: BUNDLE_ID
49
50
  });
50
51
  }
52
+ /*
53
+ * The kill switch (`plugin-revocations`, AGL-3080): the send path asks it
54
+ * before mailing a design installed from a listing, in whichever process
55
+ * sends. The Admin SDK arrives with the first read.
56
+ */ registerPluginRevocationReader({
57
+ async revocation (listingId) {
58
+ const { marketplaceRevocationReader } = await import("./server/revocation-reader.js");
59
+ return marketplaceRevocationReader.revocation(listingId);
60
+ }
61
+ }, {
62
+ pluginId: BUNDLE_ID
63
+ });
51
64
  }
52
65
 
53
66
  //# sourceMappingURL=declarations.server.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/plugins/marketplace/src/lib/declarations.server.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// The registry from its own module, not the plugin-manager barrel: boot needs\n// the registry and nothing else.\nimport { registerCustomFieldType } from '@aglyn/aglyn/plugin-manager/custom-fields'\nimport {\n listPluginOrgErasers,\n registerPluginOrgEraser,\n} from '@aglyn/aglyn/plugin-manager/plugin-org-erasure'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { RATING_FIELD } from './model/rating-field'\n\n/**\n * The marketplace plugin's server declarations: the light registrations core\n * reads at boot, before any surface loads.\n *\n * The `rating` field type's validator (AGL-434), and the marketplace's share\n * of a workspace erasure.\n *\n * The validator: A dataset field of this type\n * can be written by a path that never loads the marketplace — a visitor's\n * form submission and an automation step, both in the tenant (AGL-2773) — so\n * the validator registers from here, which every app runs at boot, rather\n * than only from the console API surface. Pure data: no React, no Admin SDK.\n */\nexport function registerMarketplaceServerDeclarations(): void {\n registerCustomFieldType(RATING_FIELD)\n /*\n * The organization's public marketplace identity, erased with it (AGL-1970).\n * Declared REQUIRED in `plugins.config.json`: the profile carries a payout\n * identifier on a world-readable document, so an erasure refuses to run\n * without this eraser rather than report a workspace erased with it\n * standing. From here, which every app and the erasure script run at boot;\n * the Admin SDK arrives with the first erasure, not with the boot.\n */\n if (!listPluginOrgErasers().includes(BUNDLE_ID)) {\n registerPluginOrgEraser(\n async (request) => {\n const { createPublisherIdentityEraser } = await import('./server/publisher-erasure')\n return createPublisherIdentityEraser()(request)\n },\n { pluginId: BUNDLE_ID },\n )\n }\n}\n"],"names":["registerCustomFieldType","listPluginOrgErasers","registerPluginOrgEraser","BUNDLE_ID","RATING_FIELD","registerMarketplaceServerDeclarations","includes","request","createPublisherIdentityEraser","pluginId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,8EAA8E;AAC9E,iCAAiC;AACjC,SAASA,uBAAuB,QAAQ,4CAA2C;AACnF,SACEC,oBAAoB,EACpBC,uBAAuB,QAClB,iDAAgD;AACvD,SAASC,SAAS,QAAQ,+BAA2B;AACrD,SAASC,YAAY,QAAQ,0BAAsB;AAEnD;;;;;;;;;;;;CAYC,GACD,OAAO,SAASC;IACdL,wBAAwBI;IACxB;;;;;;;GAOC,GACD,IAAI,CAACH,uBAAuBK,QAAQ,CAACH,YAAY;QAC/CD,wBACE,OAAOK;YACL,MAAM,EAAEC,6BAA6B,EAAE,GAAG,MAAM,MAAM,CAAC;YACvD,OAAOA,gCAAgCD;QACzC,GACA;YAAEE,UAAUN;QAAU;IAE1B;AACF"}
1
+ {"version":3,"sources":["../../../../../../libs/plugins/marketplace/src/lib/declarations.server.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// The registry from its own module, not the plugin-manager barrel: boot needs\n// the registry and nothing else.\nimport { registerCustomFieldType } from '@aglyn/aglyn/plugin-manager/custom-fields'\nimport {\n listPluginOrgErasers,\n registerPluginOrgEraser,\n} from '@aglyn/aglyn/plugin-manager/plugin-org-erasure'\nimport { registerPluginRevocationReader } from '@aglyn/aglyn/plugin-manager/plugin-revocations'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { RATING_FIELD } from './model/rating-field'\n\n/**\n * The marketplace plugin's server declarations: the light registrations core\n * reads at boot, before any surface loads.\n *\n * The `rating` field type's validator (AGL-434), the marketplace's share of\n * a workspace erasure, and its kill switch as another plugin reads it.\n *\n * The validator: A dataset field of this type\n * can be written by a path that never loads the marketplace — a visitor's\n * form submission and an automation step, both in the tenant (AGL-2773) — so\n * the validator registers from here, which every app runs at boot, rather\n * than only from the console API surface. Pure data: no React, no Admin SDK.\n */\nexport function registerMarketplaceServerDeclarations(): void {\n registerCustomFieldType(RATING_FIELD)\n /*\n * The organization's public marketplace identity, erased with it (AGL-1970).\n * Declared REQUIRED in `plugins.config.json`: the profile carries a payout\n * identifier on a world-readable document, so an erasure refuses to run\n * without this eraser rather than report a workspace erased with it\n * standing. From here, which every app and the erasure script run at boot;\n * the Admin SDK arrives with the first erasure, not with the boot.\n */\n if (!listPluginOrgErasers().includes(BUNDLE_ID)) {\n registerPluginOrgEraser(\n async (request) => {\n const { createPublisherIdentityEraser } = await import('./server/publisher-erasure')\n return createPublisherIdentityEraser()(request)\n },\n { pluginId: BUNDLE_ID },\n )\n }\n /*\n * The kill switch (`plugin-revocations`, AGL-3080): the send path asks it\n * before mailing a design installed from a listing, in whichever process\n * sends. The Admin SDK arrives with the first read.\n */\n registerPluginRevocationReader(\n {\n async revocation(listingId) {\n const { marketplaceRevocationReader } = await import('./server/revocation-reader')\n return marketplaceRevocationReader.revocation(listingId)\n },\n },\n { pluginId: BUNDLE_ID },\n )\n}\n"],"names":["registerCustomFieldType","listPluginOrgErasers","registerPluginOrgEraser","registerPluginRevocationReader","BUNDLE_ID","RATING_FIELD","registerMarketplaceServerDeclarations","includes","request","createPublisherIdentityEraser","pluginId","revocation","listingId","marketplaceRevocationReader"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,8EAA8E;AAC9E,iCAAiC;AACjC,SAASA,uBAAuB,QAAQ,4CAA2C;AACnF,SACEC,oBAAoB,EACpBC,uBAAuB,QAClB,iDAAgD;AACvD,SAASC,8BAA8B,QAAQ,iDAAgD;AAC/F,SAASC,SAAS,QAAQ,+BAA2B;AACrD,SAASC,YAAY,QAAQ,0BAAsB;AAEnD;;;;;;;;;;;;CAYC,GACD,OAAO,SAASC;IACdN,wBAAwBK;IACxB;;;;;;;GAOC,GACD,IAAI,CAACJ,uBAAuBM,QAAQ,CAACH,YAAY;QAC/CF,wBACE,OAAOM;YACL,MAAM,EAAEC,6BAA6B,EAAE,GAAG,MAAM,MAAM,CAAC;YACvD,OAAOA,gCAAgCD;QACzC,GACA;YAAEE,UAAUN;QAAU;IAE1B;IACA;;;;GAIC,GACDD,+BACE;QACE,MAAMQ,YAAWC,SAAS;YACxB,MAAM,EAAEC,2BAA2B,EAAE,GAAG,MAAM,MAAM,CAAC;YACrD,OAAOA,4BAA4BF,UAAU,CAACC;QAChD;IACF,GACA;QAAEF,UAAUN;IAAU;AAE1B"}
@@ -116,39 +116,3 @@ export declare function applyArtifactUpdate(plan: ArtifactUpdatePlan, current: u
116
116
  export declare function describeChange(change: ArtifactChange): string;
117
117
  /** A one-line value preview for a diff row. */
118
118
  export declare function summarizeValue(value: unknown): string;
119
- /**
120
- * Dataset schemas, classified by what an update would do to EXISTING RECORDS
121
- * (AGL-1018).
122
- *
123
- * The sharp edge of the whole project: adding a field is inert, but removing or
124
- * retyping one reinterprets data that is already there. So the schema plan is
125
- * not "safe vs conflict" but "additive vs destructive", and destructive changes
126
- * are never taken without a count of the records at stake.
127
- */
128
- export interface SchemaChangeSummary {
129
- /** Field ids the new version adds. Applying these cannot harm a record. */
130
- added: string[];
131
- /** Field ids it removes — the values in existing records become orphaned. */
132
- removed: string[];
133
- /** Field ids whose TYPE changed — existing values may not survive a re-read. */
134
- retyped: string[];
135
- /** Anything else (labels, options, ordering). */
136
- edited: string[];
137
- /** True when nothing removes or retypes a field. */
138
- additiveOnly: boolean;
139
- }
140
- interface SchemaShape {
141
- order?: unknown;
142
- fields?: Record<string, {
143
- type?: unknown;
144
- } | undefined>;
145
- }
146
- /**
147
- * Compares two dataset schemas field by field.
148
- *
149
- * Deliberately ignores the base: for a schema the question is not "who edited
150
- * this" but "what happens to the rows", and that is answered by the current
151
- * schema against the incoming one regardless of who moved which field.
152
- */
153
- export declare function summarizeSchemaChange(current: SchemaShape | null | undefined, incoming: SchemaShape | null | undefined): SchemaChangeSummary;
154
- export {};
@@ -187,42 +187,5 @@ function setPath(target, path, value) {
187
187
  const keys = Object.keys(value);
188
188
  return `{ ${keys.slice(0, 3).join(', ')}${keys.length > 3 ? ', …' : ''} }`;
189
189
  }
190
- /**
191
- * Compares two dataset schemas field by field.
192
- *
193
- * Deliberately ignores the base: for a schema the question is not "who edited
194
- * this" but "what happens to the rows", and that is answered by the current
195
- * schema against the incoming one regardless of who moved which field.
196
- */ export function summarizeSchemaChange(current, incoming) {
197
- var _ref, _ref1;
198
- const currentFields = (_ref = current == null ? void 0 : current.fields) != null ? _ref : {};
199
- const incomingFields = (_ref1 = incoming == null ? void 0 : incoming.fields) != null ? _ref1 : {};
200
- const added = [];
201
- const removed = [];
202
- const retyped = [];
203
- const edited = [];
204
- for (const id of Object.keys(incomingFields)){
205
- var _currentFields_id, _incomingFields_id;
206
- if (!(id in currentFields)) {
207
- added.push(id);
208
- continue;
209
- }
210
- if (String((_currentFields_id = currentFields[id]) == null ? void 0 : _currentFields_id.type) !== String((_incomingFields_id = incomingFields[id]) == null ? void 0 : _incomingFields_id.type)) {
211
- retyped.push(id);
212
- } else if (!same(currentFields[id], incomingFields[id])) {
213
- edited.push(id);
214
- }
215
- }
216
- for (const id of Object.keys(currentFields)){
217
- if (!(id in incomingFields)) removed.push(id);
218
- }
219
- return {
220
- added,
221
- removed,
222
- retyped,
223
- edited,
224
- additiveOnly: !removed.length && !retyped.length
225
- };
226
- }
227
190
 
228
191
  //# sourceMappingURL=artifact-merge.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/model/artifact-merge.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 { stableStringify } from '@aglyn/aglyn/app-utils/artifact-provenance'\n\n/**\n * Three-way diff for COPIED marketplace artifacts (AGL-1018).\n *\n * Plugins are pinned: an update moves a pointer at immutable bytes and nothing\n * can be lost. Everything else — components, layouts, templates, dataset\n * schemas, email templates — is copied into the workspace and then edited, so\n * an update is the vendor-branch problem. Re-installing overwrote the copy, and\n * a customer's edits went with it, silently.\n *\n * With AGL-1015's base snapshot every field is classifiable against three\n * versions, and the classification is the whole design:\n *\n * * changed upstream, untouched here → **safe**, take it\n * * changed here, untouched upstream → **kept**, theirs is already right\n * * changed on both sides → **conflict**, ask, and default to keeping theirs\n *\n * Pure and content-shaped on purpose: it knows nothing about Firestore or\n * artifact types, so the same function classifies a component tree, a layout\n * and a dataset schema, and can be tested without a database.\n */\n\n/** How a single field changed across base → current → incoming. */\nexport type ChangeKind =\n /** Upstream changed it, the user did not. Applying is lossless. */\n | 'safe'\n /** The user changed it, upstream did not. Applying would lose their edit. */\n | 'kept'\n /** Both changed it, to different values. Needs a decision. */\n | 'conflict'\n\nexport interface ArtifactChange {\n /** Dotted path into the content, e.g. `nodes.abc123.props.title`. */\n path: string\n kind: ChangeKind\n /** The publisher's original value at install. */\n base: unknown\n /** What the workspace has now. */\n current: unknown\n /** What the publisher ships in the new version. */\n incoming: unknown\n /**\n * True when the change adds or removes the field rather than editing it.\n * A removal is worth saying out loud separately: for a dataset schema it\n * means existing records, and for a node tree it means content disappearing\n * from a page rather than merely looking different.\n */\n added?: boolean\n removed?: boolean\n}\n\nexport interface ArtifactUpdatePlan {\n safe: ArtifactChange[]\n kept: ArtifactChange[]\n conflicts: ArtifactChange[]\n /** Fields identical in all three versions — reported as a count only. */\n unchanged: number\n /** Nothing to take: the incoming content matches what is installed. */\n identical: boolean\n}\n\n/**\n * Paths whose CHILDREN are compared as a unit rather than walked.\n *\n * A besigner node is `{id, type, props, childIds}`. Walking into it produces\n * true field-level granularity, which is what the issue asks for; but the\n * `childIds` array is where structure lives, and comparing arrays element-wise\n * makes a single insert read as a change to every position after it. Arrays are\n * therefore atomic everywhere (see {@link isAtomic}), and node maps are walked\n * by KEY — which is the structural, id-keyed comparison a node tree needs, and\n * the reason a re-ordered tree does not read as a conflict in every node.\n */\nfunction isAtomic(value: unknown): boolean {\n return (\n value === null ||\n typeof value !== 'object' ||\n Array.isArray(value) ||\n // Dates, Firestore timestamps and anything else non-plain: comparing their\n // enumerable members would compare internals, not content.\n (Object.getPrototypeOf(value) !== Object.prototype &&\n Object.getPrototypeOf(value) !== null)\n )\n}\n\nfunction same(a: unknown, b: unknown): boolean {\n return stableStringify(a) === stableStringify(b)\n}\n\nconst ABSENT = Symbol('absent')\n\nfunction get(container: unknown, key: string): unknown | typeof ABSENT {\n if (container === ABSENT || container == null || typeof container !== 'object') {\n return ABSENT\n }\n const record = container as Record<string, unknown>\n return key in record ? record[key] : ABSENT\n}\n\nfunction keysOf(...containers: unknown[]): string[] {\n const keys: string[] = []\n const seen = new Set<string>()\n for (const container of containers) {\n if (container === ABSENT || container == null || typeof container !== 'object') {\n continue\n }\n for (const key of Object.keys(container as Record<string, unknown>)) {\n if (!seen.has(key)) {\n seen.add(key)\n keys.push(key)\n }\n }\n }\n return keys\n}\n\n/**\n * Classifies every field of an artifact against its base, the workspace's\n * current copy, and the version on offer.\n *\n * `base` is required and not defaulted: without the snapshot there is no way to\n * tell the publisher's change from the user's, and guessing at one is how you\n * produce a confident, wrong merge. Callers with no base offer \"install as a\n * new copy\" and nothing else.\n */\nexport function planArtifactUpdate(\n base: unknown,\n current: unknown,\n incoming: unknown,\n): ArtifactUpdatePlan {\n const plan: ArtifactUpdatePlan = {\n safe: [],\n kept: [],\n conflicts: [],\n unchanged: 0,\n identical: false,\n }\n\n const walk = (\n baseValue: unknown,\n currentValue: unknown,\n incomingValue: unknown,\n path: string,\n ): void => {\n const userChanged = !same(baseValue, currentValue)\n const publisherChanged = !same(baseValue, incomingValue)\n\n if (!userChanged && !publisherChanged) {\n plan.unchanged += 1\n return\n }\n\n // Recurse while all three sides still have a walkable object here, so an\n // edit to one node's text and an upstream edit to a different node are two\n // independent changes rather than one whole-tree conflict.\n //\n // A side that is ABSENT stops the walk: the field was added or removed\n // wholesale, and reporting that as one change (\"this node is gone\") is both\n // truer and more readable than reporting every leaf inside it.\n const walkable =\n baseValue !== ABSENT &&\n currentValue !== ABSENT &&\n incomingValue !== ABSENT &&\n !isAtomic(baseValue) &&\n !isAtomic(currentValue) &&\n !isAtomic(incomingValue)\n if (walkable) {\n for (const key of keysOf(baseValue, currentValue, incomingValue)) {\n walk(\n get(baseValue, key),\n get(currentValue, key),\n get(incomingValue, key),\n path ? `${path}.${key}` : key,\n )\n }\n return\n }\n\n const change: ArtifactChange = {\n path,\n kind: userChanged\n ? publisherChanged\n ? 'conflict'\n : 'kept'\n : 'safe',\n base: baseValue === ABSENT ? undefined : baseValue,\n current: currentValue === ABSENT ? undefined : currentValue,\n incoming: incomingValue === ABSENT ? undefined : incomingValue,\n ...(baseValue === ABSENT ? { added: true } : {}),\n ...(incomingValue === ABSENT && baseValue !== ABSENT\n ? { removed: true }\n : {}),\n }\n // A conflict where both sides landed on the SAME value is not a conflict —\n // two people fixing the same typo agree.\n if (change.kind === 'conflict' && same(currentValue, incomingValue)) {\n plan.unchanged += 1\n return\n }\n if (change.kind === 'safe') plan.safe.push(change)\n else if (change.kind === 'kept') plan.kept.push(change)\n else plan.conflicts.push(change)\n }\n\n walk(base, current, incoming, '')\n plan.identical = same(current, incoming)\n return plan\n}\n\nfunction setPath(target: unknown, path: string[], value: unknown | typeof ABSENT): unknown {\n if (!path.length) return value === ABSENT ? undefined : value\n const [head, ...rest] = path\n const container: Record<string, unknown> =\n target != null && typeof target === 'object' && !Array.isArray(target)\n ? { ...(target as Record<string, unknown>) }\n : {}\n if (!rest.length) {\n if (value === ABSENT) delete container[head]\n else container[head] = value\n return container\n }\n container[head] = setPath(container[head], rest, value)\n return container\n}\n\nexport interface ApplyUpdateOptions {\n /**\n * Conflicting paths the user chose to take from the publisher. Everything\n * absent from this list keeps the workspace's value — the default is always\n * the safe one, so a user who applies without reading loses nothing.\n */\n takePaths?: readonly string[]\n}\n\nexport interface AppliedUpdate {\n content: unknown\n /** Paths actually written, for a summary that reports what happened. */\n applied: string[]\n /** Conflicting paths left at the workspace's value. */\n skipped: string[]\n}\n\n/**\n * Produces the merged content: the workspace's copy with every safe change\n * taken and only the explicitly chosen conflicts overwritten.\n *\n * Built by patching `current` rather than by patching `incoming`, because the\n * copy is what the site is running: anything this function fails to consider —\n * a field the publisher never shipped, a node the user added — survives by\n * default rather than vanishing.\n */\nexport function applyArtifactUpdate(\n plan: ArtifactUpdatePlan,\n current: unknown,\n options?: ApplyUpdateOptions,\n): AppliedUpdate {\n const take = new Set(options?.takePaths ?? [])\n const applied: string[] = []\n const skipped: string[] = []\n let content = current\n const write = (change: ArtifactChange) => {\n const segments = change.path ? change.path.split('.') : []\n content = setPath(\n content,\n segments,\n change.removed ? ABSENT : change.incoming,\n )\n applied.push(change.path)\n }\n for (const change of plan.safe) write(change)\n for (const change of plan.conflicts) {\n if (take.has(change.path)) write(change)\n else skipped.push(change.path)\n }\n return { content, applied, skipped }\n}\n\n/**\n * A short human label for a change, so the dialog, the summary snackbar and the\n * server's response all describe the same edit the same way.\n *\n * Values are summarised, never dumped: the interesting thing about a changed\n * node is which node and what kind of change, and a page of serialised JSON in\n * a list row is unreadable by the time it matters.\n */\nexport function describeChange(change: ArtifactChange): string {\n const label = change.path || 'the whole artifact'\n if (change.added) return `${label} — added by the publisher`\n if (change.removed) return `${label} — removed by the publisher`\n return label\n}\n\n/** A one-line value preview for a diff row. */\nexport function summarizeValue(value: unknown): string {\n if (value === undefined) return '—'\n if (value === null) return 'null'\n if (typeof value === 'string') {\n return value.length > 60 ? `${value.slice(0, 57)}…` : value || '(empty)'\n }\n if (typeof value !== 'object') return String(value)\n if (Array.isArray(value)) {\n return `${value.length} item${value.length === 1 ? '' : 's'}`\n }\n const keys = Object.keys(value as Record<string, unknown>)\n return `{ ${keys.slice(0, 3).join(', ')}${keys.length > 3 ? ', …' : ''} }`\n}\n\n/**\n * Dataset schemas, classified by what an update would do to EXISTING RECORDS\n * (AGL-1018).\n *\n * The sharp edge of the whole project: adding a field is inert, but removing or\n * retyping one reinterprets data that is already there. So the schema plan is\n * not \"safe vs conflict\" but \"additive vs destructive\", and destructive changes\n * are never taken without a count of the records at stake.\n */\nexport interface SchemaChangeSummary {\n /** Field ids the new version adds. Applying these cannot harm a record. */\n added: string[]\n /** Field ids it removes — the values in existing records become orphaned. */\n removed: string[]\n /** Field ids whose TYPE changed — existing values may not survive a re-read. */\n retyped: string[]\n /** Anything else (labels, options, ordering). */\n edited: string[]\n /** True when nothing removes or retypes a field. */\n additiveOnly: boolean\n}\n\ninterface SchemaShape {\n order?: unknown\n fields?: Record<string, { type?: unknown } | undefined>\n}\n\n/**\n * Compares two dataset schemas field by field.\n *\n * Deliberately ignores the base: for a schema the question is not \"who edited\n * this\" but \"what happens to the rows\", and that is answered by the current\n * schema against the incoming one regardless of who moved which field.\n */\nexport function summarizeSchemaChange(\n current: SchemaShape | null | undefined,\n incoming: SchemaShape | null | undefined,\n): SchemaChangeSummary {\n const currentFields = (current?.fields ?? {}) as Record<string, { type?: unknown }>\n const incomingFields = (incoming?.fields ?? {}) as Record<string, { type?: unknown }>\n const added: string[] = []\n const removed: string[] = []\n const retyped: string[] = []\n const edited: string[] = []\n for (const id of Object.keys(incomingFields)) {\n if (!(id in currentFields)) {\n added.push(id)\n continue\n }\n if (String(currentFields[id]?.type) !== String(incomingFields[id]?.type)) {\n retyped.push(id)\n } else if (!same(currentFields[id], incomingFields[id])) {\n edited.push(id)\n }\n }\n for (const id of Object.keys(currentFields)) {\n if (!(id in incomingFields)) removed.push(id)\n }\n return {\n added,\n removed,\n retyped,\n edited,\n additiveOnly: !removed.length && !retyped.length,\n }\n}\n"],"names":["stableStringify","isAtomic","value","Array","isArray","Object","getPrototypeOf","prototype","same","a","b","ABSENT","Symbol","get","container","key","record","keysOf","containers","keys","seen","Set","has","add","push","planArtifactUpdate","base","current","incoming","plan","safe","kept","conflicts","unchanged","identical","walk","baseValue","currentValue","incomingValue","path","userChanged","publisherChanged","walkable","change","kind","undefined","added","removed","setPath","target","length","head","rest","applyArtifactUpdate","options","take","takePaths","applied","skipped","content","write","segments","split","describeChange","label","summarizeValue","slice","String","join","summarizeSchemaChange","currentFields","fields","incomingFields","retyped","edited","id","type","additiveOnly"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/model/artifact-merge.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 { stableStringify } from '@aglyn/aglyn/app-utils/artifact-provenance'\n\n/**\n * Three-way diff for COPIED marketplace artifacts (AGL-1018).\n *\n * Plugins are pinned: an update moves a pointer at immutable bytes and nothing\n * can be lost. Everything else — components, layouts, templates, dataset\n * schemas, email templates — is copied into the workspace and then edited, so\n * an update is the vendor-branch problem. Re-installing overwrote the copy, and\n * a customer's edits went with it, silently.\n *\n * With AGL-1015's base snapshot every field is classifiable against three\n * versions, and the classification is the whole design:\n *\n * * changed upstream, untouched here → **safe**, take it\n * * changed here, untouched upstream → **kept**, theirs is already right\n * * changed on both sides → **conflict**, ask, and default to keeping theirs\n *\n * Pure and content-shaped on purpose: it knows nothing about Firestore or\n * artifact types, so the same function classifies a component tree, a layout\n * and a dataset schema, and can be tested without a database.\n */\n\n/** How a single field changed across base → current → incoming. */\nexport type ChangeKind =\n /** Upstream changed it, the user did not. Applying is lossless. */\n | 'safe'\n /** The user changed it, upstream did not. Applying would lose their edit. */\n | 'kept'\n /** Both changed it, to different values. Needs a decision. */\n | 'conflict'\n\nexport interface ArtifactChange {\n /** Dotted path into the content, e.g. `nodes.abc123.props.title`. */\n path: string\n kind: ChangeKind\n /** The publisher's original value at install. */\n base: unknown\n /** What the workspace has now. */\n current: unknown\n /** What the publisher ships in the new version. */\n incoming: unknown\n /**\n * True when the change adds or removes the field rather than editing it.\n * A removal is worth saying out loud separately: for a dataset schema it\n * means existing records, and for a node tree it means content disappearing\n * from a page rather than merely looking different.\n */\n added?: boolean\n removed?: boolean\n}\n\nexport interface ArtifactUpdatePlan {\n safe: ArtifactChange[]\n kept: ArtifactChange[]\n conflicts: ArtifactChange[]\n /** Fields identical in all three versions — reported as a count only. */\n unchanged: number\n /** Nothing to take: the incoming content matches what is installed. */\n identical: boolean\n}\n\n/**\n * Paths whose CHILDREN are compared as a unit rather than walked.\n *\n * A besigner node is `{id, type, props, childIds}`. Walking into it produces\n * true field-level granularity, which is what the issue asks for; but the\n * `childIds` array is where structure lives, and comparing arrays element-wise\n * makes a single insert read as a change to every position after it. Arrays are\n * therefore atomic everywhere (see {@link isAtomic}), and node maps are walked\n * by KEY — which is the structural, id-keyed comparison a node tree needs, and\n * the reason a re-ordered tree does not read as a conflict in every node.\n */\nfunction isAtomic(value: unknown): boolean {\n return (\n value === null ||\n typeof value !== 'object' ||\n Array.isArray(value) ||\n // Dates, Firestore timestamps and anything else non-plain: comparing their\n // enumerable members would compare internals, not content.\n (Object.getPrototypeOf(value) !== Object.prototype &&\n Object.getPrototypeOf(value) !== null)\n )\n}\n\nfunction same(a: unknown, b: unknown): boolean {\n return stableStringify(a) === stableStringify(b)\n}\n\nconst ABSENT = Symbol('absent')\n\nfunction get(container: unknown, key: string): unknown | typeof ABSENT {\n if (container === ABSENT || container == null || typeof container !== 'object') {\n return ABSENT\n }\n const record = container as Record<string, unknown>\n return key in record ? record[key] : ABSENT\n}\n\nfunction keysOf(...containers: unknown[]): string[] {\n const keys: string[] = []\n const seen = new Set<string>()\n for (const container of containers) {\n if (container === ABSENT || container == null || typeof container !== 'object') {\n continue\n }\n for (const key of Object.keys(container as Record<string, unknown>)) {\n if (!seen.has(key)) {\n seen.add(key)\n keys.push(key)\n }\n }\n }\n return keys\n}\n\n/**\n * Classifies every field of an artifact against its base, the workspace's\n * current copy, and the version on offer.\n *\n * `base` is required and not defaulted: without the snapshot there is no way to\n * tell the publisher's change from the user's, and guessing at one is how you\n * produce a confident, wrong merge. Callers with no base offer \"install as a\n * new copy\" and nothing else.\n */\nexport function planArtifactUpdate(\n base: unknown,\n current: unknown,\n incoming: unknown,\n): ArtifactUpdatePlan {\n const plan: ArtifactUpdatePlan = {\n safe: [],\n kept: [],\n conflicts: [],\n unchanged: 0,\n identical: false,\n }\n\n const walk = (\n baseValue: unknown,\n currentValue: unknown,\n incomingValue: unknown,\n path: string,\n ): void => {\n const userChanged = !same(baseValue, currentValue)\n const publisherChanged = !same(baseValue, incomingValue)\n\n if (!userChanged && !publisherChanged) {\n plan.unchanged += 1\n return\n }\n\n // Recurse while all three sides still have a walkable object here, so an\n // edit to one node's text and an upstream edit to a different node are two\n // independent changes rather than one whole-tree conflict.\n //\n // A side that is ABSENT stops the walk: the field was added or removed\n // wholesale, and reporting that as one change (\"this node is gone\") is both\n // truer and more readable than reporting every leaf inside it.\n const walkable =\n baseValue !== ABSENT &&\n currentValue !== ABSENT &&\n incomingValue !== ABSENT &&\n !isAtomic(baseValue) &&\n !isAtomic(currentValue) &&\n !isAtomic(incomingValue)\n if (walkable) {\n for (const key of keysOf(baseValue, currentValue, incomingValue)) {\n walk(\n get(baseValue, key),\n get(currentValue, key),\n get(incomingValue, key),\n path ? `${path}.${key}` : key,\n )\n }\n return\n }\n\n const change: ArtifactChange = {\n path,\n kind: userChanged\n ? publisherChanged\n ? 'conflict'\n : 'kept'\n : 'safe',\n base: baseValue === ABSENT ? undefined : baseValue,\n current: currentValue === ABSENT ? undefined : currentValue,\n incoming: incomingValue === ABSENT ? undefined : incomingValue,\n ...(baseValue === ABSENT ? { added: true } : {}),\n ...(incomingValue === ABSENT && baseValue !== ABSENT\n ? { removed: true }\n : {}),\n }\n // A conflict where both sides landed on the SAME value is not a conflict —\n // two people fixing the same typo agree.\n if (change.kind === 'conflict' && same(currentValue, incomingValue)) {\n plan.unchanged += 1\n return\n }\n if (change.kind === 'safe') plan.safe.push(change)\n else if (change.kind === 'kept') plan.kept.push(change)\n else plan.conflicts.push(change)\n }\n\n walk(base, current, incoming, '')\n plan.identical = same(current, incoming)\n return plan\n}\n\nfunction setPath(target: unknown, path: string[], value: unknown | typeof ABSENT): unknown {\n if (!path.length) return value === ABSENT ? undefined : value\n const [head, ...rest] = path\n const container: Record<string, unknown> =\n target != null && typeof target === 'object' && !Array.isArray(target)\n ? { ...(target as Record<string, unknown>) }\n : {}\n if (!rest.length) {\n if (value === ABSENT) delete container[head]\n else container[head] = value\n return container\n }\n container[head] = setPath(container[head], rest, value)\n return container\n}\n\nexport interface ApplyUpdateOptions {\n /**\n * Conflicting paths the user chose to take from the publisher. Everything\n * absent from this list keeps the workspace's value — the default is always\n * the safe one, so a user who applies without reading loses nothing.\n */\n takePaths?: readonly string[]\n}\n\nexport interface AppliedUpdate {\n content: unknown\n /** Paths actually written, for a summary that reports what happened. */\n applied: string[]\n /** Conflicting paths left at the workspace's value. */\n skipped: string[]\n}\n\n/**\n * Produces the merged content: the workspace's copy with every safe change\n * taken and only the explicitly chosen conflicts overwritten.\n *\n * Built by patching `current` rather than by patching `incoming`, because the\n * copy is what the site is running: anything this function fails to consider —\n * a field the publisher never shipped, a node the user added — survives by\n * default rather than vanishing.\n */\nexport function applyArtifactUpdate(\n plan: ArtifactUpdatePlan,\n current: unknown,\n options?: ApplyUpdateOptions,\n): AppliedUpdate {\n const take = new Set(options?.takePaths ?? [])\n const applied: string[] = []\n const skipped: string[] = []\n let content = current\n const write = (change: ArtifactChange) => {\n const segments = change.path ? change.path.split('.') : []\n content = setPath(\n content,\n segments,\n change.removed ? ABSENT : change.incoming,\n )\n applied.push(change.path)\n }\n for (const change of plan.safe) write(change)\n for (const change of plan.conflicts) {\n if (take.has(change.path)) write(change)\n else skipped.push(change.path)\n }\n return { content, applied, skipped }\n}\n\n/**\n * A short human label for a change, so the dialog, the summary snackbar and the\n * server's response all describe the same edit the same way.\n *\n * Values are summarised, never dumped: the interesting thing about a changed\n * node is which node and what kind of change, and a page of serialised JSON in\n * a list row is unreadable by the time it matters.\n */\nexport function describeChange(change: ArtifactChange): string {\n const label = change.path || 'the whole artifact'\n if (change.added) return `${label} — added by the publisher`\n if (change.removed) return `${label} — removed by the publisher`\n return label\n}\n\n/** A one-line value preview for a diff row. */\nexport function summarizeValue(value: unknown): string {\n if (value === undefined) return '—'\n if (value === null) return 'null'\n if (typeof value === 'string') {\n return value.length > 60 ? `${value.slice(0, 57)}…` : value || '(empty)'\n }\n if (typeof value !== 'object') return String(value)\n if (Array.isArray(value)) {\n return `${value.length} item${value.length === 1 ? '' : 's'}`\n }\n const keys = Object.keys(value as Record<string, unknown>)\n return `{ ${keys.slice(0, 3).join(', ')}${keys.length > 3 ? ', …' : ''} }`\n}\n"],"names":["stableStringify","isAtomic","value","Array","isArray","Object","getPrototypeOf","prototype","same","a","b","ABSENT","Symbol","get","container","key","record","keysOf","containers","keys","seen","Set","has","add","push","planArtifactUpdate","base","current","incoming","plan","safe","kept","conflicts","unchanged","identical","walk","baseValue","currentValue","incomingValue","path","userChanged","publisherChanged","walkable","change","kind","undefined","added","removed","setPath","target","length","head","rest","applyArtifactUpdate","options","take","takePaths","applied","skipped","content","write","segments","split","describeChange","label","summarizeValue","slice","String","join"],"mappings":"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"}
@@ -33,7 +33,7 @@ export * from './marketplace';
33
33
  * unusable from here.
34
34
  */
35
35
  export { ARTIFACT_BASE_COLLECTION, ARTIFACT_BASE_MAX_BYTES, resolveProvenance, stableStringify, type InstalledFrom, type ProvenanceState, type ResolvedProvenance, } from '@aglyn/aglyn/app-utils/artifact-provenance';
36
- export { applyArtifactUpdate, describeChange, planArtifactUpdate, summarizeSchemaChange, summarizeValue, type ArtifactChange, type ArtifactUpdatePlan, type ChangeKind, } from './artifact-merge';
36
+ export { applyArtifactUpdate, describeChange, planArtifactUpdate, summarizeValue, type ArtifactChange, type ArtifactUpdatePlan, type ChangeKind, } from './artifact-merge';
37
37
  export { compareArtifactVersions, resolveUpdateState, updateStateLabel, type UpdateComparableInstall, type UpdateComparableListing, type UpdateState, type UpdateStatus, } from './update-state';
38
38
  /**
39
39
  * The email-starter policy, re-exported for the same reason the provenance and
@@ -30,7 +30,7 @@
30
30
  * `@aglyn/aglyn` (createContext) and `@aglyn/aglyn/server` (node:fs) are both
31
31
  * unusable from here.
32
32
  */ export { ARTIFACT_BASE_COLLECTION, ARTIFACT_BASE_MAX_BYTES, resolveProvenance, stableStringify } from "@aglyn/aglyn/app-utils/artifact-provenance";
33
- export { applyArtifactUpdate, describeChange, planArtifactUpdate, summarizeSchemaChange, summarizeValue } from "./artifact-merge.js";
33
+ export { applyArtifactUpdate, describeChange, planArtifactUpdate, summarizeValue } from "./artifact-merge.js";
34
34
  export { compareArtifactVersions, resolveUpdateState, updateStateLabel } from "./update-state.js";
35
35
  /**
36
36
  * The email-starter policy, re-exported for the same reason the provenance and
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/model/index.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 * Marketplace marketplace model (AGL-413): listing/profile shapes + publish sanitization relocated from core app-utils.\n * Context-free — importable by client components, /server handlers, and\n * other plugins/apps via `@aglyn/plugins-marketplace/model`.\n */\nexport * from './marketplace'\n/**\n * Provenance and update state live in core (AGL-1015/1016) so the console can\n * read them too, but they are marketplace vocabulary — re-exported here so a\n * publishing or install route still imports the whole model from one place.\n * Named rather than star-exported: `MarketplaceArtifactType` is re-exported by\n * `./marketplace` already, and two barrels claiming it is an ambiguity.\n *\n * Deep paths rather than a core barrel, for the same reason `./marketplace`\n * uses one: this module is in the browser bundle AND in API routes, so\n * `@aglyn/aglyn` (createContext) and `@aglyn/aglyn/server` (node:fs) are both\n * unusable from here.\n */\nexport {\n ARTIFACT_BASE_COLLECTION,\n ARTIFACT_BASE_MAX_BYTES,\n resolveProvenance,\n stableStringify,\n type InstalledFrom,\n type ProvenanceState,\n type ResolvedProvenance,\n} from '@aglyn/aglyn/app-utils/artifact-provenance'\nexport {\n applyArtifactUpdate,\n describeChange,\n planArtifactUpdate,\n summarizeSchemaChange,\n summarizeValue,\n type ArtifactChange,\n type ArtifactUpdatePlan,\n type ChangeKind,\n} from './artifact-merge'\nexport {\n compareArtifactVersions,\n resolveUpdateState,\n updateStateLabel,\n type UpdateComparableInstall,\n type UpdateComparableListing,\n type UpdateState,\n type UpdateStatus,\n} from './update-state'\n/**\n * The email-starter policy, re-exported for the same reason the provenance and\n * visibility policies are: it lives in core because `scope:app` may not depend\n * on this lib, and publishing and install code should keep one import site.\n */\nexport {\n emailStarterAssuranceLabel,\n emailStarterRefusal,\n emailStarterSendBlock,\n inspectEmailStarter,\n resolveEmailStarterAssurance,\n type EmailStarterAssurance,\n type EmailStarterInspection,\n type EmailStarterSendBlock,\n type EmailStarterViolation,\n type EmailStarterViolationCode,\n} from '@aglyn/aglyn/app-utils/email-starter-policy'\n"],"names":["ARTIFACT_BASE_COLLECTION","ARTIFACT_BASE_MAX_BYTES","resolveProvenance","stableStringify","applyArtifactUpdate","describeChange","planArtifactUpdate","summarizeSchemaChange","summarizeValue","compareArtifactVersions","resolveUpdateState","updateStateLabel","emailStarterAssuranceLabel","emailStarterRefusal","emailStarterSendBlock","inspectEmailStarter","resolveEmailStarterAssurance"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;CAIC,GACD,cAAc,mBAAe;AAC7B;;;;;;;;;;;CAWC,GACD,SACEA,wBAAwB,EACxBC,uBAAuB,EACvBC,iBAAiB,EACjBC,eAAe,QAIV,6CAA4C;AACnD,SACEC,mBAAmB,EACnBC,cAAc,EACdC,kBAAkB,EAClBC,qBAAqB,EACrBC,cAAc,QAIT,sBAAkB;AACzB,SACEC,uBAAuB,EACvBC,kBAAkB,EAClBC,gBAAgB,QAKX,oBAAgB;AACvB;;;;CAIC,GACD,SACEC,0BAA0B,EAC1BC,mBAAmB,EACnBC,qBAAqB,EACrBC,mBAAmB,EACnBC,4BAA4B,QAMvB,8CAA6C"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/model/index.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 * Marketplace marketplace model (AGL-413): listing/profile shapes + publish sanitization relocated from core app-utils.\n * Context-free — importable by client components, /server handlers, and\n * other plugins/apps via `@aglyn/plugins-marketplace/model`.\n */\nexport * from './marketplace'\n/**\n * Provenance and update state live in core (AGL-1015/1016) so the console can\n * read them too, but they are marketplace vocabulary — re-exported here so a\n * publishing or install route still imports the whole model from one place.\n * Named rather than star-exported: `MarketplaceArtifactType` is re-exported by\n * `./marketplace` already, and two barrels claiming it is an ambiguity.\n *\n * Deep paths rather than a core barrel, for the same reason `./marketplace`\n * uses one: this module is in the browser bundle AND in API routes, so\n * `@aglyn/aglyn` (createContext) and `@aglyn/aglyn/server` (node:fs) are both\n * unusable from here.\n */\nexport {\n ARTIFACT_BASE_COLLECTION,\n ARTIFACT_BASE_MAX_BYTES,\n resolveProvenance,\n stableStringify,\n type InstalledFrom,\n type ProvenanceState,\n type ResolvedProvenance,\n} from '@aglyn/aglyn/app-utils/artifact-provenance'\nexport {\n applyArtifactUpdate,\n describeChange,\n planArtifactUpdate,\n summarizeValue,\n type ArtifactChange,\n type ArtifactUpdatePlan,\n type ChangeKind,\n} from './artifact-merge'\nexport {\n compareArtifactVersions,\n resolveUpdateState,\n updateStateLabel,\n type UpdateComparableInstall,\n type UpdateComparableListing,\n type UpdateState,\n type UpdateStatus,\n} from './update-state'\n/**\n * The email-starter policy, re-exported for the same reason the provenance and\n * visibility policies are: it lives in core because `scope:app` may not depend\n * on this lib, and publishing and install code should keep one import site.\n */\nexport {\n emailStarterAssuranceLabel,\n emailStarterRefusal,\n emailStarterSendBlock,\n inspectEmailStarter,\n resolveEmailStarterAssurance,\n type EmailStarterAssurance,\n type EmailStarterInspection,\n type EmailStarterSendBlock,\n type EmailStarterViolation,\n type EmailStarterViolationCode,\n} from '@aglyn/aglyn/app-utils/email-starter-policy'\n"],"names":["ARTIFACT_BASE_COLLECTION","ARTIFACT_BASE_MAX_BYTES","resolveProvenance","stableStringify","applyArtifactUpdate","describeChange","planArtifactUpdate","summarizeValue","compareArtifactVersions","resolveUpdateState","updateStateLabel","emailStarterAssuranceLabel","emailStarterRefusal","emailStarterSendBlock","inspectEmailStarter","resolveEmailStarterAssurance"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;CAIC,GACD,cAAc,mBAAe;AAC7B;;;;;;;;;;;CAWC,GACD,SACEA,wBAAwB,EACxBC,uBAAuB,EACvBC,iBAAiB,EACjBC,eAAe,QAIV,6CAA4C;AACnD,SACEC,mBAAmB,EACnBC,cAAc,EACdC,kBAAkB,EAClBC,cAAc,QAIT,sBAAkB;AACzB,SACEC,uBAAuB,EACvBC,kBAAkB,EAClBC,gBAAgB,QAKX,oBAAgB;AACvB;;;;CAIC,GACD,SACEC,0BAA0B,EAC1BC,mBAAmB,EACnBC,qBAAqB,EACrBC,mBAAmB,EACnBC,4BAA4B,QAMvB,8CAA6C"}
@@ -624,83 +624,7 @@ export declare function marketplacePriceRefusal(priceUsd: number): string | unde
624
624
  * A publisher reads it as the marketplace's rule, so the marketplace re-exports
625
625
  * it under the names its publish routes and its docs use.
626
626
  */
627
- export { PORTABLE_COMPONENT_ID_ALLOWLIST as MARKETPLACE_COMPONENT_ID_ALLOWLIST, PORTABLE_EMAIL_COMPONENT_ID_ALLOWLIST as MARKETPLACE_EMAIL_COMPONENT_ID_ALLOWLIST, PORTABLE_EMAIL_STARTER_COMPONENT_ID_ALLOWLIST as MARKETPLACE_EMAIL_STARTER_COMPONENT_ID_ALLOWLIST, sanitizePortableDefinition as sanitizeMarketplaceDefinition, portableDefinitionToNested as marketplaceDefinitionToNested, PORTABLE_SAFE_HREF as MARKETPLACE_SAFE_HREF, PORTABLE_SAFE_SRC as MARKETPLACE_SAFE_SRC, type PortableDefinitionNodes as MarketplaceDefinitionNodes, } from '@aglyn/aglyn/app-utils/node-definition-sanitizer';
628
- import { PORTABLE_DATASET_FIELD_TYPES as MARKETPLACE_DATASET_FIELD_TYPES, PORTABLE_DEFINITION_MAX_BYTES as MARKETPLACE_DEFINITION_MAX_BYTES } from '@aglyn/aglyn/app-utils/node-definition-sanitizer';
629
- export { MARKETPLACE_DATASET_FIELD_TYPES, MARKETPLACE_DEFINITION_MAX_BYTES };
630
- /** Field cap on a published schema — mirrors the console's create limit. */
631
- export declare const MARKETPLACE_DATASET_MAX_FIELDS = 100;
632
- /** A published dataset schema: the model, with no records. */
633
- export interface MarketplaceDatasetSchema {
634
- fields: Record<string, MarketplaceDatasetField>;
635
- order: string[];
636
- }
637
- export interface MarketplaceDatasetField {
638
- name: string;
639
- type: string;
640
- customType?: string;
641
- description?: string;
642
- required?: boolean;
643
- default?: unknown;
644
- validation?: Record<string, unknown>;
645
- /**
646
- * Kept so an install into an org that HAS the referenced dataset can relink
647
- * it. `datasetId` is the publisher's id and is meaningless in another org,
648
- * so `datasetLabel` carries the human name the installer matches on.
649
- */
650
- reference?: {
651
- datasetId?: string;
652
- datasetLabel?: string;
653
- displayFieldId?: string;
654
- multiple?: boolean;
655
- onDelete?: string;
656
- };
657
- }
658
- /**
659
- * Validates and strips a `DatasetModel` for publishing as a marketplace
660
- * dataset schema (AGL-657).
661
- *
662
- * Publishes STRUCTURE ONLY — records never travel. That is the whole safety
663
- * story for this artifact type: a dataset's rows are the org's customer data,
664
- * so the publish path reads the model and nothing else, and there is no code
665
- * path here that can reach the `records` subcollection.
666
- *
667
- * Two cross-org hazards the issue called out, handled by carrying enough
668
- * context for the INSTALLER to decide rather than by rejecting at publish:
669
- * - `reference` FKs point at a dataset id that only exists in the publisher's
670
- * org. The id is kept alongside the referenced dataset's label so the
671
- * installer can relink by name and degrade the field when it can't (see
672
- * `resolveInstalledDatasetSchema`).
673
- * - `customType` names a plugin-declared field type (AGL-434) whose plugin the
674
- * installing org may not have. Unknown custom types already degrade to their
675
- * base type at render, so the name rides along untouched.
676
- */
677
- export declare function sanitizeDatasetSchema(model: {
678
- fields?: Record<string, any>;
679
- order?: unknown;
680
- }): {
681
- ok: true;
682
- schema: MarketplaceDatasetSchema;
683
- } | {
684
- ok: false;
685
- error: string;
686
- };
687
- /**
688
- * Rewrites a published schema for the installing org (AGL-657).
689
- *
690
- * `reference` fields are the only part of a schema that can't cross an org
691
- * boundary as-is: they name a dataset id in the PUBLISHER's org. Relink by the
692
- * referenced dataset's label when the installing org has one by that name;
693
- * otherwise degrade the field to plain `text` and report it, because a
694
- * reference field pointing at a dataset that doesn't exist renders as a broken
695
- * picker — silently keeping the dead id would install something visibly
696
- * broken and blame the installer for it.
697
- *
698
- * `existingDatasets` maps lowercased display name → dataset id in the target.
699
- */
700
- export declare function resolveInstalledDatasetSchema(schema: MarketplaceDatasetSchema, existingDatasets: Readonly<Record<string, string>>): {
701
- schema: MarketplaceDatasetSchema;
702
- degradedFieldIds: string[];
703
- };
627
+ export { PORTABLE_COMPONENT_ID_ALLOWLIST as MARKETPLACE_COMPONENT_ID_ALLOWLIST, PORTABLE_EMAIL_COMPONENT_ID_ALLOWLIST as MARKETPLACE_EMAIL_COMPONENT_ID_ALLOWLIST, PORTABLE_EMAIL_STARTER_COMPONENT_ID_ALLOWLIST as MARKETPLACE_EMAIL_STARTER_COMPONENT_ID_ALLOWLIST, sanitizePortableDefinition as sanitizeMarketplaceDefinition, portableDefinitionToNested as marketplaceDefinitionToNested, PORTABLE_SAFE_HREF as MARKETPLACE_SAFE_HREF, PORTABLE_SAFE_SRC as MARKETPLACE_SAFE_SRC, PORTABLE_DEFINITION_MAX_BYTES as MARKETPLACE_DEFINITION_MAX_BYTES, type PortableDefinitionNodes as MarketplaceDefinitionNodes, } from '@aglyn/aglyn/app-utils/node-definition-sanitizer';
704
628
  /** One row of the listing detail's `What's included` checklist. */
705
629
  export interface ListingInclusion {
706
630
  label: string;
@@ -1,5 +1,3 @@
1
- import { _ as _extends } from "@swc/helpers/_/_extends";
2
- import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
3
1
  /**
4
2
  * @license
5
3
  * Copyright 2026 Aglyn LLC
@@ -645,173 +643,7 @@ const SUPPORT_EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
645
643
  * under neutral names, because a model-generated tree passes through it too.
646
644
  * A publisher reads it as the marketplace's rule, so the marketplace re-exports
647
645
  * it under the names its publish routes and its docs use.
648
- */ export { PORTABLE_COMPONENT_ID_ALLOWLIST as MARKETPLACE_COMPONENT_ID_ALLOWLIST, PORTABLE_EMAIL_COMPONENT_ID_ALLOWLIST as MARKETPLACE_EMAIL_COMPONENT_ID_ALLOWLIST, PORTABLE_EMAIL_STARTER_COMPONENT_ID_ALLOWLIST as MARKETPLACE_EMAIL_STARTER_COMPONENT_ID_ALLOWLIST, sanitizePortableDefinition as sanitizeMarketplaceDefinition, portableDefinitionToNested as marketplaceDefinitionToNested, PORTABLE_SAFE_HREF as MARKETPLACE_SAFE_HREF, PORTABLE_SAFE_SRC as MARKETPLACE_SAFE_SRC } from "@aglyn/aglyn/app-utils/node-definition-sanitizer";
649
- import { PORTABLE_DATASET_FIELD_TYPES as MARKETPLACE_DATASET_FIELD_TYPES, PORTABLE_DEFINITION_MAX_BYTES as MARKETPLACE_DEFINITION_MAX_BYTES } from "@aglyn/aglyn/app-utils/node-definition-sanitizer";
650
- export { MARKETPLACE_DATASET_FIELD_TYPES, MARKETPLACE_DEFINITION_MAX_BYTES };
651
- /** Field cap on a published schema — mirrors the console's create limit. */ export const MARKETPLACE_DATASET_MAX_FIELDS = 100;
652
- const KEPT_VALIDATION_KEYS = [
653
- 'required',
654
- 'regex',
655
- 'min',
656
- 'max',
657
- 'options'
658
- ];
659
- /**
660
- * Validates and strips a `DatasetModel` for publishing as a marketplace
661
- * dataset schema (AGL-657).
662
- *
663
- * Publishes STRUCTURE ONLY — records never travel. That is the whole safety
664
- * story for this artifact type: a dataset's rows are the org's customer data,
665
- * so the publish path reads the model and nothing else, and there is no code
666
- * path here that can reach the `records` subcollection.
667
- *
668
- * Two cross-org hazards the issue called out, handled by carrying enough
669
- * context for the INSTALLER to decide rather than by rejecting at publish:
670
- * - `reference` FKs point at a dataset id that only exists in the publisher's
671
- * org. The id is kept alongside the referenced dataset's label so the
672
- * installer can relink by name and degrade the field when it can't (see
673
- * `resolveInstalledDatasetSchema`).
674
- * - `customType` names a plugin-declared field type (AGL-434) whose plugin the
675
- * installing org may not have. Unknown custom types already degrade to their
676
- * base type at render, so the name rides along untouched.
677
- */ export function sanitizeDatasetSchema(model) {
678
- const fields = model == null ? void 0 : model.fields;
679
- if (!fields || typeof fields !== 'object') {
680
- return {
681
- ok: false,
682
- error: 'Dataset has no field model to publish'
683
- };
684
- }
685
- // Order is the display contract; fall back to key order for models written
686
- // before `order` was required so older datasets stay publishable.
687
- const order = Array.isArray(model.order) ? model.order.map(String) : Object.keys(fields);
688
- const kept = order.filter((id)=>fields[id]);
689
- if (!kept.length) {
690
- return {
691
- ok: false,
692
- error: 'Dataset has no fields to publish'
693
- };
694
- }
695
- if (kept.length > MARKETPLACE_DATASET_MAX_FIELDS) {
696
- return {
697
- ok: false,
698
- error: `Dataset schemas are limited to ${MARKETPLACE_DATASET_MAX_FIELDS} fields`
699
- };
700
- }
701
- const schema = {
702
- fields: {},
703
- order: kept
704
- };
705
- for (const id of kept){
706
- var _fields_id, _field_type, _field_name;
707
- const field = (_fields_id = fields[id]) != null ? _fields_id : {};
708
- const type = String((_field_type = field.type) != null ? _field_type : 'text');
709
- if (!MARKETPLACE_DATASET_FIELD_TYPES.includes(type)) {
710
- return {
711
- ok: false,
712
- error: `Field "${id}" has an unsupported type`
713
- };
714
- }
715
- const safe = {
716
- name: String((_field_name = field.name) != null ? _field_name : id).slice(0, 120),
717
- type
718
- };
719
- if (field.customType) safe.customType = String(field.customType).slice(0, 60);
720
- if (field.description) {
721
- safe.description = String(field.description).slice(0, 500);
722
- }
723
- if (field.required === true) safe.required = true;
724
- // Defaults are publisher-authored values, not data — keep only primitives
725
- // so a default can't smuggle a nested object into the installing org.
726
- if (field.default !== undefined && (typeof field.default === 'string' || typeof field.default === 'number' || typeof field.default === 'boolean')) {
727
- safe.default = field.default;
728
- }
729
- if (field.validation && typeof field.validation === 'object') {
730
- const validation = {};
731
- for (const key of KEPT_VALIDATION_KEYS){
732
- if (field.validation[key] !== undefined) {
733
- validation[key] = field.validation[key];
734
- }
735
- }
736
- if (Object.keys(validation).length) safe.validation = validation;
737
- }
738
- if (field.reference && typeof field.reference === 'object') {
739
- safe.reference = _extends({}, field.reference.datasetId && {
740
- datasetId: String(field.reference.datasetId)
741
- }, field.reference.datasetLabel && {
742
- datasetLabel: String(field.reference.datasetLabel).slice(0, 120)
743
- }, field.reference.displayFieldId && {
744
- displayFieldId: String(field.reference.displayFieldId)
745
- }, field.reference.multiple === true && {
746
- multiple: true
747
- }, field.reference.onDelete && {
748
- onDelete: String(field.reference.onDelete)
749
- });
750
- }
751
- schema.fields[id] = safe;
752
- }
753
- const serialized = JSON.stringify(schema);
754
- if (serialized.length > MARKETPLACE_DEFINITION_MAX_BYTES) {
755
- return {
756
- ok: false,
757
- error: 'Dataset schema is too large to publish'
758
- };
759
- }
760
- return {
761
- ok: true,
762
- schema
763
- };
764
- }
765
- /**
766
- * Rewrites a published schema for the installing org (AGL-657).
767
- *
768
- * `reference` fields are the only part of a schema that can't cross an org
769
- * boundary as-is: they name a dataset id in the PUBLISHER's org. Relink by the
770
- * referenced dataset's label when the installing org has one by that name;
771
- * otherwise degrade the field to plain `text` and report it, because a
772
- * reference field pointing at a dataset that doesn't exist renders as a broken
773
- * picker — silently keeping the dead id would install something visibly
774
- * broken and blame the installer for it.
775
- *
776
- * `existingDatasets` maps lowercased display name → dataset id in the target.
777
- */ export function resolveInstalledDatasetSchema(schema, existingDatasets) {
778
- const degradedFieldIds = [];
779
- const fields = {};
780
- for (const id of schema.order){
781
- var _ref;
782
- var _field_reference;
783
- const field = schema.fields[id];
784
- if (!field) continue;
785
- if (field.type !== 'reference' && !field.reference) {
786
- fields[id] = field;
787
- continue;
788
- }
789
- const label = String((_ref = (_field_reference = field.reference) == null ? void 0 : _field_reference.datasetLabel) != null ? _ref : '').toLowerCase();
790
- const relinked = label ? existingDatasets[label] : undefined;
791
- if (relinked) {
792
- fields[id] = _extends({}, field, {
793
- reference: _extends({}, field.reference, {
794
- datasetId: relinked
795
- })
796
- });
797
- continue;
798
- }
799
- const { reference: _dropped } = field, rest = _object_without_properties_loose(field, [
800
- "reference"
801
- ]);
802
- fields[id] = _extends({}, rest, {
803
- type: 'text'
804
- });
805
- degradedFieldIds.push(id);
806
- }
807
- return {
808
- schema: {
809
- fields,
810
- order: schema.order
811
- },
812
- degradedFieldIds
813
- };
814
- }
646
+ */ export { PORTABLE_COMPONENT_ID_ALLOWLIST as MARKETPLACE_COMPONENT_ID_ALLOWLIST, PORTABLE_EMAIL_COMPONENT_ID_ALLOWLIST as MARKETPLACE_EMAIL_COMPONENT_ID_ALLOWLIST, PORTABLE_EMAIL_STARTER_COMPONENT_ID_ALLOWLIST as MARKETPLACE_EMAIL_STARTER_COMPONENT_ID_ALLOWLIST, sanitizePortableDefinition as sanitizeMarketplaceDefinition, portableDefinitionToNested as marketplaceDefinitionToNested, PORTABLE_SAFE_HREF as MARKETPLACE_SAFE_HREF, PORTABLE_SAFE_SRC as MARKETPLACE_SAFE_SRC, PORTABLE_DEFINITION_MAX_BYTES as MARKETPLACE_DEFINITION_MAX_BYTES } from "@aglyn/aglyn/app-utils/node-definition-sanitizer";
815
647
  /** What each artifact type drops into the org when it installs. */ const ARTIFACT_INSTALL_RESULT = {
816
648
  plugin: 'A plugin, sandboxed on its own origin with a per-plugin CSP',
817
649
  component: 'An editable component you can place on any page',