@aglyn/aglyn 1.0.0-beta.152 → 1.0.0-beta.153

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 (42) hide show
  1. package/package.json +10 -10
  2. package/src/lib/app-utils/crm-csv.d.ts +10 -3
  3. package/src/lib/app-utils/crm-csv.js +37 -2
  4. package/src/lib/app-utils/crm-csv.js.map +1 -1
  5. package/src/lib/app-utils/crm-email-templates.js +15 -0
  6. package/src/lib/app-utils/crm-email-templates.js.map +1 -1
  7. package/src/lib/app-utils/crm.d.ts +69 -2
  8. package/src/lib/app-utils/crm.js +126 -1
  9. package/src/lib/app-utils/crm.js.map +1 -1
  10. package/src/lib/app-utils/docs-help.generated.d.ts +1 -1
  11. package/src/lib/app-utils/docs-help.generated.js +2 -0
  12. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  13. package/src/lib/app-utils/docs-index.generated.js +25 -11
  14. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  15. package/src/lib/app-utils/email-starter-policy.js +5 -3
  16. package/src/lib/app-utils/email-starter-policy.js.map +1 -1
  17. package/src/lib/app-utils/plan-entitlements.d.ts +34 -0
  18. package/src/lib/app-utils/plan-entitlements.js +40 -0
  19. package/src/lib/app-utils/plan-entitlements.js.map +1 -1
  20. package/src/lib/app-utils/server.d.ts +0 -1
  21. package/src/lib/app-utils/server.js +0 -1
  22. package/src/lib/app-utils/server.js.map +1 -1
  23. package/src/lib/plugin-manager/first-party-plugins.generated.js +1 -0
  24. package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
  25. package/src/lib/plugin-manager/plugin-artifact-inventory.d.ts +145 -0
  26. package/src/lib/plugin-manager/plugin-artifact-inventory.js +126 -0
  27. package/src/lib/plugin-manager/plugin-artifact-inventory.js.map +1 -0
  28. package/src/lib/plugin-manager/plugin-contact-capture.d.ts +32 -1
  29. package/src/lib/plugin-manager/plugin-contact-capture.js.map +1 -1
  30. package/src/lib/plugin-manager/plugin-lead-conversion.d.ts +67 -0
  31. package/src/lib/plugin-manager/plugin-lead-conversion.js +93 -0
  32. package/src/lib/plugin-manager/plugin-lead-conversion.js.map +1 -0
  33. package/src/lib/plugin-manager/plugin-org-capacity.d.ts +12 -0
  34. package/src/lib/plugin-manager/plugin-org-capacity.js.map +1 -1
  35. package/src/lib/plugin-manager/plugin-record-timeline.d.ts +8 -1
  36. package/src/lib/plugin-manager/plugin-record-timeline.js.map +1 -1
  37. package/src/lib/app-utils/marketplace-verification.d.ts +0 -95
  38. package/src/lib/app-utils/marketplace-verification.js +0 -81
  39. package/src/lib/app-utils/marketplace-verification.js.map +0 -1
  40. package/src/lib/app-utils/publisher-attestation.d.ts +0 -99
  41. package/src/lib/app-utils/publisher-attestation.js +0 -158
  42. package/src/lib/app-utils/publisher-attestation.js.map +0 -1
@@ -18,9 +18,11 @@
18
18
  * mailed.
19
19
  *
20
20
  * In core rather than beside the rest of the marketplace model for the reason
21
- * `marketplace-provenance` and `marketplace-verification` give: three
22
- * different projects have to ask these questions and a `scope:app` project may
23
- * not depend on an `aglyn:addons` lib. The marketplace plugin enforces the
21
+ * `marketplace-provenance` gives: three different projects have to ask these
22
+ * questions and a `scope:app` project may not depend on an `aglyn:addons` lib.
23
+ * `marketplace-verification` used to be cited here too and no longer can be —
24
+ * its console reader became the plugin's own surface, so the policy went with
25
+ * it (AGL-3080). A shared reason is only shared while both halves still hold. The marketplace plugin enforces the
24
26
  * policy when content is published and installed; the marketing plugin asks
25
27
  * the kill question on the send path; the console renders the assurance.
26
28
  * Three readings of "is this template safe to mail" is the failure mode this
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/email-starter-policy.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 * What a marketplace email starter may contain, and whether one may still be\n * mailed.\n *\n * In core rather than beside the rest of the marketplace model for the reason\n * `marketplace-provenance` and `marketplace-verification` give: three\n * different projects have to ask these questions and a `scope:app` project may\n * not depend on an `aglyn:addons` lib. The marketplace plugin enforces the\n * policy when content is published and installed; the marketing plugin asks\n * the kill question on the send path; the console renders the assurance.\n * Three readings of \"is this template safe to mail\" is the failure mode this\n * module exists to prevent.\n *\n * ## Why an email design needs a policy the page allowlist does not supply\n *\n * A published page component is rendered by us, inside a document we serve,\n * behind our CSP. A published EMAIL design is rendered by the recipient's mail\n * client, out of our reach entirely, and it leaves on a sending domain whose\n * reputation every tenant shares. The two facts that follow are the whole\n * reason this file is stricter than `sanitizeMarketplaceDefinition`:\n *\n * - **A remote asset in an email is a read receipt.** The recipient's client\n * fetches every `src` on open, so whatever host the string names learns that\n * this person opened this message, at this time, from this IP. Naming a\n * host the PUBLISHER controls hands a third party a recipient-level\n * engagement feed on the tenant's audience — processing with no recorded\n * basis and no `assertedBy` provenance, which is exactly what the consent\n * architecture exists to make impossible. A 1×1 pixel is the deliberate form;\n * an ordinary hero image is the same disclosure with a picture attached.\n * - **A link is sent under the tenant's name.** A recipient reads the From\n * header, not the publisher's listing page.\n *\n * So `src` is held to first-party or inline, and `href` to a transport a\n * recipient's client will not silently downgrade. Violations are REFUSED\n * rather than stripped: `sanitizePublishedNodeProps` drops an unsafe URL and\n * publishes the rest, which is right for a page — a missing image is a\n * cosmetic defect — and wrong here, because the publisher whose tracker was\n * quietly removed learns nothing and tries again.\n */\n\nimport { isMediaCdnPath, isMediaRef, parseMediaRef } from './media-ref'\nimport { isListingWideRevocation, isPluginRevoked } from './plugin-manifest'\nimport type { PluginRevocation } from './plugin-manifest'\nimport { decodeStoredNodes } from './stored-nodes'\n\n/** Props that carry a URL into a recipient's mail client. */\nconst URL_PROPS = ['src', 'href'] as const\n\n/**\n * Props whose value is markup rather than text.\n *\n * `emailHtml` is already off `MARKETPLACE_EMAIL_COMPONENT_ID_ALLOWLIST`, so a\n * publish cannot mint one. This catches the same content arriving as a PROP on\n * a block that is on the list — a hand-written document, or a block that grows\n * an `html` prop later — which the component-id allowlist alone would pass.\n */\nconst MARKUP_PROPS = ['html', 'dangerouslySetInnerHTML'] as const\n\n/** Merge-tag opener. Recipient data must never reach a URL (see below). */\nconst MERGE_TAG_OPEN = '{{'\n\n/** Inline image data, which is inert and discloses nothing on open. */\nconst INLINE_IMAGE = /^data:image\\//i\n\n/** Link transports a recipient's client will not silently downgrade. */\nconst MAIL_SAFE_HREF = /^(https:\\/\\/|mailto:|\\/|#)/i\n\nexport type EmailStarterViolationCode =\n /** A `src` naming a host that is not ours — the read-receipt vector. */\n | 'remote-asset'\n /** An `href` on a transport we will not send under a tenant's name. */\n | 'unsafe-link'\n /** Recipient data heading for a URL. */\n | 'merge-tag-in-url'\n /** Markup we would have to trust rather than render. */\n | 'raw-markup'\n /** The node map could not be read, so nothing about it is known. */\n | 'unreadable'\n\nexport interface EmailStarterViolation {\n code: EmailStarterViolationCode\n /** The offending node's id, or `'nodes'` when the map itself is the fault. */\n nodeId: string\n componentId: string\n /** The prop that carried it, for a message that can be acted on. */\n prop: string\n /** Truncated: a violation is reported to a publisher, and often logged. */\n value: string\n}\n\nexport interface EmailStarterInspection {\n ok: boolean\n violations: EmailStarterViolation[]\n /**\n * Every host this design would send a recipient to, sorted and deduplicated.\n *\n * The inspectable half of the link policy. A tenant about to mail their\n * customers under their own From header is entitled to see the list of\n * domains a stranger's template points them at, and no allowlist can decide\n * that for them — a link to a legitimate retailer and a link to the\n * publisher's affiliate funnel are the same shape.\n */\n linkHosts: string[]\n /**\n * Media scopes belonging to somebody other than the installing site.\n *\n * Not a violation: the asset is served by our own CDN, so the publisher\n * learns nothing about the recipient, and refusing would make almost every\n * real template uninstallable. It is REPORTED because the reference stays\n * live after a copy-on-install — the publisher can still replace the bytes\n * behind it — which is the one way an installed design can change without\n * an update being taken.\n */\n foreignMediaScopes: string[]\n /** The decoded map, so a caller need not decode twice. Null when unreadable. */\n nodes: Record<string, unknown> | null\n}\n\nfunction truncate(value: unknown): string {\n const text = typeof value === 'string' ? value : String(value ?? '')\n return text.length > 120 ? `${text.slice(0, 117)}...` : text\n}\n\n/**\n * The host a link points at, or null for a same-origin or non-URL target.\n *\n * `URL` rather than a regex because the question is \"which host will the mail\n * client resolve this to\", and that is the parser's answer, not ours. A value\n * that will not parse is not reported as a host — it is already refused by the\n * transport check, and inventing a host for it would put a fiction on a screen\n * a tenant is using to make a decision.\n */\nfunction linkHost(value: string): string | null {\n if (!/^https?:\\/\\//i.test(value)) return null\n try {\n return new URL(value).host.toLowerCase() || null\n } catch {\n return null\n }\n}\n\n/**\n * Whether a decoded value is a node MAP rather than something that merely\n * survived decoding.\n *\n * The case this exists for is the AGL-1394 shape: `nodes` read raw off a\n * screen version is a msgpack `Buffer`, and `Object.entries` of a Buffer\n * yields byte INDICES paired with NUMBERS. Every check below asks a node for a\n * `props` object, finds none, and the walk completes reporting a clean design.\n * A scanner that cannot read its input must say so, not pass it.\n *\n * `decodeStoredNodes` handles the three storage forms, so anything still\n * failing here was never a node map to begin with.\n */\nfunction isNodeMap(value: unknown): value is Record<string, unknown> {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return false\n const entries = Object.values(value as Record<string, unknown>)\n if (!entries.length) return false\n return entries.every(\n (entry) => Boolean(entry) && typeof entry === 'object' && !Array.isArray(entry),\n )\n}\n\n/**\n * Reads a marketplace email design against the mail-client policy above.\n *\n * Takes `nodes` in whatever form the document stored it — a plain Firestore\n * map, compressed msgpack bytes, or the `{type:'Buffer'}` envelope an export\n * bundle carries — and decodes before walking, because a naive walk over the\n * compressed form finds nothing and reports the design clean.\n *\n * @param raw - the version document's `nodes` field, undecoded.\n * @param options - carries `hostId`, the site the design is being installed\n * onto, used only to classify media scopes as local or foreign. Omitted at\n * publish time, where there is no installing site yet.\n */\nexport function inspectEmailStarter(\n raw: unknown,\n options?: { hostId?: string | null },\n): EmailStarterInspection {\n const decoded = decodeStoredNodes<Record<string, unknown>>(raw)\n if (!isNodeMap(decoded)) {\n return {\n ok: false,\n violations: [\n {\n code: 'unreadable',\n nodeId: 'nodes',\n componentId: '',\n prop: 'nodes',\n value: decoded === null ? 'undecodable' : 'not a node map',\n },\n ],\n linkHosts: [],\n foreignMediaScopes: [],\n nodes: null,\n }\n }\n\n const violations: EmailStarterViolation[] = []\n const hosts = new Set<string>()\n const foreignScopes = new Set<string>()\n\n for (const [nodeId, value] of Object.entries(decoded)) {\n const node = value as { componentId?: unknown; props?: unknown }\n const componentId = String(node.componentId ?? '')\n const props =\n node.props && typeof node.props === 'object' && !Array.isArray(node.props)\n ? (node.props as Record<string, unknown>)\n : {}\n\n for (const prop of MARKUP_PROPS) {\n if (props[prop] === undefined) continue\n violations.push({\n code: 'raw-markup',\n nodeId,\n componentId,\n prop,\n value: truncate(props[prop]),\n })\n }\n\n for (const prop of URL_PROPS) {\n const value = props[prop]\n if (typeof value !== 'string' || !value) continue\n\n // Checked before the transport rules, and reported as its own code,\n // because the two failures need different sentences: an `http:` link is\n // a mistake to correct, and a merge tag in a URL is recipient data on\n // its way into a query string — a standing prohibition, and the reason\n // it is prohibited is that a URL is logged by every hop that serves it.\n if (value.includes(MERGE_TAG_OPEN)) {\n violations.push({\n code: 'merge-tag-in-url',\n nodeId,\n componentId,\n prop,\n value: truncate(value),\n })\n continue\n }\n\n if (prop === 'src') {\n if (isMediaRef(value)) {\n const ref = parseMediaRef(value)\n if (!ref) {\n violations.push({\n code: 'remote-asset',\n nodeId,\n componentId,\n prop,\n value: truncate(value),\n })\n continue\n }\n if (options?.hostId && !ref.scope.split(':').includes(options.hostId)) {\n foreignScopes.add(ref.scope)\n }\n continue\n }\n if (isMediaCdnPath(value) || INLINE_IMAGE.test(value)) continue\n violations.push({\n code: 'remote-asset',\n nodeId,\n componentId,\n prop,\n value: truncate(value),\n })\n continue\n }\n\n const host = linkHost(value)\n if (host) hosts.add(host)\n if (!MAIL_SAFE_HREF.test(value)) {\n violations.push({\n code: 'unsafe-link',\n nodeId,\n componentId,\n prop,\n value: truncate(value),\n })\n }\n }\n }\n\n return {\n ok: violations.length === 0,\n violations,\n linkHosts: [...hosts].sort(),\n foreignMediaScopes: [...foreignScopes].sort(),\n nodes: decoded,\n }\n}\n\n/** One sentence per violation code, in the second person. */\nconst VIOLATION_COPY: Record<EmailStarterViolationCode, string> = {\n 'remote-asset':\n 'images must come from your own media library or be inline — an image ' +\n 'loaded from another site tells that site who opened the email',\n 'unsafe-link':\n 'links must be https, mailto, or a path on the sending site',\n 'merge-tag-in-url':\n 'a merge tag may not appear in a link or image address — that puts ' +\n 'recipient data into a URL',\n 'raw-markup':\n 'raw HTML cannot be published as an email design; build it from email ' +\n 'blocks',\n unreadable: 'the design could not be read',\n}\n\n/**\n * A refusal message for a failed inspection, or `undefined` when it passed.\n *\n * One message naming ONE violation and its count, rather than a list: the\n * caller is an API route whose body is rendered in a dialog, and a publisher\n * fixing eleven images fixes them the same way they fix the first.\n */\nexport function emailStarterRefusal(\n inspection: EmailStarterInspection,\n): string | undefined {\n const [first] = inspection.violations\n if (!first) return undefined\n const more = inspection.violations.length - 1\n const at = first.componentId ? ` (on a ${first.componentId} block)` : ''\n return (\n `This email design cannot be published or installed: ` +\n `${VIOLATION_COPY[first.code]}${at}. Found \"${first.value}\"` +\n (more > 0 ? ` and ${more} more like it.` : '.')\n )\n}\n\n/**\n * How much is actually known about the version of an email starter that was\n * installed.\n *\n * `unreviewed` is a real state, not a missing one, and it is the DEFAULT.\n */\nexport type EmailStarterAssurance = 'approved' | 'unreviewed' | 'rejected'\n\n/**\n * The assurance of ONE version, read from that version's own document.\n *\n * Deliberately takes the version and nothing else. A listing carries\n * `reviewStatus: 'verified'` and a `latestVersionReviewState` mirror, and both\n * describe the listing or its newest build — neither is a statement about the\n * version somebody is installing right now. Passing the listing in here would\n * let version 7 inherit version 3's approval, which is the precise failure the\n * per-version model exists to prevent, and it would do it silently.\n *\n * An absent `reviewState` is `unreviewed`, never `approved`: an email starter\n * is auto-listed like every other copied artifact, so most versions have no\n * verdict at all and the honest answer is that nobody has looked.\n */\nexport function resolveEmailStarterAssurance(\n version: { reviewState?: unknown } | null | undefined,\n): EmailStarterAssurance {\n const state = String(version?.reviewState ?? '')\n if (state === 'approved') return 'approved'\n if (state === 'rejected') return 'rejected'\n return 'unreviewed'\n}\n\n/** What the console says about an assurance, so no surface invents its own. */\nexport function emailStarterAssuranceLabel(\n assurance: EmailStarterAssurance,\n): string {\n switch (assurance) {\n case 'approved':\n return 'Reviewed by Aglyn'\n case 'rejected':\n return 'Turned down in review'\n default:\n return 'Not reviewed'\n }\n}\n\n/** Why an installed email design may not be mailed. */\nexport interface EmailStarterSendBlock {\n code: 'killed'\n /** Rendered to the tenant; staff's reason when they recorded one. */\n reason: string\n}\n\n/**\n * Whether the kill switch stops this installed design from being SENT.\n *\n * ## What a kill has to reach, and why the other levers do not reach it\n *\n * An email starter is copied on install: the tree becomes a screen document in\n * the tenant's own site and nothing reads the listing again. That is the right\n * versioning model — a publisher's push must not rewrite an email a tenant is\n * mailing from — but it means every lever the marketplace already had stops at\n * the storefront. `deletedAt` unpublishes, `hiddenAt` blocks browse and blocks\n * the next install, and a rejected version was never installable. None of them\n * is felt by a tenant who installed last week and is sending today.\n *\n * So the kill switch is the one that has to reach through, and it is checked\n * HERE — at the send — rather than by deleting or rewriting the tenant's\n * document. Refusing the send and leaving the content alone is the same rule\n * capacity enforcement follows: a control that reaches a customer's own data\n * to enforce a decision about somebody else's artifact takes the wrong thing\n * away. The tenant keeps the design, keeps editing it, keeps previewing it;\n * what they cannot do is put it on the shared sending domain.\n *\n * Rejection is deliberately NOT a kill and must not become one here: a version\n * turned down in review was never installable, so nothing is running it, and\n * conflating the two would let a review verdict silently stop live mail.\n *\n * A listing-wide kill catches an install whose provenance never recorded a\n * version (everything installed before the provenance stamp). A per-version\n * kill cannot, and does not pretend to — `isPluginRevoked` answers false for\n * an empty version against a version list, which is the honest reading.\n */\nexport function emailStarterSendBlock(input: {\n /** The install stamp on the document being sent. */\n installedFrom: { listingId?: string | null; version?: string | null } | null | undefined\n /** `revocations/{listingId}`, or null when there is none. */\n revocation: PluginRevocation | null | undefined\n}): EmailStarterSendBlock | null {\n const listingId = input.installedFrom?.listingId\n if (!listingId || !input.revocation) return null\n const version = String(input.installedFrom?.version ?? '')\n const killed = version\n ? isPluginRevoked(input.revocation, version)\n : isListingWideRevocation(input.revocation)\n if (!killed) return null\n const reason = String(input.revocation.reason ?? '').trim()\n return {\n code: 'killed',\n reason:\n 'This email was installed from a marketplace template that Aglyn has ' +\n 'since stopped' +\n (reason ? `: ${reason}. ` : '. ') +\n 'Sending from it is blocked. The design is still here and still ' +\n 'editable, and a campaign built from your own design sends normally.',\n }\n}\n"],"names":["isMediaCdnPath","isMediaRef","parseMediaRef","isListingWideRevocation","isPluginRevoked","decodeStoredNodes","URL_PROPS","MARKUP_PROPS","MERGE_TAG_OPEN","INLINE_IMAGE","MAIL_SAFE_HREF","truncate","value","text","String","length","slice","linkHost","test","URL","host","toLowerCase","isNodeMap","Array","isArray","entries","Object","values","every","entry","Boolean","inspectEmailStarter","raw","options","decoded","ok","violations","code","nodeId","componentId","prop","linkHosts","foreignMediaScopes","nodes","hosts","Set","foreignScopes","node","props","undefined","push","includes","ref","hostId","scope","split","add","sort","VIOLATION_COPY","unreadable","emailStarterRefusal","inspection","first","more","at","resolveEmailStarterAssurance","version","state","reviewState","emailStarterAssuranceLabel","assurance","emailStarterSendBlock","input","listingId","installedFrom","revocation","killed","reason","trim"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/email-starter-policy.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 * What a marketplace email starter may contain, and whether one may still be\n * mailed.\n *\n * In core rather than beside the rest of the marketplace model for the reason\n * `marketplace-provenance` gives: three different projects have to ask these\n * questions and a `scope:app` project may not depend on an `aglyn:addons` lib.\n * `marketplace-verification` used to be cited here too and no longer can be —\n * its console reader became the plugin's own surface, so the policy went with\n * it (AGL-3080). A shared reason is only shared while both halves still hold. The marketplace plugin enforces the\n * policy when content is published and installed; the marketing plugin asks\n * the kill question on the send path; the console renders the assurance.\n * Three readings of \"is this template safe to mail\" is the failure mode this\n * module exists to prevent.\n *\n * ## Why an email design needs a policy the page allowlist does not supply\n *\n * A published page component is rendered by us, inside a document we serve,\n * behind our CSP. A published EMAIL design is rendered by the recipient's mail\n * client, out of our reach entirely, and it leaves on a sending domain whose\n * reputation every tenant shares. The two facts that follow are the whole\n * reason this file is stricter than `sanitizeMarketplaceDefinition`:\n *\n * - **A remote asset in an email is a read receipt.** The recipient's client\n * fetches every `src` on open, so whatever host the string names learns that\n * this person opened this message, at this time, from this IP. Naming a\n * host the PUBLISHER controls hands a third party a recipient-level\n * engagement feed on the tenant's audience — processing with no recorded\n * basis and no `assertedBy` provenance, which is exactly what the consent\n * architecture exists to make impossible. A 1×1 pixel is the deliberate form;\n * an ordinary hero image is the same disclosure with a picture attached.\n * - **A link is sent under the tenant's name.** A recipient reads the From\n * header, not the publisher's listing page.\n *\n * So `src` is held to first-party or inline, and `href` to a transport a\n * recipient's client will not silently downgrade. Violations are REFUSED\n * rather than stripped: `sanitizePublishedNodeProps` drops an unsafe URL and\n * publishes the rest, which is right for a page — a missing image is a\n * cosmetic defect — and wrong here, because the publisher whose tracker was\n * quietly removed learns nothing and tries again.\n */\n\nimport { isMediaCdnPath, isMediaRef, parseMediaRef } from './media-ref'\nimport { isListingWideRevocation, isPluginRevoked } from './plugin-manifest'\nimport type { PluginRevocation } from './plugin-manifest'\nimport { decodeStoredNodes } from './stored-nodes'\n\n/** Props that carry a URL into a recipient's mail client. */\nconst URL_PROPS = ['src', 'href'] as const\n\n/**\n * Props whose value is markup rather than text.\n *\n * `emailHtml` is already off `MARKETPLACE_EMAIL_COMPONENT_ID_ALLOWLIST`, so a\n * publish cannot mint one. This catches the same content arriving as a PROP on\n * a block that is on the list — a hand-written document, or a block that grows\n * an `html` prop later — which the component-id allowlist alone would pass.\n */\nconst MARKUP_PROPS = ['html', 'dangerouslySetInnerHTML'] as const\n\n/** Merge-tag opener. Recipient data must never reach a URL (see below). */\nconst MERGE_TAG_OPEN = '{{'\n\n/** Inline image data, which is inert and discloses nothing on open. */\nconst INLINE_IMAGE = /^data:image\\//i\n\n/** Link transports a recipient's client will not silently downgrade. */\nconst MAIL_SAFE_HREF = /^(https:\\/\\/|mailto:|\\/|#)/i\n\nexport type EmailStarterViolationCode =\n /** A `src` naming a host that is not ours — the read-receipt vector. */\n | 'remote-asset'\n /** An `href` on a transport we will not send under a tenant's name. */\n | 'unsafe-link'\n /** Recipient data heading for a URL. */\n | 'merge-tag-in-url'\n /** Markup we would have to trust rather than render. */\n | 'raw-markup'\n /** The node map could not be read, so nothing about it is known. */\n | 'unreadable'\n\nexport interface EmailStarterViolation {\n code: EmailStarterViolationCode\n /** The offending node's id, or `'nodes'` when the map itself is the fault. */\n nodeId: string\n componentId: string\n /** The prop that carried it, for a message that can be acted on. */\n prop: string\n /** Truncated: a violation is reported to a publisher, and often logged. */\n value: string\n}\n\nexport interface EmailStarterInspection {\n ok: boolean\n violations: EmailStarterViolation[]\n /**\n * Every host this design would send a recipient to, sorted and deduplicated.\n *\n * The inspectable half of the link policy. A tenant about to mail their\n * customers under their own From header is entitled to see the list of\n * domains a stranger's template points them at, and no allowlist can decide\n * that for them — a link to a legitimate retailer and a link to the\n * publisher's affiliate funnel are the same shape.\n */\n linkHosts: string[]\n /**\n * Media scopes belonging to somebody other than the installing site.\n *\n * Not a violation: the asset is served by our own CDN, so the publisher\n * learns nothing about the recipient, and refusing would make almost every\n * real template uninstallable. It is REPORTED because the reference stays\n * live after a copy-on-install — the publisher can still replace the bytes\n * behind it — which is the one way an installed design can change without\n * an update being taken.\n */\n foreignMediaScopes: string[]\n /** The decoded map, so a caller need not decode twice. Null when unreadable. */\n nodes: Record<string, unknown> | null\n}\n\nfunction truncate(value: unknown): string {\n const text = typeof value === 'string' ? value : String(value ?? '')\n return text.length > 120 ? `${text.slice(0, 117)}...` : text\n}\n\n/**\n * The host a link points at, or null for a same-origin or non-URL target.\n *\n * `URL` rather than a regex because the question is \"which host will the mail\n * client resolve this to\", and that is the parser's answer, not ours. A value\n * that will not parse is not reported as a host — it is already refused by the\n * transport check, and inventing a host for it would put a fiction on a screen\n * a tenant is using to make a decision.\n */\nfunction linkHost(value: string): string | null {\n if (!/^https?:\\/\\//i.test(value)) return null\n try {\n return new URL(value).host.toLowerCase() || null\n } catch {\n return null\n }\n}\n\n/**\n * Whether a decoded value is a node MAP rather than something that merely\n * survived decoding.\n *\n * The case this exists for is the AGL-1394 shape: `nodes` read raw off a\n * screen version is a msgpack `Buffer`, and `Object.entries` of a Buffer\n * yields byte INDICES paired with NUMBERS. Every check below asks a node for a\n * `props` object, finds none, and the walk completes reporting a clean design.\n * A scanner that cannot read its input must say so, not pass it.\n *\n * `decodeStoredNodes` handles the three storage forms, so anything still\n * failing here was never a node map to begin with.\n */\nfunction isNodeMap(value: unknown): value is Record<string, unknown> {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return false\n const entries = Object.values(value as Record<string, unknown>)\n if (!entries.length) return false\n return entries.every(\n (entry) => Boolean(entry) && typeof entry === 'object' && !Array.isArray(entry),\n )\n}\n\n/**\n * Reads a marketplace email design against the mail-client policy above.\n *\n * Takes `nodes` in whatever form the document stored it — a plain Firestore\n * map, compressed msgpack bytes, or the `{type:'Buffer'}` envelope an export\n * bundle carries — and decodes before walking, because a naive walk over the\n * compressed form finds nothing and reports the design clean.\n *\n * @param raw - the version document's `nodes` field, undecoded.\n * @param options - carries `hostId`, the site the design is being installed\n * onto, used only to classify media scopes as local or foreign. Omitted at\n * publish time, where there is no installing site yet.\n */\nexport function inspectEmailStarter(\n raw: unknown,\n options?: { hostId?: string | null },\n): EmailStarterInspection {\n const decoded = decodeStoredNodes<Record<string, unknown>>(raw)\n if (!isNodeMap(decoded)) {\n return {\n ok: false,\n violations: [\n {\n code: 'unreadable',\n nodeId: 'nodes',\n componentId: '',\n prop: 'nodes',\n value: decoded === null ? 'undecodable' : 'not a node map',\n },\n ],\n linkHosts: [],\n foreignMediaScopes: [],\n nodes: null,\n }\n }\n\n const violations: EmailStarterViolation[] = []\n const hosts = new Set<string>()\n const foreignScopes = new Set<string>()\n\n for (const [nodeId, value] of Object.entries(decoded)) {\n const node = value as { componentId?: unknown; props?: unknown }\n const componentId = String(node.componentId ?? '')\n const props =\n node.props && typeof node.props === 'object' && !Array.isArray(node.props)\n ? (node.props as Record<string, unknown>)\n : {}\n\n for (const prop of MARKUP_PROPS) {\n if (props[prop] === undefined) continue\n violations.push({\n code: 'raw-markup',\n nodeId,\n componentId,\n prop,\n value: truncate(props[prop]),\n })\n }\n\n for (const prop of URL_PROPS) {\n const value = props[prop]\n if (typeof value !== 'string' || !value) continue\n\n // Checked before the transport rules, and reported as its own code,\n // because the two failures need different sentences: an `http:` link is\n // a mistake to correct, and a merge tag in a URL is recipient data on\n // its way into a query string — a standing prohibition, and the reason\n // it is prohibited is that a URL is logged by every hop that serves it.\n if (value.includes(MERGE_TAG_OPEN)) {\n violations.push({\n code: 'merge-tag-in-url',\n nodeId,\n componentId,\n prop,\n value: truncate(value),\n })\n continue\n }\n\n if (prop === 'src') {\n if (isMediaRef(value)) {\n const ref = parseMediaRef(value)\n if (!ref) {\n violations.push({\n code: 'remote-asset',\n nodeId,\n componentId,\n prop,\n value: truncate(value),\n })\n continue\n }\n if (options?.hostId && !ref.scope.split(':').includes(options.hostId)) {\n foreignScopes.add(ref.scope)\n }\n continue\n }\n if (isMediaCdnPath(value) || INLINE_IMAGE.test(value)) continue\n violations.push({\n code: 'remote-asset',\n nodeId,\n componentId,\n prop,\n value: truncate(value),\n })\n continue\n }\n\n const host = linkHost(value)\n if (host) hosts.add(host)\n if (!MAIL_SAFE_HREF.test(value)) {\n violations.push({\n code: 'unsafe-link',\n nodeId,\n componentId,\n prop,\n value: truncate(value),\n })\n }\n }\n }\n\n return {\n ok: violations.length === 0,\n violations,\n linkHosts: [...hosts].sort(),\n foreignMediaScopes: [...foreignScopes].sort(),\n nodes: decoded,\n }\n}\n\n/** One sentence per violation code, in the second person. */\nconst VIOLATION_COPY: Record<EmailStarterViolationCode, string> = {\n 'remote-asset':\n 'images must come from your own media library or be inline — an image ' +\n 'loaded from another site tells that site who opened the email',\n 'unsafe-link':\n 'links must be https, mailto, or a path on the sending site',\n 'merge-tag-in-url':\n 'a merge tag may not appear in a link or image address — that puts ' +\n 'recipient data into a URL',\n 'raw-markup':\n 'raw HTML cannot be published as an email design; build it from email ' +\n 'blocks',\n unreadable: 'the design could not be read',\n}\n\n/**\n * A refusal message for a failed inspection, or `undefined` when it passed.\n *\n * One message naming ONE violation and its count, rather than a list: the\n * caller is an API route whose body is rendered in a dialog, and a publisher\n * fixing eleven images fixes them the same way they fix the first.\n */\nexport function emailStarterRefusal(\n inspection: EmailStarterInspection,\n): string | undefined {\n const [first] = inspection.violations\n if (!first) return undefined\n const more = inspection.violations.length - 1\n const at = first.componentId ? ` (on a ${first.componentId} block)` : ''\n return (\n `This email design cannot be published or installed: ` +\n `${VIOLATION_COPY[first.code]}${at}. Found \"${first.value}\"` +\n (more > 0 ? ` and ${more} more like it.` : '.')\n )\n}\n\n/**\n * How much is actually known about the version of an email starter that was\n * installed.\n *\n * `unreviewed` is a real state, not a missing one, and it is the DEFAULT.\n */\nexport type EmailStarterAssurance = 'approved' | 'unreviewed' | 'rejected'\n\n/**\n * The assurance of ONE version, read from that version's own document.\n *\n * Deliberately takes the version and nothing else. A listing carries\n * `reviewStatus: 'verified'` and a `latestVersionReviewState` mirror, and both\n * describe the listing or its newest build — neither is a statement about the\n * version somebody is installing right now. Passing the listing in here would\n * let version 7 inherit version 3's approval, which is the precise failure the\n * per-version model exists to prevent, and it would do it silently.\n *\n * An absent `reviewState` is `unreviewed`, never `approved`: an email starter\n * is auto-listed like every other copied artifact, so most versions have no\n * verdict at all and the honest answer is that nobody has looked.\n */\nexport function resolveEmailStarterAssurance(\n version: { reviewState?: unknown } | null | undefined,\n): EmailStarterAssurance {\n const state = String(version?.reviewState ?? '')\n if (state === 'approved') return 'approved'\n if (state === 'rejected') return 'rejected'\n return 'unreviewed'\n}\n\n/** What the console says about an assurance, so no surface invents its own. */\nexport function emailStarterAssuranceLabel(\n assurance: EmailStarterAssurance,\n): string {\n switch (assurance) {\n case 'approved':\n return 'Reviewed by Aglyn'\n case 'rejected':\n return 'Turned down in review'\n default:\n return 'Not reviewed'\n }\n}\n\n/** Why an installed email design may not be mailed. */\nexport interface EmailStarterSendBlock {\n code: 'killed'\n /** Rendered to the tenant; staff's reason when they recorded one. */\n reason: string\n}\n\n/**\n * Whether the kill switch stops this installed design from being SENT.\n *\n * ## What a kill has to reach, and why the other levers do not reach it\n *\n * An email starter is copied on install: the tree becomes a screen document in\n * the tenant's own site and nothing reads the listing again. That is the right\n * versioning model — a publisher's push must not rewrite an email a tenant is\n * mailing from — but it means every lever the marketplace already had stops at\n * the storefront. `deletedAt` unpublishes, `hiddenAt` blocks browse and blocks\n * the next install, and a rejected version was never installable. None of them\n * is felt by a tenant who installed last week and is sending today.\n *\n * So the kill switch is the one that has to reach through, and it is checked\n * HERE — at the send — rather than by deleting or rewriting the tenant's\n * document. Refusing the send and leaving the content alone is the same rule\n * capacity enforcement follows: a control that reaches a customer's own data\n * to enforce a decision about somebody else's artifact takes the wrong thing\n * away. The tenant keeps the design, keeps editing it, keeps previewing it;\n * what they cannot do is put it on the shared sending domain.\n *\n * Rejection is deliberately NOT a kill and must not become one here: a version\n * turned down in review was never installable, so nothing is running it, and\n * conflating the two would let a review verdict silently stop live mail.\n *\n * A listing-wide kill catches an install whose provenance never recorded a\n * version (everything installed before the provenance stamp). A per-version\n * kill cannot, and does not pretend to — `isPluginRevoked` answers false for\n * an empty version against a version list, which is the honest reading.\n */\nexport function emailStarterSendBlock(input: {\n /** The install stamp on the document being sent. */\n installedFrom: { listingId?: string | null; version?: string | null } | null | undefined\n /** `revocations/{listingId}`, or null when there is none. */\n revocation: PluginRevocation | null | undefined\n}): EmailStarterSendBlock | null {\n const listingId = input.installedFrom?.listingId\n if (!listingId || !input.revocation) return null\n const version = String(input.installedFrom?.version ?? '')\n const killed = version\n ? isPluginRevoked(input.revocation, version)\n : isListingWideRevocation(input.revocation)\n if (!killed) return null\n const reason = String(input.revocation.reason ?? '').trim()\n return {\n code: 'killed',\n reason:\n 'This email was installed from a marketplace template that Aglyn has ' +\n 'since stopped' +\n (reason ? `: ${reason}. ` : '. ') +\n 'Sending from it is blocked. The design is still here and still ' +\n 'editable, and a campaign built from your own design sends normally.',\n }\n}\n"],"names":["isMediaCdnPath","isMediaRef","parseMediaRef","isListingWideRevocation","isPluginRevoked","decodeStoredNodes","URL_PROPS","MARKUP_PROPS","MERGE_TAG_OPEN","INLINE_IMAGE","MAIL_SAFE_HREF","truncate","value","text","String","length","slice","linkHost","test","URL","host","toLowerCase","isNodeMap","Array","isArray","entries","Object","values","every","entry","Boolean","inspectEmailStarter","raw","options","decoded","ok","violations","code","nodeId","componentId","prop","linkHosts","foreignMediaScopes","nodes","hosts","Set","foreignScopes","node","props","undefined","push","includes","ref","hostId","scope","split","add","sort","VIOLATION_COPY","unreadable","emailStarterRefusal","inspection","first","more","at","resolveEmailStarterAssurance","version","state","reviewState","emailStarterAssuranceLabel","assurance","emailStarterSendBlock","input","listingId","installedFrom","revocation","killed","reason","trim"],"mappings":"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"}
@@ -2187,6 +2187,40 @@ export interface DatasetQuotaResult {
2187
2187
  * plan's hard max; beyond the max the only path is upgrading.
2188
2188
  */
2189
2189
  export declare function checkDatasetQuota(org: Partial<AglynOrgBilling> | null | undefined, currentUsage: number): DatasetQuotaResult;
2190
+ export interface PluginOrgCapacityQuotaResult {
2191
+ /** False once usage meets the effective limit. */
2192
+ allowed: boolean;
2193
+ /** Included + purchased, clamped to the plan's ceiling for this capacity. */
2194
+ limit: number;
2195
+ remaining: number;
2196
+ /** What the plan includes — the figure a downgrade warning quotes. */
2197
+ included: number;
2198
+ /** What the org bought on top, 0 on a dead subscription. */
2199
+ purchased: number;
2200
+ /** How far buying can raise it on this plan. */
2201
+ ceiling: number;
2202
+ }
2203
+ /**
2204
+ * The quota for a capacity a PLUGIN backs (AGL-3080), from its declaration.
2205
+ *
2206
+ * `checkDatasetQuota` is this arithmetic written out for the one capacity
2207
+ * core happened to know the name of. It has a dozen callers on money paths
2208
+ * and keeps its own shape; what moves here is the ability to ask the same
2209
+ * question about a capacity core does not name, which is what a console meter
2210
+ * needs — it draws a row per declared capacity and cannot switch on `datasets`.
2211
+ *
2212
+ * The two must agree, and `plan-entitlements-plugin-capacity.spec.ts` is what
2213
+ * holds that: for every plan, this answers exactly what `checkDatasetQuota`
2214
+ * answers for the declared dataset capacity. A second implementation of a
2215
+ * limit is worth having only while something refuses to let the two drift.
2216
+ *
2217
+ * ⚑ An undeclared kind, or a declaration naming a field no plan carries, is
2218
+ * ZERO — included, ceiling and limit all — which reads as "you are over" and
2219
+ * shows a meter full. That is the same direction `overLimitRows` and
2220
+ * `includedCapacity` already fail in, and for the same reason: the reassuring
2221
+ * answer here would be a meter promising room the create is refused at.
2222
+ */
2223
+ export declare function checkPluginOrgCapacityQuota(org: Partial<AglynOrgBilling> | null | undefined, kind: string, currentUsage: number): PluginOrgCapacityQuotaResult;
2190
2224
  export interface DataStorageQuotaResult {
2191
2225
  /**
2192
2226
  * False only when the plan hard-blocks (no overage pricing) and usage
@@ -18,6 +18,7 @@ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_wi
18
18
  import { isOrgOverrideReasonCode } from "./org-override-reason.js";
19
19
  import { PLATFORM_BRAND_NAME, PLATFORM_BRANDING_PROFILE } from "./platform-brand.js";
20
20
  import { listPluginFeatures, listPluginSeatAddons, pluginSeatAddon } from "../plugin-manager/plugin-entitlements.js";
21
+ import { pluginOrgCapacity } from "../plugin-manager/plugin-org-capacity.js";
21
22
  /** Sentinel for quotas a plan does not cap; `checkQuota` always allows. */ export const UNLIMITED = Number.POSITIVE_INFINITY;
22
23
  /**
23
24
  * How many saved form definitions one site may hold, on every plan that can
@@ -3570,6 +3571,45 @@ export function checkQuota(org, quota, currentUsage) {
3570
3571
  addonPriceUsd
3571
3572
  };
3572
3573
  }
3574
+ /**
3575
+ * The quota for a capacity a PLUGIN backs (AGL-3080), from its declaration.
3576
+ *
3577
+ * `checkDatasetQuota` is this arithmetic written out for the one capacity
3578
+ * core happened to know the name of. It has a dozen callers on money paths
3579
+ * and keeps its own shape; what moves here is the ability to ask the same
3580
+ * question about a capacity core does not name, which is what a console meter
3581
+ * needs — it draws a row per declared capacity and cannot switch on `datasets`.
3582
+ *
3583
+ * The two must agree, and `plan-entitlements-plugin-capacity.spec.ts` is what
3584
+ * holds that: for every plan, this answers exactly what `checkDatasetQuota`
3585
+ * answers for the declared dataset capacity. A second implementation of a
3586
+ * limit is worth having only while something refuses to let the two drift.
3587
+ *
3588
+ * ⚑ An undeclared kind, or a declaration naming a field no plan carries, is
3589
+ * ZERO — included, ceiling and limit all — which reads as "you are over" and
3590
+ * shows a meter full. That is the same direction `overLimitRows` and
3591
+ * `includedCapacity` already fail in, and for the same reason: the reassuring
3592
+ * answer here would be a meter promising room the create is refused at.
3593
+ */ export function checkPluginOrgCapacityQuota(org, kind, currentUsage) {
3594
+ var _entitlements_capacity_includedEntitlement, _entitlements_capacity_purchaseCeilingEntitlement, _resolvePurchasedAddons_capacity_addonKind;
3595
+ const capacity = pluginOrgCapacity(kind);
3596
+ const entitlements = resolveOrgEntitlements(org);
3597
+ const included = capacity ? Number((_entitlements_capacity_includedEntitlement = entitlements[capacity.includedEntitlement]) != null ? _entitlements_capacity_includedEntitlement : 0) : 0;
3598
+ const ceiling = capacity ? Number((_entitlements_capacity_purchaseCeilingEntitlement = entitlements[capacity.purchaseCeilingEntitlement]) != null ? _entitlements_capacity_purchaseCeilingEntitlement : 0) : 0;
3599
+ // The add-on kind IS the `seatAddons` key it is stored under — the
3600
+ // generator refuses two capacities on one add-on kind, so one purchased
3601
+ // quantity can only ever raise the capacity that sells it.
3602
+ const purchased = capacity ? Math.max(0, Number((_resolvePurchasedAddons_capacity_addonKind = resolvePurchasedAddons(org)[capacity.addonKind]) != null ? _resolvePurchasedAddons_capacity_addonKind : 0)) : 0;
3603
+ const limit = Math.min(included + purchased, ceiling);
3604
+ return {
3605
+ allowed: currentUsage < limit,
3606
+ limit,
3607
+ remaining: Math.max(0, limit - currentUsage),
3608
+ included,
3609
+ purchased,
3610
+ ceiling
3611
+ };
3612
+ }
3573
3613
  /**
3574
3614
  * Org dataset-storage meter (AGL-240): aggregate stored document bytes
3575
3615
  * across `orgs/{orgId}/datasets`. Plans with an `extraDataGbMonthlyUsd`