@aglyn/aglyn 1.0.0-beta.211 → 1.0.0-beta.213
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -11
- package/src/lib/app-utils/docs-index.generated.js +14 -7
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/plugin-bundle-checks.d.ts +3 -2
- package/src/lib/app-utils/plugin-bundle-checks.js +242 -3
- package/src/lib/app-utils/plugin-bundle-checks.js.map +1 -1
- package/src/lib/app-utils/plugin-host-abi.d.ts +61 -0
- package/src/lib/app-utils/plugin-host-abi.js +68 -0
- package/src/lib/app-utils/plugin-host-abi.js.map +1 -0
- package/src/lib/app-utils/realm-host-surface.generated.d.ts +9 -0
- package/src/lib/app-utils/realm-host-surface.generated.js +143 -0
- package/src/lib/app-utils/realm-host-surface.generated.js.map +1 -0
- package/src/lib/app-utils/search-engine-verification.d.ts +98 -0
- package/src/lib/app-utils/search-engine-verification.js +140 -0
- package/src/lib/app-utils/search-engine-verification.js.map +1 -0
- package/src/lib/app-utils/site-entity-json-ld.d.ts +9 -1
- package/src/lib/app-utils/site-entity-json-ld.js +25 -7
- package/src/lib/app-utils/site-entity-json-ld.js.map +1 -1
- package/src/lib/app-utils/variables.d.ts +18 -19
- package/src/lib/app-utils/variables.js +24 -23
- package/src/lib/app-utils/variables.js.map +1 -1
- package/src/lib/app-utils/webhook-delivery.js +3 -1
- package/src/lib/app-utils/webhook-delivery.js.map +1 -1
- package/src/lib/foundation/definitions/platform.types.d.ts +24 -0
- package/src/lib/foundation/definitions/platform.types.js +2 -1
- package/src/lib/foundation/definitions/platform.types.js.map +1 -1
- package/src/lib/plugin-manager/enabled-plugins.d.ts +6 -0
- package/src/lib/plugin-manager/enabled-plugins.js +5 -0
- package/src/lib/plugin-manager/enabled-plugins.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.d.ts +6 -0
- package/src/lib/plugin-manager/first-party-plugins.generated.js +8 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
- package/src/lib/plugin-manager/plugin-contributions.d.ts +59 -0
- package/src/lib/plugin-manager/plugin-contributions.js +113 -0
- package/src/lib/plugin-manager/plugin-contributions.js.map +1 -1
- package/src/lib/plugin-manager/realm-host-aglyn.generated.d.ts +8 -0
- package/src/lib/plugin-manager/realm-host-aglyn.generated.js +101 -0
- package/src/lib/plugin-manager/realm-host-aglyn.generated.js.map +1 -0
- package/src/lib/plugin-manager/realm-plugins.d.ts +3 -2
- package/src/lib/plugin-manager/realm-plugins.js +8 -3
- package/src/lib/plugin-manager/realm-plugins.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/site-entity-json-ld.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 {\n type ContentAuthorImageContext,\n type HostSeoEntity,\n type SiteEntityHost,\n AUTHOR_NAME_MAX_LENGTH,\n SITE_ENTITY_FRAGMENT,\n contentAuthorSchemaType,\n hostSeoEntityImageJsonLd,\n siteProfileUrls,\n} from './content-authors'\nimport {\n type LocalBusinessType,\n PAYMENT_ACCEPTED_MAX_LENGTH,\n PRICE_RANGE_MAX_LENGTH,\n localBusinessType,\n normalizeAreaServed,\n parseOpeningHours,\n} from './local-business'\n\n/*\n * The SITE entity's JSON-LD: the nested `publisher` and the standalone node.\n *\n * ⛔ Kept apart from `content-authors`, which the compose pipeline reads and\n * therefore ships whole in the site runtime a published page downloads\n * (`check:tenant-wire-weight`). Only the tenant page's server render calls\n * these, and the LocalBusiness allow-list and hours parser they carry\n * (AGL-3383) have no business in a visitor's bundle. Reached through\n * `@aglyn/aglyn/server` only.\n */\n\n/** `content-authors`' trim-and-cap, for the fields this module reads. */\nconst text = (value: unknown, max = AUTHOR_NAME_MAX_LENGTH): string =>\n typeof value === 'string' ? value.trim().slice(0, max) : ''\n\n/**\n * The `@type` the SITE entity publishes under — {@link contentAuthorSchemaType}\n * with one refinement (AGL-3383): an entity that is not a Person and names a\n * business type on the allow-list publishes as that type.\n *\n * Person wins over a business type, because `schema.org` gives a Person none\n * of the properties a business type exists to carry. The stored `type` stays\n * Organization or Person either way; every LocalBusiness subtype IS an\n * Organization, so a consumer reading this as one is still right.\n *\n * Shared by the standalone node and the nested `publisher`, which carry the\n * same `@id` and must therefore not disagree about what they are.\n */\nexport function siteEntitySchemaType(\n entity: HostSeoEntity | null | undefined,\n): 'Person' | 'Organization' | LocalBusinessType {\n const base = contentAuthorSchemaType(entity?.type)\n if (base === 'Person') return base\n return localBusinessType(entity?.businessType) ?? base\n}\n\n/**\n * `schema.org` JSON-LD for the SITE entity (`host.seo.entity`) — the value\n * `Article.publisher` and `WebSite.publisher` are built from.\n *\n * Lives beside {@link contentAuthorJsonLd} on purpose (AGL-2486): a site\n * entity and a post author answer the same question about two different\n * subjects, and the reason the Person branch was broken for two years is that\n * the answer was written inline at its one call site where nothing could\n * test it. One module, two entry points, one `Person`/`Organization`\n * decision — {@link contentAuthorSchemaType}.\n *\n * `logo` is emitted verbatim on BOTH branches, unchanged from what the tenant\n * has always published. It is `host.seo.entity.logo`, a plain URL field on\n * the Setup form rather than a media-picker target, and re-keying it to\n * `image` for a Person would change the publisher block on every page of\n * every site that set one — a separate decision from this one.\n */\nexport function hostSeoEntityJsonLd(\n entity: HostSeoEntity | null | undefined,\n): Record<string, unknown> | undefined {\n const name = text(entity?.name, 400)\n if (!name) return undefined\n return {\n '@type': siteEntitySchemaType(entity),\n name,\n }\n}\n\n/**\n * The site's publisher as a STANDALONE, fully populated `schema.org` node\n * (AGL-2716).\n *\n * ## Why this exists next to {@link hostSeoEntityJsonLd}\n *\n * That function builds the value `publisher` takes: a `@type` and a `name`,\n * nested inside a `WebSite` or an `Article`, gated on the author having filled\n * in the Entity form. It is deliberately narrow, and every call site nests it.\n *\n * A consumer asking \"who runs this site, and how do I reach them\" — an AI\n * assistant answering a contact question, a readiness audit checking whether a\n * business is identifiable — looks for a TOP-LEVEL node of type `Organization`\n * carrying `name`, `description`, `contactPoint` and `address`. A publisher\n * nested two levels inside a `WebSite` answers a different question, and a\n * publisher that is absent because nobody filled in a form answers none.\n *\n * So this one:\n *\n * - is emitted as its own `<script type=\"application/ld+json\">`, at the top\n * level, where it can be found without walking into another node;\n * - FALLS BACK to the site's own identity when the Entity form is empty, so\n * every site on the platform publishes a complete entity rather than only\n * the sites whose author found the form;\n * - carries the contact and address fields, which are what turn \"a name\" into\n * \"a business a consumer can verify\".\n *\n * ## Why the fallback is safe\n *\n * It changes nothing that already exists: `hostSeoEntityJsonLd` still gates on\n * `entity.name`, so `publisher` appears exactly where it appeared before. The\n * fallback only decides what THIS node says, and a site's display name is its\n * publisher's name on the overwhelming majority of sites — the two differ when\n * a company's trading name differs from its site's, which is precisely the case\n * the Entity form exists to express.\n *\n * ## `@id`\n *\n * Both this node and the nested `publisher` carry the same `@id` when an\n * origin is known, so a consumer merges them into one entity instead of\n * reading a site that names its publisher twice with different detail.\n *\n * Returns `undefined` only when there is no name to be had from anywhere —\n * a host document with no entity, no display name and no SEO title.\n */\nexport function siteEntityJsonLd(\n host: SiteEntityHost | null | undefined,\n context?: ContentAuthorImageContext,\n): Record<string, unknown> | undefined {\n const entity = host?.seo?.entity ?? undefined\n const name =\n text(entity?.name, 400) ||\n text(host?.displayName, 400) ||\n text(host?.seo?.title, 400)\n if (!name) return undefined\n\n const origin = context?.origin?.replace(/\\/+$/, '') ?? ''\n const description =\n text(entity?.description, 800) || text(host?.seo?.description, 800)\n const url = text(entity?.url, 800) || origin\n\n /*\n `sameAs` is de-duplicated and https-only, matching the author serializer:\n a profile URL that is not fetchable teaches a consumer nothing, and the\n same profile listed twice reads as two identities.\n\n Through `siteProfileUrls` (AGL-3148) so the site's rendered social links\n count as profiles it claims, rather than only the SEO field that no host\n on the platform had filled in.\n */\n const sameAs = siteProfileUrls(host)\n\n /*\n A contactPoint needs something to contact. `contactType` alone is a label\n on nothing, so the point is emitted only when an address or a number is\n present — `schema.org` requires neither, and a consumer that finds a\n ContactPoint with no channel has been told less than it would learn from\n its absence.\n */\n const email = text(entity?.email, 320)\n const telephone = text(entity?.telephone, 64)\n const contactType = text(entity?.contactType, 120) || 'customer support'\n const contactPoint =\n email || telephone\n ? {\n '@type': 'ContactPoint',\n contactType,\n ...(email ? { email } : {}),\n ...(telephone ? { telephone } : {}),\n }\n : undefined\n\n const addressFields = {\n streetAddress: text(entity?.address?.streetAddress, 200),\n addressLocality: text(entity?.address?.addressLocality, 120),\n addressRegion: text(entity?.address?.addressRegion, 120),\n postalCode: text(entity?.address?.postalCode, 40),\n addressCountry: text(entity?.address?.addressCountry, 120),\n }\n const address = Object.entries(addressFields).filter(([, value]) => value)\n const schemaType = siteEntitySchemaType(entity)\n const business =\n schemaType !== 'Person' && schemaType !== 'Organization'\n ? localBusinessJsonLd(entity, telephone)\n : {}\n\n return {\n '@context': 'https://schema.org',\n '@type': schemaType,\n ...(origin ? { '@id': `${origin}/${SITE_ENTITY_FRAGMENT}` } : {}),\n name,\n ...(description ? { description } : {}),\n ...(url ? { url } : {}),\n // `logo` for an Organization, `image` for a Person — the one field whose\n // KEY the branch changes. Shared with the nested publisher so the two\n // nodes cannot describe the same entity with different pictures.\n ...hostSeoEntityImageJsonLd(entity, context),\n ...(sameAs.length ? { sameAs } : {}),\n ...(contactPoint ? { contactPoint } : {}),\n ...(address.length\n ? { address: { '@type': 'PostalAddress', ...Object.fromEntries(address) } }\n : {}),\n ...business,\n }\n}\n\n/**\n * The properties only a LocalBusiness carries (AGL-3383), spread after\n * everything an Organization publishes so an Organization's output is\n * unchanged key for key.\n *\n * - `telephone` on the node itself as well as on its `contactPoint`: a\n * LocalBusiness has the property directly, and it is where a local result\n * reads the number from.\n * - `areaServed` as named `City` places. A service-area business commonly has\n * no street address to publish, and this is what says where it works.\n * - `openingHoursSpecification` from the hours text, invalid lines dropped.\n *\n * Every one of them is omitted when empty, never published as `[]` or `''`.\n */\nfunction localBusinessJsonLd(\n entity: HostSeoEntity | null | undefined,\n telephone: string,\n): Record<string, unknown> {\n const areaServed = normalizeAreaServed(entity?.areaServed)\n const hours = parseOpeningHours(entity?.openingHours)\n const priceRange = text(entity?.priceRange, PRICE_RANGE_MAX_LENGTH)\n const paymentAccepted = text(\n entity?.paymentAccepted,\n PAYMENT_ACCEPTED_MAX_LENGTH,\n )\n return {\n ...(telephone ? { telephone } : {}),\n ...(areaServed.length\n ? { areaServed: areaServed.map((name) => ({ '@type': 'City', name })) }\n : {}),\n ...(hours.length\n ? {\n openingHoursSpecification: hours.map((row) => ({\n '@type': 'OpeningHoursSpecification',\n dayOfWeek: row.days,\n opens: row.opens,\n closes: row.closes,\n })),\n }\n : {}),\n ...(priceRange ? { priceRange } : {}),\n ...(paymentAccepted ? { paymentAccepted } : {}),\n }\n}\n"],"names":["AUTHOR_NAME_MAX_LENGTH","SITE_ENTITY_FRAGMENT","contentAuthorSchemaType","hostSeoEntityImageJsonLd","siteProfileUrls","PAYMENT_ACCEPTED_MAX_LENGTH","PRICE_RANGE_MAX_LENGTH","localBusinessType","normalizeAreaServed","parseOpeningHours","text","value","max","trim","slice","siteEntitySchemaType","entity","base","type","businessType","hostSeoEntityJsonLd","name","undefined","siteEntityJsonLd","host","context","seo","displayName","title","origin","replace","description","url","sameAs","email","telephone","contactType","contactPoint","addressFields","streetAddress","address","addressLocality","addressRegion","postalCode","addressCountry","Object","entries","filter","schemaType","business","localBusinessJsonLd","length","fromEntries","areaServed","hours","openingHours","priceRange","paymentAccepted","map","openingHoursSpecification","row","dayOfWeek","days","opens","closes"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/site-entity-json-ld.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 {\n type ContentAuthorImageContext,\n type HostSeoEntity,\n type SiteEntityHost,\n AUTHOR_NAME_MAX_LENGTH,\n SITE_ENTITY_FRAGMENT,\n contentAuthorSchemaType,\n hostSeoEntityImageJsonLd,\n siteProfileUrls,\n} from './content-authors'\nimport {\n type LocalBusinessType,\n PAYMENT_ACCEPTED_MAX_LENGTH,\n PRICE_RANGE_MAX_LENGTH,\n localBusinessType,\n normalizeAreaServed,\n parseOpeningHours,\n} from './local-business'\n\n/*\n * The SITE entity's JSON-LD: the nested `publisher` and the standalone node.\n *\n * ⛔ Kept apart from `content-authors`, which the compose pipeline reads and\n * therefore ships whole in the site runtime a published page downloads\n * (`check:tenant-wire-weight`). Only the tenant page's server render calls\n * these, and the LocalBusiness allow-list and hours parser they carry\n * (AGL-3383) have no business in a visitor's bundle. Reached through\n * `@aglyn/aglyn/server` only.\n */\n\n/** `content-authors`' trim-and-cap, for the fields this module reads. */\nconst text = (value: unknown, max = AUTHOR_NAME_MAX_LENGTH): string =>\n typeof value === 'string' ? value.trim().slice(0, max) : ''\n\n/**\n * The `@type` the SITE entity publishes under — {@link contentAuthorSchemaType}\n * with one refinement (AGL-3383): an entity that is not a Person and names a\n * business type on the allow-list publishes as that type.\n *\n * Person wins over a business type, because `schema.org` gives a Person none\n * of the properties a business type exists to carry. The stored `type` stays\n * Organization or Person either way; every LocalBusiness subtype IS an\n * Organization, so a consumer reading this as one is still right.\n *\n * Shared by the standalone node and the nested `publisher`, which carry the\n * same `@id` and must therefore not disagree about what they are.\n */\nexport function siteEntitySchemaType(\n entity: HostSeoEntity | null | undefined,\n): 'Person' | 'Organization' | LocalBusinessType {\n const base = contentAuthorSchemaType(entity?.type)\n if (base === 'Person') return base\n return localBusinessType(entity?.businessType) ?? base\n}\n\n/**\n * `schema.org` JSON-LD for the SITE entity (`host.seo.entity`) — the value\n * `Article.publisher` and `WebSite.publisher` are built from.\n *\n * Lives beside {@link contentAuthorJsonLd} on purpose (AGL-2486): a site\n * entity and a post author answer the same question about two different\n * subjects, and the reason the Person branch was broken for two years is that\n * the answer was written inline at its one call site where nothing could\n * test it. One module, two entry points, one `Person`/`Organization`\n * decision — {@link contentAuthorSchemaType}.\n *\n * `logo` is emitted verbatim on BOTH branches, unchanged from what the tenant\n * has always published. It is `host.seo.entity.logo`, a plain URL field on\n * the Setup form rather than a media-picker target, and re-keying it to\n * `image` for a Person would change the publisher block on every page of\n * every site that set one — a separate decision from this one.\n *\n * Given the site's origin, the node carries the standalone entity's `@id`\n * (AGL-3398). Without it a consumer reads this name-only node as a SECOND\n * entity: Google's Rich Results Test listed a site's LocalBusiness twice, the\n * nested one missing telephone, address, price range and image. With it, the\n * two are one entity described in two places.\n */\nexport function hostSeoEntityJsonLd(\n entity: HostSeoEntity | null | undefined,\n context?: { origin?: string },\n): Record<string, unknown> | undefined {\n const name = text(entity?.name, 400)\n if (!name) return undefined\n const origin = context?.origin?.replace(/\\/+$/, '') ?? ''\n return {\n '@type': siteEntitySchemaType(entity),\n ...(origin ? { '@id': `${origin}/${SITE_ENTITY_FRAGMENT}` } : {}),\n name,\n }\n}\n\n/**\n * The site's publisher as a STANDALONE, fully populated `schema.org` node\n * (AGL-2716).\n *\n * ## Why this exists next to {@link hostSeoEntityJsonLd}\n *\n * That function builds the value `publisher` takes: a `@type` and a `name`,\n * nested inside a `WebSite` or an `Article`, gated on the author having filled\n * in the Entity form. It is deliberately narrow, and every call site nests it.\n *\n * A consumer asking \"who runs this site, and how do I reach them\" — an AI\n * assistant answering a contact question, a readiness audit checking whether a\n * business is identifiable — looks for a TOP-LEVEL node of type `Organization`\n * carrying `name`, `description`, `contactPoint` and `address`. A publisher\n * nested two levels inside a `WebSite` answers a different question, and a\n * publisher that is absent because nobody filled in a form answers none.\n *\n * So this one:\n *\n * - is emitted as its own `<script type=\"application/ld+json\">`, at the top\n * level, where it can be found without walking into another node;\n * - FALLS BACK to the site's own identity when the Entity form is empty, so\n * every site on the platform publishes a complete entity rather than only\n * the sites whose author found the form;\n * - carries the contact and address fields, which are what turn \"a name\" into\n * \"a business a consumer can verify\".\n *\n * ## Why the fallback is safe\n *\n * It changes nothing that already exists: `hostSeoEntityJsonLd` still gates on\n * `entity.name`, so `publisher` appears exactly where it appeared before. The\n * fallback only decides what THIS node says, and a site's display name is its\n * publisher's name on the overwhelming majority of sites — the two differ when\n * a company's trading name differs from its site's, which is precisely the case\n * the Entity form exists to express.\n *\n * ## `@id`\n *\n * Both this node and the nested `publisher` carry the same `@id` when an\n * origin is known, so a consumer merges them into one entity instead of\n * reading a site that names its publisher twice with different detail.\n *\n * Returns `undefined` only when there is no name to be had from anywhere —\n * a host document with no entity, no display name and no SEO title.\n */\nexport function siteEntityJsonLd(\n host: SiteEntityHost | null | undefined,\n context?: ContentAuthorImageContext,\n): Record<string, unknown> | undefined {\n const entity = host?.seo?.entity ?? undefined\n const name =\n text(entity?.name, 400) ||\n text(host?.displayName, 400) ||\n text(host?.seo?.title, 400)\n if (!name) return undefined\n\n const origin = context?.origin?.replace(/\\/+$/, '') ?? ''\n const description =\n text(entity?.description, 800) || text(host?.seo?.description, 800)\n const url = text(entity?.url, 800) || origin\n\n /*\n `sameAs` is de-duplicated and https-only, matching the author serializer:\n a profile URL that is not fetchable teaches a consumer nothing, and the\n same profile listed twice reads as two identities.\n\n Through `siteProfileUrls` (AGL-3148) so the site's rendered social links\n count as profiles it claims, rather than only the SEO field that no host\n on the platform had filled in.\n */\n const sameAs = siteProfileUrls(host)\n\n /*\n A contactPoint needs something to contact. `contactType` alone is a label\n on nothing, so the point is emitted only when an address or a number is\n present — `schema.org` requires neither, and a consumer that finds a\n ContactPoint with no channel has been told less than it would learn from\n its absence.\n */\n const email = text(entity?.email, 320)\n const telephone = text(entity?.telephone, 64)\n const contactType = text(entity?.contactType, 120) || 'customer support'\n const contactPoint =\n email || telephone\n ? {\n '@type': 'ContactPoint',\n contactType,\n ...(email ? { email } : {}),\n ...(telephone ? { telephone } : {}),\n }\n : undefined\n\n const addressFields = {\n streetAddress: text(entity?.address?.streetAddress, 200),\n addressLocality: text(entity?.address?.addressLocality, 120),\n addressRegion: text(entity?.address?.addressRegion, 120),\n postalCode: text(entity?.address?.postalCode, 40),\n addressCountry: text(entity?.address?.addressCountry, 120),\n }\n const address = Object.entries(addressFields).filter(([, value]) => value)\n const schemaType = siteEntitySchemaType(entity)\n const picture = hostSeoEntityImageJsonLd(entity, context)\n const business =\n schemaType !== 'Person' && schemaType !== 'Organization'\n ? localBusinessJsonLd(entity, telephone, picture.logo)\n : {}\n\n return {\n '@context': 'https://schema.org',\n '@type': schemaType,\n ...(origin ? { '@id': `${origin}/${SITE_ENTITY_FRAGMENT}` } : {}),\n name,\n ...(description ? { description } : {}),\n ...(url ? { url } : {}),\n // `logo` for an Organization, `image` for a Person — the one field whose\n // KEY the branch changes. Shared with the nested publisher so the two\n // nodes cannot describe the same entity with different pictures.\n ...picture,\n ...(sameAs.length ? { sameAs } : {}),\n ...(contactPoint ? { contactPoint } : {}),\n ...(address.length\n ? { address: { '@type': 'PostalAddress', ...Object.fromEntries(address) } }\n : {}),\n ...business,\n }\n}\n\n/**\n * The properties only a LocalBusiness carries (AGL-3383), spread after\n * everything an Organization publishes so an Organization's output is\n * unchanged key for key.\n *\n * - `telephone` on the node itself as well as on its `contactPoint`: a\n * LocalBusiness has the property directly, and it is where a local result\n * reads the number from.\n * - `areaServed` as named `City` places. A service-area business commonly has\n * no street address to publish, and this is what says where it works.\n * - `openingHoursSpecification` from the hours text, invalid lines dropped.\n * - `image`, the entity logo (AGL-3398): Google recommends a picture of a\n * LocalBusiness and reads `image`, not the Organization-only `logo`, which\n * stays as well.\n *\n * Every one of them is omitted when empty, never published as `[]` or `''`.\n */\nfunction localBusinessJsonLd(\n entity: HostSeoEntity | null | undefined,\n telephone: string,\n logo: string | undefined,\n): Record<string, unknown> {\n const areaServed = normalizeAreaServed(entity?.areaServed)\n const hours = parseOpeningHours(entity?.openingHours)\n const priceRange = text(entity?.priceRange, PRICE_RANGE_MAX_LENGTH)\n const paymentAccepted = text(\n entity?.paymentAccepted,\n PAYMENT_ACCEPTED_MAX_LENGTH,\n )\n return {\n ...(telephone ? { telephone } : {}),\n ...(areaServed.length\n ? { areaServed: areaServed.map((name) => ({ '@type': 'City', name })) }\n : {}),\n ...(hours.length\n ? {\n openingHoursSpecification: hours.map((row) => ({\n '@type': 'OpeningHoursSpecification',\n dayOfWeek: row.days,\n opens: row.opens,\n closes: row.closes,\n })),\n }\n : {}),\n ...(priceRange ? { priceRange } : {}),\n ...(paymentAccepted ? { paymentAccepted } : {}),\n ...(logo ? { image: logo } : {}),\n }\n}\n"],"names":["AUTHOR_NAME_MAX_LENGTH","SITE_ENTITY_FRAGMENT","contentAuthorSchemaType","hostSeoEntityImageJsonLd","siteProfileUrls","PAYMENT_ACCEPTED_MAX_LENGTH","PRICE_RANGE_MAX_LENGTH","localBusinessType","normalizeAreaServed","parseOpeningHours","text","value","max","trim","slice","siteEntitySchemaType","entity","base","type","businessType","hostSeoEntityJsonLd","context","name","undefined","origin","replace","siteEntityJsonLd","host","seo","displayName","title","description","url","sameAs","email","telephone","contactType","contactPoint","addressFields","streetAddress","address","addressLocality","addressRegion","postalCode","addressCountry","Object","entries","filter","schemaType","picture","business","localBusinessJsonLd","logo","length","fromEntries","areaServed","hours","openingHours","priceRange","paymentAccepted","map","openingHoursSpecification","row","dayOfWeek","days","opens","closes","image"],"mappings":"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"}
|
|
@@ -56,20 +56,6 @@ export type HostFunctionLookup = Record<string, HostFunction>;
|
|
|
56
56
|
export declare function resolveBindings(text: string, variables: Record<string, HostVariable>, functions?: HostFunctionLookup): string;
|
|
57
57
|
/** Fast pre-check so binding-free content skips the walk entirely. */
|
|
58
58
|
export declare function hasBindings(text: string): boolean;
|
|
59
|
-
/**
|
|
60
|
-
* Applies `resolveBindings` to every string prop across a normalized nodes
|
|
61
|
-
* map (mutating a shallow copy). String props only — objects/arrays (sx,
|
|
62
|
-
* option lists) pass through untouched; unsafe values in hrefs remain
|
|
63
|
-
* covered by the render-time SAFE_HREF/sanitizer checks.
|
|
64
|
-
*/
|
|
65
|
-
/** Component id of the interactive function widget (plugins-mui). */
|
|
66
|
-
export declare const FUNCTION_WIDGET_COMPONENT_ID = "functionWidget";
|
|
67
|
-
/**
|
|
68
|
-
* Component id of the Calculator container (plugins-mui, AGL-3202): the
|
|
69
|
-
* other element that runs a function in the visitor's browser, and so the
|
|
70
|
-
* other one compose hands a definition to.
|
|
71
|
-
*/
|
|
72
|
-
export declare const FUNCTION_SCOPE_COMPONENT_ID = "functionScope";
|
|
73
59
|
/**
|
|
74
60
|
* The site variables ONE function reads, by name (AGL-3202).
|
|
75
61
|
*
|
|
@@ -82,15 +68,28 @@ export declare const FUNCTION_SCOPE_COMPONENT_ID = "functionScope";
|
|
|
82
68
|
*/
|
|
83
69
|
export declare function functionGlobals(definition: HostFunction, variables: Record<string, HostVariable>): Record<string, number | string | boolean>;
|
|
84
70
|
/**
|
|
85
|
-
* Injects each function
|
|
86
|
-
* (AGL-93): the client runs the shared evaluator locally, so the
|
|
87
|
-
* page carries the definition instead of calling home. Unknown
|
|
88
|
-
* the node untouched (the
|
|
71
|
+
* Injects each function-running element's definition into its props at
|
|
72
|
+
* compose time (AGL-93): the client runs the shared evaluator locally, so the
|
|
73
|
+
* published page carries the definition instead of calling home. Unknown
|
|
74
|
+
* names leave the node untouched (the element renders its editor
|
|
75
|
+
* placeholder).
|
|
76
|
+
*
|
|
77
|
+
* Which elements run a function is DECLARED (AGL-3393): `bindings` maps a
|
|
78
|
+
* component id to the prop that names its function, collected from each
|
|
79
|
+
* plugin's `contributes.site.functionBindings`. Core names no element, so a
|
|
80
|
+
* marketplace plugin's element is bound exactly as a first-party one is, and
|
|
81
|
+
* an element nobody declared is never handed a definition.
|
|
89
82
|
*
|
|
90
83
|
* With `variables`, the site variables that function reads ride along as
|
|
91
84
|
* `globals` (AGL-3202) — see {@link functionGlobals} for why it is only
|
|
92
85
|
* those. The key is omitted when the function reads none, so a page built
|
|
93
86
|
* before this existed serializes exactly as it did.
|
|
94
87
|
*/
|
|
95
|
-
export declare function attachFunctionDefinitions<T extends Record<string, any>>(nodes: T, functions: HostFunctionLookup, variables
|
|
88
|
+
export declare function attachFunctionDefinitions<T extends Record<string, any>>(nodes: T, functions: HostFunctionLookup, variables: Record<string, HostVariable>, bindings: Readonly<Record<string, string>>): T;
|
|
89
|
+
/**
|
|
90
|
+
* Applies `resolveBindings` to every string prop across a normalized nodes
|
|
91
|
+
* map (mutating a shallow copy). String props only — objects/arrays (sx,
|
|
92
|
+
* option lists) pass through untouched; unsafe values in hrefs remain
|
|
93
|
+
* covered by the render-time SAFE_HREF/sanitizer checks.
|
|
94
|
+
*/
|
|
96
95
|
export declare function resolveNodesBindings<T extends Record<string, any>>(nodes: T, variables: Record<string, HostVariable>, functions?: HostFunctionLookup): T;
|
|
@@ -138,21 +138,6 @@ export const VARIABLE_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]{0,39}$/;
|
|
|
138
138
|
/** Fast pre-check so binding-free content skips the walk entirely. */ export function hasBindings(text) {
|
|
139
139
|
return /\{\{/.test(text);
|
|
140
140
|
}
|
|
141
|
-
/**
|
|
142
|
-
* Applies `resolveBindings` to every string prop across a normalized nodes
|
|
143
|
-
* map (mutating a shallow copy). String props only — objects/arrays (sx,
|
|
144
|
-
* option lists) pass through untouched; unsafe values in hrefs remain
|
|
145
|
-
* covered by the render-time SAFE_HREF/sanitizer checks.
|
|
146
|
-
*/ /** Component id of the interactive function widget (plugins-mui). */ export const FUNCTION_WIDGET_COMPONENT_ID = 'functionWidget';
|
|
147
|
-
/**
|
|
148
|
-
* Component id of the Calculator container (plugins-mui, AGL-3202): the
|
|
149
|
-
* other element that runs a function in the visitor's browser, and so the
|
|
150
|
-
* other one compose hands a definition to.
|
|
151
|
-
*/ export const FUNCTION_SCOPE_COMPONENT_ID = 'functionScope';
|
|
152
|
-
const FUNCTION_RUNNING_COMPONENT_IDS = new Set([
|
|
153
|
-
FUNCTION_WIDGET_COMPONENT_ID,
|
|
154
|
-
FUNCTION_SCOPE_COMPONENT_ID
|
|
155
|
-
]);
|
|
156
141
|
/**
|
|
157
142
|
* The site variables ONE function reads, by name (AGL-3202).
|
|
158
143
|
*
|
|
@@ -173,21 +158,32 @@ const FUNCTION_RUNNING_COMPONENT_IDS = new Set([
|
|
|
173
158
|
return globals;
|
|
174
159
|
}
|
|
175
160
|
/**
|
|
176
|
-
* Injects each function
|
|
177
|
-
* (AGL-93): the client runs the shared evaluator locally, so the
|
|
178
|
-
* page carries the definition instead of calling home. Unknown
|
|
179
|
-
* the node untouched (the
|
|
161
|
+
* Injects each function-running element's definition into its props at
|
|
162
|
+
* compose time (AGL-93): the client runs the shared evaluator locally, so the
|
|
163
|
+
* published page carries the definition instead of calling home. Unknown
|
|
164
|
+
* names leave the node untouched (the element renders its editor
|
|
165
|
+
* placeholder).
|
|
166
|
+
*
|
|
167
|
+
* Which elements run a function is DECLARED (AGL-3393): `bindings` maps a
|
|
168
|
+
* component id to the prop that names its function, collected from each
|
|
169
|
+
* plugin's `contributes.site.functionBindings`. Core names no element, so a
|
|
170
|
+
* marketplace plugin's element is bound exactly as a first-party one is, and
|
|
171
|
+
* an element nobody declared is never handed a definition.
|
|
180
172
|
*
|
|
181
173
|
* With `variables`, the site variables that function reads ride along as
|
|
182
174
|
* `globals` (AGL-3202) — see {@link functionGlobals} for why it is only
|
|
183
175
|
* those. The key is omitted when the function reads none, so a page built
|
|
184
176
|
* before this existed serializes exactly as it did.
|
|
185
|
-
*/ export function attachFunctionDefinitions(nodes, functions, variables
|
|
186
|
-
if (!Object.keys(functions).length)
|
|
177
|
+
*/ export function attachFunctionDefinitions(nodes, functions, variables, bindings) {
|
|
178
|
+
if (!Object.keys(functions).length || !Object.keys(bindings).length) {
|
|
179
|
+
return nodes;
|
|
180
|
+
}
|
|
187
181
|
const next = {};
|
|
188
182
|
for (const [id, node] of Object.entries(nodes)){
|
|
189
183
|
var _node_props;
|
|
190
|
-
const
|
|
184
|
+
const componentId = node == null ? void 0 : node.componentId;
|
|
185
|
+
const prop = typeof componentId === 'string' && Object.prototype.hasOwnProperty.call(bindings, componentId) ? bindings[componentId] : undefined;
|
|
186
|
+
const name = prop ? node == null ? void 0 : (_node_props = node.props) == null ? void 0 : _node_props[prop] : undefined;
|
|
191
187
|
const definition = name ? functions[String(name).trim()] : undefined;
|
|
192
188
|
if (!definition) {
|
|
193
189
|
next[id] = node;
|
|
@@ -204,7 +200,12 @@ const FUNCTION_RUNNING_COMPONENT_IDS = new Set([
|
|
|
204
200
|
}
|
|
205
201
|
return next;
|
|
206
202
|
}
|
|
207
|
-
|
|
203
|
+
/**
|
|
204
|
+
* Applies `resolveBindings` to every string prop across a normalized nodes
|
|
205
|
+
* map (mutating a shallow copy). String props only — objects/arrays (sx,
|
|
206
|
+
* option lists) pass through untouched; unsafe values in hrefs remain
|
|
207
|
+
* covered by the render-time SAFE_HREF/sanitizer checks.
|
|
208
|
+
*/ export function resolveNodesBindings(nodes, variables, functions = {}) {
|
|
208
209
|
if (!Object.keys(variables).length && !Object.keys(functions).length) {
|
|
209
210
|
return nodes;
|
|
210
211
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/variables.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// Token grammar shared with the picker/besigner (AGL-185/194): id-form\n// tokens only — legacy name resolution retired after the production\n// content migration ran clean.\nimport {\n FUNCTION_TOKEN_PATTERN,\n VARIABLE_ID_TOKEN_PATTERN,\n} from './binding-tokens'\nimport {\n evaluateExpression,\n evaluateHostFunction,\n functionReferencedNames,\n type HostFunction,\n} from './functions'\n\n/**\n * Host variables (Component Builder, AGL-91): typed values authored in the\n * console and bound into component text props with `{{name}}` — and, since\n * AGL-93, function results with `{{fn:name(args)}}`. Pure data module\n * shared by the console editor UI and the tenant compose pipeline.\n */\n\n/** Types from the mockup's Edit Variable dialog (\"Function\" lands with AGL-92). */\nexport type HostVariableType =\n | 'date'\n | 'time'\n | 'text'\n | 'number'\n | 'boolean'\n | 'dictionary'\n | 'collection'\n\nexport const HOST_VARIABLE_TYPE_LABELS: Record<HostVariableType, string> = {\n date: 'Date',\n time: 'Time',\n text: 'Plain text',\n number: 'Number',\n boolean: 'True/false (boolean)',\n dictionary: 'Dictionary (key: value)',\n collection: 'Collection list (values)',\n}\n\n/** `hosts/{hostId}/variables/{id}` doc. */\nexport interface HostVariable {\n /** Binding identifier — `{{name}}`. */\n name: string\n type: HostVariableType\n /** Persisted as a string; formatted per type at resolve time. */\n value?: string\n /**\n * Workflow (by name) whose result becomes this variable's value at\n * compose time (AGL-129); `value` acts as the fallback when the\n * workflow is missing or fails.\n */\n workflowName?: string\n /** Computed source by workflow doc id (AGL-261); wins over the name. */\n workflowId?: string\n}\n\nexport const VARIABLE_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]{0,39}$/\n\n/** Human-readable rendering of a variable's value for text interpolation. */\nexport function formatVariableValue(variable: HostVariable): string {\n const raw = variable.value ?? ''\n switch (variable.type) {\n case 'boolean':\n return raw === 'true' ? 'true' : 'false'\n case 'number': {\n const parsed = Number(raw)\n return Number.isFinite(parsed) ? String(parsed) : ''\n }\n case 'date': {\n const parsed = new Date(raw)\n return Number.isNaN(parsed.getTime())\n ? raw\n : parsed.toLocaleDateString(undefined, {\n year: 'numeric',\n month: 'long',\n day: 'numeric',\n timeZone: 'UTC',\n })\n }\n case 'dictionary':\n case 'collection': {\n // Stored as JSON; render collections comma-joined and dictionaries\n // as `key: value` pairs so a bare {{name}} stays readable.\n try {\n const parsed = JSON.parse(raw)\n if (Array.isArray(parsed)) return parsed.map(String).join(', ')\n if (parsed && typeof parsed === 'object') {\n return Object.entries(parsed)\n .map(([key, value]) => `${key}: ${value}`)\n .join(', ')\n }\n } catch {\n // fall through to the raw string\n }\n return raw\n }\n case 'time':\n case 'text':\n default:\n return raw\n }\n}\n\n/** A function definition lookup keyed by function name (AGL-93). */\nexport type HostFunctionLookup = Record<string, HostFunction>\n\n/**\n * Variable values as an expression scope (numbers stay numeric). Keyed by\n * each doc's NAME regardless of how the lookup map is keyed (AGL-194:\n * maps are id-keyed now, but expressions reference variables by name).\n */\nfunction variableScope(\n variables: Record<string, HostVariable>,\n): Record<string, number | string | boolean> {\n const scope: Record<string, number | string | boolean> = {}\n for (const variable of Object.values(variables)) {\n if (!variable?.name) continue\n if (variable.type === 'number') {\n scope[variable.name] = Number(variable.value ?? 0)\n } else if (variable.type === 'boolean') {\n scope[variable.name] = variable.value === 'true'\n } else {\n scope[variable.name] = variable.value ?? ''\n }\n }\n return scope\n}\n\n/**\n * Replaces binding tokens with values (AGL-91/93/185/194): id forms\n * `{{var:id}}` / `{{fn:id(args)}}` resolve through id-keyed lookup maps.\n * Function args are expressions over literals and variable NAMES (the\n * scope keys by each doc's name). Unknown function refs keep the literal\n * token; unknown variable ids render empty; bare `{{name}}` strings are\n * plain text since the legacy fallback retired.\n */\nexport function resolveBindings(\n text: string,\n variables: Record<string, HostVariable>,\n functions: HostFunctionLookup = {},\n): string {\n const withVariableIds = text.replace(\n VARIABLE_ID_TOKEN_PATTERN,\n (_token, id) => {\n const variable = variables[String(id)]\n // Unknown ids render EMPTY, not literal (AGL-186): a raw doc id is\n // meaningless to visitors, unlike a readable legacy {{name}} token.\n return variable ? formatVariableValue(variable) : ''\n },\n )\n const withFunctions = withVariableIds.replace(\n FUNCTION_TOKEN_PATTERN,\n (token, rawName, rawArgs) => {\n const definition = functions[String(rawName).trim()]\n if (!definition) return token\n try {\n const scope = variableScope(variables)\n const argValues = String(rawArgs)\n .split(',')\n .map((argument) => argument.trim())\n .filter((argument) => argument.length > 0)\n .map((argument) => evaluateExpression(argument, scope))\n const args: Record<string, unknown> = {}\n definition.parameters?.forEach((parameter, index) => {\n args[parameter.name] = argValues[index]\n })\n // The site's variables are the function's outer scope (AGL-3202),\n // so `{{fn:id(25)}}` can price 25 sites without every rate being\n // passed in by position. Arguments still win over a variable of the\n // same name, as every parameter does.\n const result = evaluateHostFunction(definition, args, {\n globals: scope,\n })\n return result.ok ? String(result.value) : token\n } catch {\n return token\n }\n },\n )\n // Legacy {{name}} resolution retired (AGL-194): the production content\n // migration ran clean, so bare-name tokens are plain text now. Typing\n // them in the editor still works — save-time normalization converts\n // known names to id tokens before they persist.\n return withFunctions\n}\n\n/** Fast pre-check so binding-free content skips the walk entirely. */\nexport function hasBindings(text: string): boolean {\n return /\\{\\{/.test(text)\n}\n\n/**\n * Applies `resolveBindings` to every string prop across a normalized nodes\n * map (mutating a shallow copy). String props only — objects/arrays (sx,\n * option lists) pass through untouched; unsafe values in hrefs remain\n * covered by the render-time SAFE_HREF/sanitizer checks.\n */\n/** Component id of the interactive function widget (plugins-mui). */\nexport const FUNCTION_WIDGET_COMPONENT_ID = 'functionWidget'\n\n/**\n * Component id of the Calculator container (plugins-mui, AGL-3202): the\n * other element that runs a function in the visitor's browser, and so the\n * other one compose hands a definition to.\n */\nexport const FUNCTION_SCOPE_COMPONENT_ID = 'functionScope'\n\nconst FUNCTION_RUNNING_COMPONENT_IDS: ReadonlySet<string> = new Set([\n FUNCTION_WIDGET_COMPONENT_ID,\n FUNCTION_SCOPE_COMPONENT_ID,\n])\n\n/**\n * The site variables ONE function reads, by name (AGL-3202).\n *\n * A widget runs in the visitor's browser, so whatever it may read has to be\n * on the page. Handing it the whole variable list would publish every value\n * a site holds to every page that places a calculator; this hands over the\n * names the function's own expressions use and nothing else. A name the\n * function declares itself is not a variable read — its parameter or local\n * shadows the variable — so it is not shipped either.\n */\nexport function functionGlobals(\n definition: HostFunction,\n variables: Record<string, HostVariable>,\n): Record<string, number | string | boolean> {\n const scope = variableScope(variables)\n const globals: Record<string, number | string | boolean> = {}\n for (const name of functionReferencedNames(definition)) {\n if (Object.prototype.hasOwnProperty.call(scope, name)) {\n globals[name] = scope[name]\n }\n }\n return globals\n}\n\n/**\n * Injects each function widget's definition into its props at compose time\n * (AGL-93): the client runs the shared evaluator locally, so the published\n * page carries the definition instead of calling home. Unknown names leave\n * the node untouched (the widget renders its editor placeholder).\n *\n * With `variables`, the site variables that function reads ride along as\n * `globals` (AGL-3202) — see {@link functionGlobals} for why it is only\n * those. The key is omitted when the function reads none, so a page built\n * before this existed serializes exactly as it did.\n */\nexport function attachFunctionDefinitions<T extends Record<string, any>>(\n nodes: T,\n functions: HostFunctionLookup,\n variables: Record<string, HostVariable> = {},\n): T {\n if (!Object.keys(functions).length) return nodes\n const next: Record<string, any> = {}\n for (const [id, node] of Object.entries(nodes)) {\n const name = FUNCTION_RUNNING_COMPONENT_IDS.has(node?.componentId)\n ? node?.props?.functionName\n : undefined\n const definition = name ? functions[String(name).trim()] : undefined\n if (!definition) {\n next[id] = node\n continue\n }\n const globals = functionGlobals(definition, variables)\n next[id] = {\n ...node,\n props: {\n ...node.props,\n definition,\n ...(Object.keys(globals).length ? { globals } : {}),\n },\n }\n }\n return next as T\n}\n\nexport function resolveNodesBindings<T extends Record<string, any>>(\n nodes: T,\n variables: Record<string, HostVariable>,\n functions: HostFunctionLookup = {},\n): T {\n if (!Object.keys(variables).length && !Object.keys(functions).length) {\n return nodes\n }\n const next: Record<string, any> = {}\n for (const [id, node] of Object.entries(nodes)) {\n const props = node?.props\n if (!props) {\n next[id] = node\n continue\n }\n let changed = false\n const nextProps: Record<string, unknown> = { ...props }\n for (const [key, value] of Object.entries(props)) {\n if (typeof value === 'string' && hasBindings(value)) {\n nextProps[key] = resolveBindings(value, variables, functions)\n changed = true\n }\n }\n next[id] = changed ? { ...node, props: nextProps } : node\n }\n return next as T\n}\n"],"names":["FUNCTION_TOKEN_PATTERN","VARIABLE_ID_TOKEN_PATTERN","evaluateExpression","evaluateHostFunction","functionReferencedNames","HOST_VARIABLE_TYPE_LABELS","date","time","text","number","boolean","dictionary","collection","VARIABLE_NAME_PATTERN","formatVariableValue","variable","raw","value","type","parsed","Number","isFinite","String","Date","isNaN","getTime","toLocaleDateString","undefined","year","month","day","timeZone","JSON","parse","Array","isArray","map","join","Object","entries","key","variableScope","variables","scope","values","name","resolveBindings","functions","withVariableIds","replace","_token","id","withFunctions","token","rawName","rawArgs","definition","trim","argValues","split","argument","filter","length","args","parameters","forEach","parameter","index","result","globals","ok","hasBindings","test","FUNCTION_WIDGET_COMPONENT_ID","FUNCTION_SCOPE_COMPONENT_ID","FUNCTION_RUNNING_COMPONENT_IDS","Set","functionGlobals","prototype","hasOwnProperty","call","attachFunctionDefinitions","nodes","keys","next","node","has","componentId","props","functionName","resolveNodesBindings","changed","nextProps"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/variables.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// Token grammar shared with the picker/besigner (AGL-185/194): id-form\n// tokens only — legacy name resolution retired after the production\n// content migration ran clean.\nimport {\n FUNCTION_TOKEN_PATTERN,\n VARIABLE_ID_TOKEN_PATTERN,\n} from './binding-tokens'\nimport {\n evaluateExpression,\n evaluateHostFunction,\n functionReferencedNames,\n type HostFunction,\n} from './functions'\n\n/**\n * Host variables (Component Builder, AGL-91): typed values authored in the\n * console and bound into component text props with `{{name}}` — and, since\n * AGL-93, function results with `{{fn:name(args)}}`. Pure data module\n * shared by the console editor UI and the tenant compose pipeline.\n */\n\n/** Types from the mockup's Edit Variable dialog (\"Function\" lands with AGL-92). */\nexport type HostVariableType =\n | 'date'\n | 'time'\n | 'text'\n | 'number'\n | 'boolean'\n | 'dictionary'\n | 'collection'\n\nexport const HOST_VARIABLE_TYPE_LABELS: Record<HostVariableType, string> = {\n date: 'Date',\n time: 'Time',\n text: 'Plain text',\n number: 'Number',\n boolean: 'True/false (boolean)',\n dictionary: 'Dictionary (key: value)',\n collection: 'Collection list (values)',\n}\n\n/** `hosts/{hostId}/variables/{id}` doc. */\nexport interface HostVariable {\n /** Binding identifier — `{{name}}`. */\n name: string\n type: HostVariableType\n /** Persisted as a string; formatted per type at resolve time. */\n value?: string\n /**\n * Workflow (by name) whose result becomes this variable's value at\n * compose time (AGL-129); `value` acts as the fallback when the\n * workflow is missing or fails.\n */\n workflowName?: string\n /** Computed source by workflow doc id (AGL-261); wins over the name. */\n workflowId?: string\n}\n\nexport const VARIABLE_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]{0,39}$/\n\n/** Human-readable rendering of a variable's value for text interpolation. */\nexport function formatVariableValue(variable: HostVariable): string {\n const raw = variable.value ?? ''\n switch (variable.type) {\n case 'boolean':\n return raw === 'true' ? 'true' : 'false'\n case 'number': {\n const parsed = Number(raw)\n return Number.isFinite(parsed) ? String(parsed) : ''\n }\n case 'date': {\n const parsed = new Date(raw)\n return Number.isNaN(parsed.getTime())\n ? raw\n : parsed.toLocaleDateString(undefined, {\n year: 'numeric',\n month: 'long',\n day: 'numeric',\n timeZone: 'UTC',\n })\n }\n case 'dictionary':\n case 'collection': {\n // Stored as JSON; render collections comma-joined and dictionaries\n // as `key: value` pairs so a bare {{name}} stays readable.\n try {\n const parsed = JSON.parse(raw)\n if (Array.isArray(parsed)) return parsed.map(String).join(', ')\n if (parsed && typeof parsed === 'object') {\n return Object.entries(parsed)\n .map(([key, value]) => `${key}: ${value}`)\n .join(', ')\n }\n } catch {\n // fall through to the raw string\n }\n return raw\n }\n case 'time':\n case 'text':\n default:\n return raw\n }\n}\n\n/** A function definition lookup keyed by function name (AGL-93). */\nexport type HostFunctionLookup = Record<string, HostFunction>\n\n/**\n * Variable values as an expression scope (numbers stay numeric). Keyed by\n * each doc's NAME regardless of how the lookup map is keyed (AGL-194:\n * maps are id-keyed now, but expressions reference variables by name).\n */\nfunction variableScope(\n variables: Record<string, HostVariable>,\n): Record<string, number | string | boolean> {\n const scope: Record<string, number | string | boolean> = {}\n for (const variable of Object.values(variables)) {\n if (!variable?.name) continue\n if (variable.type === 'number') {\n scope[variable.name] = Number(variable.value ?? 0)\n } else if (variable.type === 'boolean') {\n scope[variable.name] = variable.value === 'true'\n } else {\n scope[variable.name] = variable.value ?? ''\n }\n }\n return scope\n}\n\n/**\n * Replaces binding tokens with values (AGL-91/93/185/194): id forms\n * `{{var:id}}` / `{{fn:id(args)}}` resolve through id-keyed lookup maps.\n * Function args are expressions over literals and variable NAMES (the\n * scope keys by each doc's name). Unknown function refs keep the literal\n * token; unknown variable ids render empty; bare `{{name}}` strings are\n * plain text since the legacy fallback retired.\n */\nexport function resolveBindings(\n text: string,\n variables: Record<string, HostVariable>,\n functions: HostFunctionLookup = {},\n): string {\n const withVariableIds = text.replace(\n VARIABLE_ID_TOKEN_PATTERN,\n (_token, id) => {\n const variable = variables[String(id)]\n // Unknown ids render EMPTY, not literal (AGL-186): a raw doc id is\n // meaningless to visitors, unlike a readable legacy {{name}} token.\n return variable ? formatVariableValue(variable) : ''\n },\n )\n const withFunctions = withVariableIds.replace(\n FUNCTION_TOKEN_PATTERN,\n (token, rawName, rawArgs) => {\n const definition = functions[String(rawName).trim()]\n if (!definition) return token\n try {\n const scope = variableScope(variables)\n const argValues = String(rawArgs)\n .split(',')\n .map((argument) => argument.trim())\n .filter((argument) => argument.length > 0)\n .map((argument) => evaluateExpression(argument, scope))\n const args: Record<string, unknown> = {}\n definition.parameters?.forEach((parameter, index) => {\n args[parameter.name] = argValues[index]\n })\n // The site's variables are the function's outer scope (AGL-3202),\n // so `{{fn:id(25)}}` can price 25 sites without every rate being\n // passed in by position. Arguments still win over a variable of the\n // same name, as every parameter does.\n const result = evaluateHostFunction(definition, args, {\n globals: scope,\n })\n return result.ok ? String(result.value) : token\n } catch {\n return token\n }\n },\n )\n // Legacy {{name}} resolution retired (AGL-194): the production content\n // migration ran clean, so bare-name tokens are plain text now. Typing\n // them in the editor still works — save-time normalization converts\n // known names to id tokens before they persist.\n return withFunctions\n}\n\n/** Fast pre-check so binding-free content skips the walk entirely. */\nexport function hasBindings(text: string): boolean {\n return /\\{\\{/.test(text)\n}\n\n/**\n * The site variables ONE function reads, by name (AGL-3202).\n *\n * A widget runs in the visitor's browser, so whatever it may read has to be\n * on the page. Handing it the whole variable list would publish every value\n * a site holds to every page that places a calculator; this hands over the\n * names the function's own expressions use and nothing else. A name the\n * function declares itself is not a variable read — its parameter or local\n * shadows the variable — so it is not shipped either.\n */\nexport function functionGlobals(\n definition: HostFunction,\n variables: Record<string, HostVariable>,\n): Record<string, number | string | boolean> {\n const scope = variableScope(variables)\n const globals: Record<string, number | string | boolean> = {}\n for (const name of functionReferencedNames(definition)) {\n if (Object.prototype.hasOwnProperty.call(scope, name)) {\n globals[name] = scope[name]\n }\n }\n return globals\n}\n\n/**\n * Injects each function-running element's definition into its props at\n * compose time (AGL-93): the client runs the shared evaluator locally, so the\n * published page carries the definition instead of calling home. Unknown\n * names leave the node untouched (the element renders its editor\n * placeholder).\n *\n * Which elements run a function is DECLARED (AGL-3393): `bindings` maps a\n * component id to the prop that names its function, collected from each\n * plugin's `contributes.site.functionBindings`. Core names no element, so a\n * marketplace plugin's element is bound exactly as a first-party one is, and\n * an element nobody declared is never handed a definition.\n *\n * With `variables`, the site variables that function reads ride along as\n * `globals` (AGL-3202) — see {@link functionGlobals} for why it is only\n * those. The key is omitted when the function reads none, so a page built\n * before this existed serializes exactly as it did.\n */\nexport function attachFunctionDefinitions<T extends Record<string, any>>(\n nodes: T,\n functions: HostFunctionLookup,\n variables: Record<string, HostVariable>,\n bindings: Readonly<Record<string, string>>,\n): T {\n if (!Object.keys(functions).length || !Object.keys(bindings).length) {\n return nodes\n }\n const next: Record<string, any> = {}\n for (const [id, node] of Object.entries(nodes)) {\n const componentId = node?.componentId\n const prop =\n typeof componentId === 'string' &&\n Object.prototype.hasOwnProperty.call(bindings, componentId)\n ? bindings[componentId]\n : undefined\n const name = prop ? node?.props?.[prop] : undefined\n const definition = name ? functions[String(name).trim()] : undefined\n if (!definition) {\n next[id] = node\n continue\n }\n const globals = functionGlobals(definition, variables)\n next[id] = {\n ...node,\n props: {\n ...node.props,\n definition,\n ...(Object.keys(globals).length ? { globals } : {}),\n },\n }\n }\n return next as T\n}\n\n/**\n * Applies `resolveBindings` to every string prop across a normalized nodes\n * map (mutating a shallow copy). String props only — objects/arrays (sx,\n * option lists) pass through untouched; unsafe values in hrefs remain\n * covered by the render-time SAFE_HREF/sanitizer checks.\n */\nexport function resolveNodesBindings<T extends Record<string, any>>(\n nodes: T,\n variables: Record<string, HostVariable>,\n functions: HostFunctionLookup = {},\n): T {\n if (!Object.keys(variables).length && !Object.keys(functions).length) {\n return nodes\n }\n const next: Record<string, any> = {}\n for (const [id, node] of Object.entries(nodes)) {\n const props = node?.props\n if (!props) {\n next[id] = node\n continue\n }\n let changed = false\n const nextProps: Record<string, unknown> = { ...props }\n for (const [key, value] of Object.entries(props)) {\n if (typeof value === 'string' && hasBindings(value)) {\n nextProps[key] = resolveBindings(value, variables, functions)\n changed = true\n }\n }\n next[id] = changed ? { ...node, props: nextProps } : node\n }\n return next as T\n}\n"],"names":["FUNCTION_TOKEN_PATTERN","VARIABLE_ID_TOKEN_PATTERN","evaluateExpression","evaluateHostFunction","functionReferencedNames","HOST_VARIABLE_TYPE_LABELS","date","time","text","number","boolean","dictionary","collection","VARIABLE_NAME_PATTERN","formatVariableValue","variable","raw","value","type","parsed","Number","isFinite","String","Date","isNaN","getTime","toLocaleDateString","undefined","year","month","day","timeZone","JSON","parse","Array","isArray","map","join","Object","entries","key","variableScope","variables","scope","values","name","resolveBindings","functions","withVariableIds","replace","_token","id","withFunctions","token","rawName","rawArgs","definition","trim","argValues","split","argument","filter","length","args","parameters","forEach","parameter","index","result","globals","ok","hasBindings","test","functionGlobals","prototype","hasOwnProperty","call","attachFunctionDefinitions","nodes","bindings","keys","next","node","componentId","prop","props","resolveNodesBindings","changed","nextProps"],"mappings":"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"}
|
|
@@ -132,7 +132,9 @@ export const CONNECT_SCOPE_METADATA_VALUE = 'connect';
|
|
|
132
132
|
* meeting the failure at payment time. Without this event the handler cannot
|
|
133
133
|
* run — which is exactly the state AGL-2122 found and fixed.
|
|
134
134
|
*/ export const REQUIRED_CONNECT_WEBHOOK_EVENTS = [
|
|
135
|
-
'account.updated'
|
|
135
|
+
'account.updated',
|
|
136
|
+
'payout.failed',
|
|
137
|
+
'payout.paid'
|
|
136
138
|
];
|
|
137
139
|
/**
|
|
138
140
|
* Does this Stripe endpoint object carry our Connect scope marker?
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/webhook-delivery.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 * DID THE WEBHOOK ACTUALLY DO ANYTHING? (AGL-1954)\n *\n * Every signal we have about the billing webhook is a signal about the\n * REQUEST. Stripe records `delivery_success` off the status code. The\n * idempotency claim in `stripeEvents` records that the signature verified and\n * the handler was entered. `/api/health/billing` (AGL-1924) counts what\n * Stripe attempted and failed to deliver. All three read green for the one\n * failure this module exists to catch: **a handler that answers 200 and moves\n * nothing.**\n *\n * That is not hypothetical here. AGL-1798 is exactly it — `charge.refunded`\n * was never subscribed on the live destination, so AGL-1546's entitlement\n * revocation had no trigger, and every other indicator looked fine for as\n * long as it lasted. AGL-1551 is the same class from the other side: a week\n * of 100% rejected deliveries behind a green \"Active\" badge.\n *\n * ## The honest signal is an EFFECT, not a return code\n *\n * A check that asserts \"the handler ran\" is nearly as blind as the 200 —\n * the handler DID run in the AGL-1798 shape, it just had no work registered\n * for the event. So the route reports what it COMMITTED: an org's plan\n * mirrored, a `platformRevenue` row written, a refund stamped, an orphan\n * recorded, an audit row appended, a plugin claiming the event. Those are the\n * things whose absence is the bug.\n *\n * ## Three outcomes, and conflating the middle two is its own failure\n *\n * A binary \"did it write something\" would fire on every legitimately\n * irrelevant delivery — a tenant shopper's subscription carries no\n * `metadata.orgId` and correctly moves nothing on OUR side; a marketplace\n * refund resolves to no workspace customer and is correctly left to the\n * plugins; a `won` dispute nobody claimed reversed no money and correctly\n * wakes nobody. Alerting on those is alert fatigue, which ends with the alarm\n * muted and the real one lost inside it.\n *\n * So a branch that decides an event is not its business must SAY SO, by\n * name, through {@link WebhookEffectLedger.skip}. That is what separates:\n *\n * | outcome | meaning | alarm |\n * |-----------|----------------------------------------------------|-------|\n * | `acted` | something durable committed, or a plugin claimed it | no |\n * | `ignored` | a branch named a reason, or we never asked for this event type | no |\n * | `inert` | an event we deliberately subscribe to produced NEITHER | YES |\n *\n * `inert` is the whole point. Falling off the end of the dispatch with no\n * effect and no stated reason is precisely what a deleted write, an\n * unregistered plugin handler, or a renamed event type looks like from\n * inside the process — and it is the only one of the three that nothing else\n * in this system can see.\n *\n * ## Why the \"did we ask for it\" test is the required-events list\n *\n * Stripe delivers what the destination is subscribed to, and the destination\n * is created by `tools/scripts/setup-stripe.mjs` from `WEBHOOK_EVENTS`. An\n * event type outside that list reaching us is either a hand-added\n * subscription or a Connect delivery arriving at the platform destination —\n * unremarkable, and never our silent-drop bug, because we never claimed to\n * handle it. An event type INSIDE it is one we went out of our way to ask\n * Stripe for, so producing nothing from it is a contradiction by\n * construction.\n *\n * `REQUIRED_WEBHOOK_EVENTS` below is a copy of that list, and a copy is a\n * drift hazard — so `webhook-delivery.spec.ts` reads the `.mjs` and fails on\n * any divergence, in either direction. The alternative (moving the list\n * across the nx boundary out of `tools/`) is a wider change than this\n * warrants and is tracked on AGL-1948.\n *\n * Pure: no clock, no I/O, no Firestore. The route records, this decides, the\n * spec exercises every branch without a network.\n */\n\n/** What a single delivery turned out to be. */\nexport type WebhookDeliveryOutcome = 'acted' | 'ignored' | 'inert'\n\n/**\n * The event types the platform destination is subscribed to, mirroring\n * `WEBHOOK_EVENTS` in `tools/scripts/lib/stripe-webhook-health.mjs`.\n *\n * The list is the definition of \"we asked for this\", which is what makes a\n * no-op delivery of one of them a defect rather than a non-event. Kept in\n * the same order as the source so a diff between the two reads cleanly.\n */\nexport const REQUIRED_WEBHOOK_EVENTS: readonly string[] = [\n 'customer.subscription.created',\n 'customer.subscription.updated',\n 'customer.subscription.deleted',\n 'checkout.session.completed',\n 'invoice.finalized',\n 'invoice.paid',\n 'invoice.payment_failed',\n 'charge.refunded',\n 'charge.dispute.created',\n 'charge.dispute.closed',\n // AI overage charged as it accrues (AGL-3011), in the source list's order.\n 'invoice.voided',\n 'invoice.marked_uncollectible',\n 'customer.updated',\n 'payment_method.attached',\n 'payment_method.detached',\n // Fraud signals to staff (AGL-3356).\n 'radar.early_fraud_warning.created',\n 'review.opened',\n]\n\n/**\n * How the CONNECT destination identifies itself (AGL-1948), mirroring\n * `CONNECT_SCOPE_METADATA_KEY` / `CONNECT_SCOPE_METADATA_VALUE` in\n * `tools/scripts/lib/stripe-webhook-health.mjs`.\n *\n * Connected-account events are delivered only to a destination created with\n * `connect: true`, and Stripe's API does not report that flag back on the\n * endpoint object — so the destination is identified by metadata we set at\n * creation instead. Same copy-and-guard arrangement as the list above.\n */\nexport const CONNECT_SCOPE_METADATA_KEY = 'aglyn_scope'\nexport const CONNECT_SCOPE_METADATA_VALUE = 'connect'\n\n/**\n * What the Connect destination must carry, mirroring\n * `CONNECT_WEBHOOK_EVENTS` in the same script lib.\n *\n * `account.updated` is the whole point of it: AGL-1997's `syncConnectAccountStatus`\n * is what stops a merchant whose Stripe account was later restricted from\n * going on selling against a stale `stripeChargesEnabled`, with the SHOPPER\n * meeting the failure at payment time. Without this event the handler cannot\n * run — which is exactly the state AGL-2122 found and fixed.\n */\nexport const REQUIRED_CONNECT_WEBHOOK_EVENTS: readonly string[] = ['account.updated']\n\n/**\n * Does this Stripe endpoint object carry our Connect scope marker?\n *\n * Coverage for it reuses `unsubscribedRequiredEvents` with\n * `REQUIRED_CONNECT_WEBHOOK_EVENTS` as the `required` argument — the wildcard\n * and null handling are identical questions, and a second copy of that logic\n * is a second place for it to drift.\n */\nexport function isConnectWebhookEndpoint(endpoint: unknown): boolean {\n const metadata = (endpoint as { metadata?: Record<string, unknown> })?.metadata\n return metadata?.[CONNECT_SCOPE_METADATA_KEY] === CONNECT_SCOPE_METADATA_VALUE\n}\n\n/**\n * A per-delivery record of what actually committed.\n *\n * Deliberately append-only and stringly-typed: the reasons are read by a\n * human in a log line and by a spec, never switched on. Keeping it dumb is\n * what stops it growing into a second dispatch table that can disagree with\n * the real one.\n */\nexport interface WebhookEffectLedger {\n /** Durable things this delivery committed, in the order they landed. */\n readonly effects: readonly string[]\n /** Reasons a branch decided the event was none of its business. */\n readonly skips: readonly string[]\n /**\n * Record a committed effect. Call this AFTER the write it names, so a\n * throw between the two cannot claim an effect that never landed.\n */\n effect(name: string): void\n /**\n * Record a deliberate no-op and why. This is the difference between \"not\n * ours\" and \"broken\", and it is the half that prevents alert fatigue.\n */\n skip(reason: string): void\n}\n\nexport function createWebhookEffectLedger(): WebhookEffectLedger {\n const effects: string[] = []\n const skips: string[] = []\n return {\n effects,\n skips,\n effect(name: string) {\n effects.push(name)\n },\n skip(reason: string) {\n skips.push(reason)\n },\n }\n}\n\n/** Firestore methods that MUTATE. Everything else is a read or a builder. */\nconst WRITE_METHODS = new Set(['set', 'update', 'create', 'delete', 'add'])\n\n/**\n * Wrap a Firestore handle so every write it commits lands in the ledger.\n *\n * ## Why this exists rather than a `ledger.effect()` beside each write\n *\n * A hand-placed note next to a write is a check that CANNOT FAIL in the one\n * way that matters. Delete the write and leave the note, and the ledger\n * cheerfully reports an effect that never happened — which is the same\n * \"asserts its own literals\" defect the health probes are written to avoid.\n * The note has to be caused by the write, not merely adjacent to it.\n *\n * So the effect is recorded from INSIDE the call, after it resolves. A write\n * that throws records nothing (it did not commit); a write that is deleted\n * from the source records nothing (it is not called). There is no edit to\n * the handler that removes the work and keeps the signal.\n *\n * ## What it deliberately does not see\n *\n * Writes issued through a DIFFERENT Firestore handle — the shared\n * `tenant-data-admin` writers (`writeOrgBilling`, `notifyOrgAdmins`,\n * `notifyStaff`), which each open their own — and anything a plugin does\n * inside its own handler. Those are noted explicitly by the caller where\n * they are the only consequence of a branch, and the comments at those call\n * sites say so. The boundary is stated rather than hidden: this covers the\n * route's own writes, which is where the route's own bugs live.\n *\n * A Proxy rather than a hand-written facade because the wrapped object is\n * passed on to `updateExisting` and friends, which call methods this module\n * has never heard of. A facade would have to enumerate them and would\n * silently drop the ones it forgot.\n */\nexport function observeWrites<T extends object>(\n handle: T,\n ledger: WebhookEffectLedger,\n label = 'firestore',\n): T {\n return new Proxy(handle, {\n get(target, property, receiver) {\n const value = Reflect.get(target, property, receiver)\n if (typeof value !== 'function' || typeof property !== 'string') {\n return value\n }\n const method = property\n return (...args: unknown[]) => {\n const result = (value as (...a: unknown[]) => unknown).apply(\n target,\n args,\n )\n // `collection('orgs')` names the thing being written, so the ledger\n // reads `orgs.update` rather than `firestore.update`. Everything\n // else inherits the label it was reached through.\n const nextLabel =\n method === 'collection' && typeof args[0] === 'string'\n ? (args[0] as string)\n : label\n if (WRITE_METHODS.has(method)) {\n // Recorded on RESOLUTION. A rejected write committed nothing and\n // must not read as an effect.\n return Promise.resolve(result).then((settled) => {\n ledger.effect(`${nextLabel}.${method}`)\n return settled\n })\n }\n // Builders (`collection`, `doc`, `where`, `limit`) return objects\n // that can themselves be written through, so the wrapper follows\n // them. Thenables are returned bare: wrapping a promise would make\n // `then` look like a builder and recurse forever.\n if (\n result &&\n typeof result === 'object' &&\n typeof (result as { then?: unknown }).then !== 'function'\n ) {\n return observeWrites(result as object, ledger, nextLabel)\n }\n return result\n }\n },\n })\n}\n\nexport interface WebhookDeliveryVerdict {\n outcome: WebhookDeliveryOutcome\n /** One short phrase naming WHY, for the log line and the spec. */\n reason: string\n}\n\n/**\n * Classify one delivery.\n *\n * Order is the argument:\n *\n * 1. **Any effect, or any plugin claim, wins.** A delivery that both wrote\n * something and skipped something else did work; a partially-skipped\n * handler is not an idle one.\n * 2. **A named skip is an answer.** Reaching a branch that consciously\n * decided \"not ours\" is a handled event, not a dropped one.\n * 3. **An event type we never subscribed to is not our problem.** Nothing\n * promised to handle it.\n * 4. **Everything else is inert** — and by elimination that means: we asked\n * Stripe for this event, we received it, no handler claimed it, no branch\n * wrote anything, and no branch could say why.\n */\nexport function classifyWebhookDelivery(input: {\n type: string\n effects: readonly string[]\n skips: readonly string[]\n claimed: boolean\n /** Overridable so the spec can prove the rule rather than the list. */\n required?: readonly string[]\n}): WebhookDeliveryVerdict {\n const { type, effects, skips, claimed } = input\n const required = input.required ?? REQUIRED_WEBHOOK_EVENTS\n if (effects.length > 0) return { outcome: 'acted', reason: effects[0] }\n if (claimed) return { outcome: 'acted', reason: 'plugin-claimed' }\n if (skips.length > 0) return { outcome: 'ignored', reason: skips[0] }\n if (!required.includes(type)) {\n return { outcome: 'ignored', reason: 'not-subscribed' }\n }\n return { outcome: 'inert', reason: 'no-effect' }\n}\n\n/**\n * How long after Stripe CREATES an event its first delivery attempt may\n * plausibly take to reach us, in seconds. Mirrors `RETRY_LAG_SECONDS` in\n * `tools/scripts/lib/stripe-webhook-health.mjs`, under the same\n * read-the-`.mjs`-as-text drift guard as the lists above.\n *\n * Calibrated against the live account on 2026-08-18: the five deliveries that\n * succeeded first time landed in 1.0–3.7s; the one that did not —\n * `evt_1U49XtDYHP4psn7hA9VHPnZz`, whose three 400s ARE the three failures the\n * Stripe Dashboard was showing — landed 16,665s (4h 37m) late. Two minutes\n * sits three orders of magnitude clear of the healthy band and still inside\n * Stripe's first automatic retry interval, so it separates the two without\n * straddling either.\n */\nexport const RETRY_LAG_SECONDS = 120\n\n/**\n * What a delivery's lag says about the attempts BEFORE it.\n *\n * `unknown` is a first-class outcome, not a synonym for either answer. See\n * {@link classifyDeliveryLag} for why that matters more here than usual.\n */\nexport type WebhookDeliveryAttempt = 'first-attempt' | 'retried' | 'unknown'\n\n/** Why {@link classifyDeliveryLag} answered as it did. */\nexport type WebhookDeliveryLagReason =\n | 'measured'\n | 'created-absent'\n | 'created-not-a-number'\n | 'created-non-positive'\n | 'received-unusable'\n | 'clock-skew'\n\nexport interface WebhookDeliveryLagVerdict {\n attempt: WebhookDeliveryAttempt\n /** Seconds between `event.created` and our claim. Null unless `measured`. */\n lagSeconds: number | null\n reason: WebhookDeliveryLagReason\n}\n\n/**\n * DID THIS DELIVERY ONLY LAND ON A RETRY? (AGL-2039, the last arm of AGL-1948)\n *\n * ## The reconciliation this exists to close\n *\n * `/api/health/billing` scores EVENTS and `GET /v1/events?delivery_success=false`\n * is a TERMINAL-state filter: it selects events that are still pending or\n * failed EVERY attempt. An event that 400s three times and then succeeds on\n * the fourth reads back `delivery_success: true` and is, to that filter,\n * indistinguishable from one that succeeded immediately. The Stripe Dashboard\n * scores delivery ATTEMPTS, so the same hour reads 0% here and 30% there —\n * and both numbers are right. The three real failures were visible only in\n * the second reading, and each was a mirror that did not run when it should\n * have.\n *\n * The recoverable half of the attempt history is in our own data. The route\n * claims `stripeEvents/{id}` AFTER signature verification and DELETES the\n * claim when a handler throws, so the stamp always records the attempt that\n * actually got through. The distance from `event.created` to that stamp is\n * the delivery lag, and a lag of minutes means an earlier attempt failed.\n *\n * ## Why this is decided at WRITE time, not at read time\n *\n * The obvious implementation reads `receivedAt` off every claim document in\n * the window and subtracts. That turns a `.count()` aggregation — one\n * integer, zero documents read — into a document scan on a PUBLIC,\n * unauthenticated health endpoint polled every five minutes. Deciding here,\n * once, at the moment of the claim, lets the probe stay an aggregation over a\n * marker field, exactly like AGL-1954's `inertAtMs`.\n *\n * ## Absent, zero and unknown are THREE different things\n *\n * `strictNullChecks` is off repo-wide, so an absent `created` folds to falsy\n * and a naive `Number(event?.created ?? 0)` yields 0 — which subtracts to a\n * lag of ~1.7 BILLION seconds and reports every such delivery as a retry,\n * forever, on a perfectly healthy webhook. A monitoring signal that reds on\n * its own missing input is worse than no signal, because it is the thing that\n * gets muted. So each of those inputs is rejected by name:\n *\n * | input | attempt | reason |\n * |-----------------------------|------------|-------------------------|\n * | field absent / null | `unknown` | `created-absent` |\n * | a string, NaN, Infinity | `unknown` | `created-not-a-number` |\n * | `0` or negative | `unknown` | `created-non-positive` |\n * | our own stamp unusable | `unknown` | `received-unusable` |\n * | created in the FUTURE by more than the bar | `unknown` | `clock-skew` |\n *\n * A lag between `-threshold` and `+threshold` is `first-attempt`: sub-second\n * clock skew between Stripe's clock and ours is ordinary and is not evidence\n * of anything.\n *\n * Pure: no clock, no I/O. The caller supplies both timestamps.\n */\nexport function classifyDeliveryLag(input: {\n /** `event.created`, in unix SECONDS, exactly as Stripe sent it. */\n eventCreatedSeconds: unknown\n /** When we claimed the event, in unix MILLISECONDS. */\n receivedAtMs: unknown\n /** Overridable so the spec can prove the rule rather than the constant. */\n thresholdSeconds?: number\n}): WebhookDeliveryLagVerdict {\n const threshold = input.thresholdSeconds ?? RETRY_LAG_SECONDS\n const created = input.eventCreatedSeconds\n if (created === undefined || created === null) {\n return { attempt: 'unknown', lagSeconds: null, reason: 'created-absent' }\n }\n if (typeof created !== 'number' || !Number.isFinite(created)) {\n return {\n attempt: 'unknown',\n lagSeconds: null,\n reason: 'created-not-a-number',\n }\n }\n // Zero is the value an absent field folds to under the repo's null rules,\n // and no Stripe event was created at the epoch. Rejected by name so it can\n // never be mistaken for a measurement.\n if (created <= 0) {\n return {\n attempt: 'unknown',\n lagSeconds: null,\n reason: 'created-non-positive',\n }\n }\n const received = input.receivedAtMs\n if (typeof received !== 'number' || !Number.isFinite(received) || received <= 0) {\n return { attempt: 'unknown', lagSeconds: null, reason: 'received-unusable' }\n }\n const lagSeconds = received / 1000 - created\n // An event stamped in OUR future by more than the threshold is a clock\n // disagreement, not a fast delivery, and reporting it as a healthy first\n // attempt would be an answer we cannot support.\n if (lagSeconds < -threshold) {\n return { attempt: 'unknown', lagSeconds: null, reason: 'clock-skew' }\n }\n return {\n attempt: lagSeconds > threshold ? 'retried' : 'first-attempt',\n lagSeconds,\n reason: 'measured',\n }\n}\n\n/**\n * Which required events is a destination NOT subscribed to? (AGL-1948)\n *\n * The same blind spot from the configuration side, and the cheaper half of\n * it: a subscription removed by hand in the Stripe dashboard produces no\n * failed delivery, no rejected request and no inert one either — Stripe\n * simply stops sending, and every count on the health probe reads a\n * perfectly healthy zero. `charge.refunded` was missing from the live\n * endpoint for exactly this reason (AGL-1798) and the only thing that ever\n * noticed was a script nobody ran.\n *\n * `['*']` is Stripe's wildcard subscription and covers everything.\n *\n * Returns the names, sorted, so the probe body is stable and a diff between\n * two readings is meaningful.\n */\nexport function unsubscribedRequiredEvents(\n enabledEvents: readonly string[] | null | undefined,\n required: readonly string[] = REQUIRED_WEBHOOK_EVENTS,\n): string[] {\n if (!enabledEvents) return []\n if (enabledEvents.includes('*')) return []\n const enabled = new Set(enabledEvents)\n return required.filter((event) => !enabled.has(event)).sort()\n}\n"],"names":["REQUIRED_WEBHOOK_EVENTS","CONNECT_SCOPE_METADATA_KEY","CONNECT_SCOPE_METADATA_VALUE","REQUIRED_CONNECT_WEBHOOK_EVENTS","isConnectWebhookEndpoint","endpoint","metadata","createWebhookEffectLedger","effects","skips","effect","name","push","skip","reason","WRITE_METHODS","Set","observeWrites","handle","ledger","label","Proxy","get","target","property","receiver","value","Reflect","method","args","result","apply","nextLabel","has","Promise","resolve","then","settled","classifyWebhookDelivery","input","type","claimed","required","length","outcome","includes","RETRY_LAG_SECONDS","classifyDeliveryLag","threshold","thresholdSeconds","created","eventCreatedSeconds","undefined","attempt","lagSeconds","Number","isFinite","received","receivedAtMs","unsubscribedRequiredEvents","enabledEvents","enabled","filter","event","sort"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/webhook-delivery.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 * DID THE WEBHOOK ACTUALLY DO ANYTHING? (AGL-1954)\n *\n * Every signal we have about the billing webhook is a signal about the\n * REQUEST. Stripe records `delivery_success` off the status code. The\n * idempotency claim in `stripeEvents` records that the signature verified and\n * the handler was entered. `/api/health/billing` (AGL-1924) counts what\n * Stripe attempted and failed to deliver. All three read green for the one\n * failure this module exists to catch: **a handler that answers 200 and moves\n * nothing.**\n *\n * That is not hypothetical here. AGL-1798 is exactly it — `charge.refunded`\n * was never subscribed on the live destination, so AGL-1546's entitlement\n * revocation had no trigger, and every other indicator looked fine for as\n * long as it lasted. AGL-1551 is the same class from the other side: a week\n * of 100% rejected deliveries behind a green \"Active\" badge.\n *\n * ## The honest signal is an EFFECT, not a return code\n *\n * A check that asserts \"the handler ran\" is nearly as blind as the 200 —\n * the handler DID run in the AGL-1798 shape, it just had no work registered\n * for the event. So the route reports what it COMMITTED: an org's plan\n * mirrored, a `platformRevenue` row written, a refund stamped, an orphan\n * recorded, an audit row appended, a plugin claiming the event. Those are the\n * things whose absence is the bug.\n *\n * ## Three outcomes, and conflating the middle two is its own failure\n *\n * A binary \"did it write something\" would fire on every legitimately\n * irrelevant delivery — a tenant shopper's subscription carries no\n * `metadata.orgId` and correctly moves nothing on OUR side; a marketplace\n * refund resolves to no workspace customer and is correctly left to the\n * plugins; a `won` dispute nobody claimed reversed no money and correctly\n * wakes nobody. Alerting on those is alert fatigue, which ends with the alarm\n * muted and the real one lost inside it.\n *\n * So a branch that decides an event is not its business must SAY SO, by\n * name, through {@link WebhookEffectLedger.skip}. That is what separates:\n *\n * | outcome | meaning | alarm |\n * |-----------|----------------------------------------------------|-------|\n * | `acted` | something durable committed, or a plugin claimed it | no |\n * | `ignored` | a branch named a reason, or we never asked for this event type | no |\n * | `inert` | an event we deliberately subscribe to produced NEITHER | YES |\n *\n * `inert` is the whole point. Falling off the end of the dispatch with no\n * effect and no stated reason is precisely what a deleted write, an\n * unregistered plugin handler, or a renamed event type looks like from\n * inside the process — and it is the only one of the three that nothing else\n * in this system can see.\n *\n * ## Why the \"did we ask for it\" test is the required-events list\n *\n * Stripe delivers what the destination is subscribed to, and the destination\n * is created by `tools/scripts/setup-stripe.mjs` from `WEBHOOK_EVENTS`. An\n * event type outside that list reaching us is either a hand-added\n * subscription or a Connect delivery arriving at the platform destination —\n * unremarkable, and never our silent-drop bug, because we never claimed to\n * handle it. An event type INSIDE it is one we went out of our way to ask\n * Stripe for, so producing nothing from it is a contradiction by\n * construction.\n *\n * `REQUIRED_WEBHOOK_EVENTS` below is a copy of that list, and a copy is a\n * drift hazard — so `webhook-delivery.spec.ts` reads the `.mjs` and fails on\n * any divergence, in either direction. The alternative (moving the list\n * across the nx boundary out of `tools/`) is a wider change than this\n * warrants and is tracked on AGL-1948.\n *\n * Pure: no clock, no I/O, no Firestore. The route records, this decides, the\n * spec exercises every branch without a network.\n */\n\n/** What a single delivery turned out to be. */\nexport type WebhookDeliveryOutcome = 'acted' | 'ignored' | 'inert'\n\n/**\n * The event types the platform destination is subscribed to, mirroring\n * `WEBHOOK_EVENTS` in `tools/scripts/lib/stripe-webhook-health.mjs`.\n *\n * The list is the definition of \"we asked for this\", which is what makes a\n * no-op delivery of one of them a defect rather than a non-event. Kept in\n * the same order as the source so a diff between the two reads cleanly.\n */\nexport const REQUIRED_WEBHOOK_EVENTS: readonly string[] = [\n 'customer.subscription.created',\n 'customer.subscription.updated',\n 'customer.subscription.deleted',\n 'checkout.session.completed',\n 'invoice.finalized',\n 'invoice.paid',\n 'invoice.payment_failed',\n 'charge.refunded',\n 'charge.dispute.created',\n 'charge.dispute.closed',\n // AI overage charged as it accrues (AGL-3011), in the source list's order.\n 'invoice.voided',\n 'invoice.marked_uncollectible',\n 'customer.updated',\n 'payment_method.attached',\n 'payment_method.detached',\n // Fraud signals to staff (AGL-3356).\n 'radar.early_fraud_warning.created',\n 'review.opened',\n]\n\n/**\n * How the CONNECT destination identifies itself (AGL-1948), mirroring\n * `CONNECT_SCOPE_METADATA_KEY` / `CONNECT_SCOPE_METADATA_VALUE` in\n * `tools/scripts/lib/stripe-webhook-health.mjs`.\n *\n * Connected-account events are delivered only to a destination created with\n * `connect: true`, and Stripe's API does not report that flag back on the\n * endpoint object — so the destination is identified by metadata we set at\n * creation instead. Same copy-and-guard arrangement as the list above.\n */\nexport const CONNECT_SCOPE_METADATA_KEY = 'aglyn_scope'\nexport const CONNECT_SCOPE_METADATA_VALUE = 'connect'\n\n/**\n * What the Connect destination must carry, mirroring\n * `CONNECT_WEBHOOK_EVENTS` in the same script lib.\n *\n * `account.updated` is the whole point of it: AGL-1997's `syncConnectAccountStatus`\n * is what stops a merchant whose Stripe account was later restricted from\n * going on selling against a stale `stripeChargesEnabled`, with the SHOPPER\n * meeting the failure at payment time. Without this event the handler cannot\n * run — which is exactly the state AGL-2122 found and fixed.\n */\nexport const REQUIRED_CONNECT_WEBHOOK_EVENTS: readonly string[] = [\n 'account.updated',\n 'payout.failed',\n 'payout.paid',\n]\n\n/**\n * Does this Stripe endpoint object carry our Connect scope marker?\n *\n * Coverage for it reuses `unsubscribedRequiredEvents` with\n * `REQUIRED_CONNECT_WEBHOOK_EVENTS` as the `required` argument — the wildcard\n * and null handling are identical questions, and a second copy of that logic\n * is a second place for it to drift.\n */\nexport function isConnectWebhookEndpoint(endpoint: unknown): boolean {\n const metadata = (endpoint as { metadata?: Record<string, unknown> })?.metadata\n return metadata?.[CONNECT_SCOPE_METADATA_KEY] === CONNECT_SCOPE_METADATA_VALUE\n}\n\n/**\n * A per-delivery record of what actually committed.\n *\n * Deliberately append-only and stringly-typed: the reasons are read by a\n * human in a log line and by a spec, never switched on. Keeping it dumb is\n * what stops it growing into a second dispatch table that can disagree with\n * the real one.\n */\nexport interface WebhookEffectLedger {\n /** Durable things this delivery committed, in the order they landed. */\n readonly effects: readonly string[]\n /** Reasons a branch decided the event was none of its business. */\n readonly skips: readonly string[]\n /**\n * Record a committed effect. Call this AFTER the write it names, so a\n * throw between the two cannot claim an effect that never landed.\n */\n effect(name: string): void\n /**\n * Record a deliberate no-op and why. This is the difference between \"not\n * ours\" and \"broken\", and it is the half that prevents alert fatigue.\n */\n skip(reason: string): void\n}\n\nexport function createWebhookEffectLedger(): WebhookEffectLedger {\n const effects: string[] = []\n const skips: string[] = []\n return {\n effects,\n skips,\n effect(name: string) {\n effects.push(name)\n },\n skip(reason: string) {\n skips.push(reason)\n },\n }\n}\n\n/** Firestore methods that MUTATE. Everything else is a read or a builder. */\nconst WRITE_METHODS = new Set(['set', 'update', 'create', 'delete', 'add'])\n\n/**\n * Wrap a Firestore handle so every write it commits lands in the ledger.\n *\n * ## Why this exists rather than a `ledger.effect()` beside each write\n *\n * A hand-placed note next to a write is a check that CANNOT FAIL in the one\n * way that matters. Delete the write and leave the note, and the ledger\n * cheerfully reports an effect that never happened — which is the same\n * \"asserts its own literals\" defect the health probes are written to avoid.\n * The note has to be caused by the write, not merely adjacent to it.\n *\n * So the effect is recorded from INSIDE the call, after it resolves. A write\n * that throws records nothing (it did not commit); a write that is deleted\n * from the source records nothing (it is not called). There is no edit to\n * the handler that removes the work and keeps the signal.\n *\n * ## What it deliberately does not see\n *\n * Writes issued through a DIFFERENT Firestore handle — the shared\n * `tenant-data-admin` writers (`writeOrgBilling`, `notifyOrgAdmins`,\n * `notifyStaff`), which each open their own — and anything a plugin does\n * inside its own handler. Those are noted explicitly by the caller where\n * they are the only consequence of a branch, and the comments at those call\n * sites say so. The boundary is stated rather than hidden: this covers the\n * route's own writes, which is where the route's own bugs live.\n *\n * A Proxy rather than a hand-written facade because the wrapped object is\n * passed on to `updateExisting` and friends, which call methods this module\n * has never heard of. A facade would have to enumerate them and would\n * silently drop the ones it forgot.\n */\nexport function observeWrites<T extends object>(\n handle: T,\n ledger: WebhookEffectLedger,\n label = 'firestore',\n): T {\n return new Proxy(handle, {\n get(target, property, receiver) {\n const value = Reflect.get(target, property, receiver)\n if (typeof value !== 'function' || typeof property !== 'string') {\n return value\n }\n const method = property\n return (...args: unknown[]) => {\n const result = (value as (...a: unknown[]) => unknown).apply(\n target,\n args,\n )\n // `collection('orgs')` names the thing being written, so the ledger\n // reads `orgs.update` rather than `firestore.update`. Everything\n // else inherits the label it was reached through.\n const nextLabel =\n method === 'collection' && typeof args[0] === 'string'\n ? (args[0] as string)\n : label\n if (WRITE_METHODS.has(method)) {\n // Recorded on RESOLUTION. A rejected write committed nothing and\n // must not read as an effect.\n return Promise.resolve(result).then((settled) => {\n ledger.effect(`${nextLabel}.${method}`)\n return settled\n })\n }\n // Builders (`collection`, `doc`, `where`, `limit`) return objects\n // that can themselves be written through, so the wrapper follows\n // them. Thenables are returned bare: wrapping a promise would make\n // `then` look like a builder and recurse forever.\n if (\n result &&\n typeof result === 'object' &&\n typeof (result as { then?: unknown }).then !== 'function'\n ) {\n return observeWrites(result as object, ledger, nextLabel)\n }\n return result\n }\n },\n })\n}\n\nexport interface WebhookDeliveryVerdict {\n outcome: WebhookDeliveryOutcome\n /** One short phrase naming WHY, for the log line and the spec. */\n reason: string\n}\n\n/**\n * Classify one delivery.\n *\n * Order is the argument:\n *\n * 1. **Any effect, or any plugin claim, wins.** A delivery that both wrote\n * something and skipped something else did work; a partially-skipped\n * handler is not an idle one.\n * 2. **A named skip is an answer.** Reaching a branch that consciously\n * decided \"not ours\" is a handled event, not a dropped one.\n * 3. **An event type we never subscribed to is not our problem.** Nothing\n * promised to handle it.\n * 4. **Everything else is inert** — and by elimination that means: we asked\n * Stripe for this event, we received it, no handler claimed it, no branch\n * wrote anything, and no branch could say why.\n */\nexport function classifyWebhookDelivery(input: {\n type: string\n effects: readonly string[]\n skips: readonly string[]\n claimed: boolean\n /** Overridable so the spec can prove the rule rather than the list. */\n required?: readonly string[]\n}): WebhookDeliveryVerdict {\n const { type, effects, skips, claimed } = input\n const required = input.required ?? REQUIRED_WEBHOOK_EVENTS\n if (effects.length > 0) return { outcome: 'acted', reason: effects[0] }\n if (claimed) return { outcome: 'acted', reason: 'plugin-claimed' }\n if (skips.length > 0) return { outcome: 'ignored', reason: skips[0] }\n if (!required.includes(type)) {\n return { outcome: 'ignored', reason: 'not-subscribed' }\n }\n return { outcome: 'inert', reason: 'no-effect' }\n}\n\n/**\n * How long after Stripe CREATES an event its first delivery attempt may\n * plausibly take to reach us, in seconds. Mirrors `RETRY_LAG_SECONDS` in\n * `tools/scripts/lib/stripe-webhook-health.mjs`, under the same\n * read-the-`.mjs`-as-text drift guard as the lists above.\n *\n * Calibrated against the live account on 2026-08-18: the five deliveries that\n * succeeded first time landed in 1.0–3.7s; the one that did not —\n * `evt_1U49XtDYHP4psn7hA9VHPnZz`, whose three 400s ARE the three failures the\n * Stripe Dashboard was showing — landed 16,665s (4h 37m) late. Two minutes\n * sits three orders of magnitude clear of the healthy band and still inside\n * Stripe's first automatic retry interval, so it separates the two without\n * straddling either.\n */\nexport const RETRY_LAG_SECONDS = 120\n\n/**\n * What a delivery's lag says about the attempts BEFORE it.\n *\n * `unknown` is a first-class outcome, not a synonym for either answer. See\n * {@link classifyDeliveryLag} for why that matters more here than usual.\n */\nexport type WebhookDeliveryAttempt = 'first-attempt' | 'retried' | 'unknown'\n\n/** Why {@link classifyDeliveryLag} answered as it did. */\nexport type WebhookDeliveryLagReason =\n | 'measured'\n | 'created-absent'\n | 'created-not-a-number'\n | 'created-non-positive'\n | 'received-unusable'\n | 'clock-skew'\n\nexport interface WebhookDeliveryLagVerdict {\n attempt: WebhookDeliveryAttempt\n /** Seconds between `event.created` and our claim. Null unless `measured`. */\n lagSeconds: number | null\n reason: WebhookDeliveryLagReason\n}\n\n/**\n * DID THIS DELIVERY ONLY LAND ON A RETRY? (AGL-2039, the last arm of AGL-1948)\n *\n * ## The reconciliation this exists to close\n *\n * `/api/health/billing` scores EVENTS and `GET /v1/events?delivery_success=false`\n * is a TERMINAL-state filter: it selects events that are still pending or\n * failed EVERY attempt. An event that 400s three times and then succeeds on\n * the fourth reads back `delivery_success: true` and is, to that filter,\n * indistinguishable from one that succeeded immediately. The Stripe Dashboard\n * scores delivery ATTEMPTS, so the same hour reads 0% here and 30% there —\n * and both numbers are right. The three real failures were visible only in\n * the second reading, and each was a mirror that did not run when it should\n * have.\n *\n * The recoverable half of the attempt history is in our own data. The route\n * claims `stripeEvents/{id}` AFTER signature verification and DELETES the\n * claim when a handler throws, so the stamp always records the attempt that\n * actually got through. The distance from `event.created` to that stamp is\n * the delivery lag, and a lag of minutes means an earlier attempt failed.\n *\n * ## Why this is decided at WRITE time, not at read time\n *\n * The obvious implementation reads `receivedAt` off every claim document in\n * the window and subtracts. That turns a `.count()` aggregation — one\n * integer, zero documents read — into a document scan on a PUBLIC,\n * unauthenticated health endpoint polled every five minutes. Deciding here,\n * once, at the moment of the claim, lets the probe stay an aggregation over a\n * marker field, exactly like AGL-1954's `inertAtMs`.\n *\n * ## Absent, zero and unknown are THREE different things\n *\n * `strictNullChecks` is off repo-wide, so an absent `created` folds to falsy\n * and a naive `Number(event?.created ?? 0)` yields 0 — which subtracts to a\n * lag of ~1.7 BILLION seconds and reports every such delivery as a retry,\n * forever, on a perfectly healthy webhook. A monitoring signal that reds on\n * its own missing input is worse than no signal, because it is the thing that\n * gets muted. So each of those inputs is rejected by name:\n *\n * | input | attempt | reason |\n * |-----------------------------|------------|-------------------------|\n * | field absent / null | `unknown` | `created-absent` |\n * | a string, NaN, Infinity | `unknown` | `created-not-a-number` |\n * | `0` or negative | `unknown` | `created-non-positive` |\n * | our own stamp unusable | `unknown` | `received-unusable` |\n * | created in the FUTURE by more than the bar | `unknown` | `clock-skew` |\n *\n * A lag between `-threshold` and `+threshold` is `first-attempt`: sub-second\n * clock skew between Stripe's clock and ours is ordinary and is not evidence\n * of anything.\n *\n * Pure: no clock, no I/O. The caller supplies both timestamps.\n */\nexport function classifyDeliveryLag(input: {\n /** `event.created`, in unix SECONDS, exactly as Stripe sent it. */\n eventCreatedSeconds: unknown\n /** When we claimed the event, in unix MILLISECONDS. */\n receivedAtMs: unknown\n /** Overridable so the spec can prove the rule rather than the constant. */\n thresholdSeconds?: number\n}): WebhookDeliveryLagVerdict {\n const threshold = input.thresholdSeconds ?? RETRY_LAG_SECONDS\n const created = input.eventCreatedSeconds\n if (created === undefined || created === null) {\n return { attempt: 'unknown', lagSeconds: null, reason: 'created-absent' }\n }\n if (typeof created !== 'number' || !Number.isFinite(created)) {\n return {\n attempt: 'unknown',\n lagSeconds: null,\n reason: 'created-not-a-number',\n }\n }\n // Zero is the value an absent field folds to under the repo's null rules,\n // and no Stripe event was created at the epoch. Rejected by name so it can\n // never be mistaken for a measurement.\n if (created <= 0) {\n return {\n attempt: 'unknown',\n lagSeconds: null,\n reason: 'created-non-positive',\n }\n }\n const received = input.receivedAtMs\n if (typeof received !== 'number' || !Number.isFinite(received) || received <= 0) {\n return { attempt: 'unknown', lagSeconds: null, reason: 'received-unusable' }\n }\n const lagSeconds = received / 1000 - created\n // An event stamped in OUR future by more than the threshold is a clock\n // disagreement, not a fast delivery, and reporting it as a healthy first\n // attempt would be an answer we cannot support.\n if (lagSeconds < -threshold) {\n return { attempt: 'unknown', lagSeconds: null, reason: 'clock-skew' }\n }\n return {\n attempt: lagSeconds > threshold ? 'retried' : 'first-attempt',\n lagSeconds,\n reason: 'measured',\n }\n}\n\n/**\n * Which required events is a destination NOT subscribed to? (AGL-1948)\n *\n * The same blind spot from the configuration side, and the cheaper half of\n * it: a subscription removed by hand in the Stripe dashboard produces no\n * failed delivery, no rejected request and no inert one either — Stripe\n * simply stops sending, and every count on the health probe reads a\n * perfectly healthy zero. `charge.refunded` was missing from the live\n * endpoint for exactly this reason (AGL-1798) and the only thing that ever\n * noticed was a script nobody ran.\n *\n * `['*']` is Stripe's wildcard subscription and covers everything.\n *\n * Returns the names, sorted, so the probe body is stable and a diff between\n * two readings is meaningful.\n */\nexport function unsubscribedRequiredEvents(\n enabledEvents: readonly string[] | null | undefined,\n required: readonly string[] = REQUIRED_WEBHOOK_EVENTS,\n): string[] {\n if (!enabledEvents) return []\n if (enabledEvents.includes('*')) return []\n const enabled = new Set(enabledEvents)\n return required.filter((event) => !enabled.has(event)).sort()\n}\n"],"names":["REQUIRED_WEBHOOK_EVENTS","CONNECT_SCOPE_METADATA_KEY","CONNECT_SCOPE_METADATA_VALUE","REQUIRED_CONNECT_WEBHOOK_EVENTS","isConnectWebhookEndpoint","endpoint","metadata","createWebhookEffectLedger","effects","skips","effect","name","push","skip","reason","WRITE_METHODS","Set","observeWrites","handle","ledger","label","Proxy","get","target","property","receiver","value","Reflect","method","args","result","apply","nextLabel","has","Promise","resolve","then","settled","classifyWebhookDelivery","input","type","claimed","required","length","outcome","includes","RETRY_LAG_SECONDS","classifyDeliveryLag","threshold","thresholdSeconds","created","eventCreatedSeconds","undefined","attempt","lagSeconds","Number","isFinite","received","receivedAtMs","unsubscribedRequiredEvents","enabledEvents","enabled","filter","event","sort"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsEC,GAED,6CAA6C,GAG7C;;;;;;;CAOC,GACD,OAAO,MAAMA,0BAA6C;IACxD;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA,2EAA2E;IAC3E;IACA;IACA;IACA;IACA;IACA,qCAAqC;IACrC;IACA;CACD,CAAA;AAED;;;;;;;;;CASC,GACD,OAAO,MAAMC,6BAA6B,cAAa;AACvD,OAAO,MAAMC,+BAA+B,UAAS;AAErD;;;;;;;;;CASC,GACD,OAAO,MAAMC,kCAAqD;IAChE;IACA;IACA;CACD,CAAA;AAED;;;;;;;CAOC,GACD,OAAO,SAASC,yBAAyBC,QAAiB;IACxD,MAAMC,WAAYD,4BAAD,AAACA,SAAqDC,QAAQ;IAC/E,OAAOA,CAAAA,4BAAAA,QAAU,CAACL,2BAA2B,MAAKC;AACpD;AA2BA,OAAO,SAASK;IACd,MAAMC,UAAoB,EAAE;IAC5B,MAAMC,QAAkB,EAAE;IAC1B,OAAO;QACLD;QACAC;QACAC,QAAOC,IAAY;YACjBH,QAAQI,IAAI,CAACD;QACf;QACAE,MAAKC,MAAc;YACjBL,MAAMG,IAAI,CAACE;QACb;IACF;AACF;AAEA,2EAA2E,GAC3E,MAAMC,gBAAgB,IAAIC,IAAI;IAAC;IAAO;IAAU;IAAU;IAAU;CAAM;AAE1E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8BC,GACD,OAAO,SAASC,cACdC,MAAS,EACTC,MAA2B,EAC3BC,QAAQ,WAAW;IAEnB,OAAO,IAAIC,MAAMH,QAAQ;QACvBI,KAAIC,MAAM,EAAEC,QAAQ,EAAEC,QAAQ;YAC5B,MAAMC,QAAQC,QAAQL,GAAG,CAACC,QAAQC,UAAUC;YAC5C,IAAI,OAAOC,UAAU,cAAc,OAAOF,aAAa,UAAU;gBAC/D,OAAOE;YACT;YACA,MAAME,SAASJ;YACf,OAAO,CAAC,GAAGK;gBACT,MAAMC,SAAS,AAACJ,MAAuCK,KAAK,CAC1DR,QACAM;gBAEF,oEAAoE;gBACpE,iEAAiE;gBACjE,kDAAkD;gBAClD,MAAMG,YACJJ,WAAW,gBAAgB,OAAOC,IAAI,CAAC,EAAE,KAAK,WACzCA,IAAI,CAAC,EAAE,GACRT;gBACN,IAAIL,cAAckB,GAAG,CAACL,SAAS;oBAC7B,iEAAiE;oBACjE,8BAA8B;oBAC9B,OAAOM,QAAQC,OAAO,CAACL,QAAQM,IAAI,CAAC,CAACC;wBACnClB,OAAOT,MAAM,CAAC,GAAGsB,UAAU,CAAC,EAAEJ,QAAQ;wBACtC,OAAOS;oBACT;gBACF;gBACA,kEAAkE;gBAClE,iEAAiE;gBACjE,mEAAmE;gBACnE,kDAAkD;gBAClD,IACEP,UACA,OAAOA,WAAW,YAClB,OAAO,AAACA,OAA8BM,IAAI,KAAK,YAC/C;oBACA,OAAOnB,cAAca,QAAkBX,QAAQa;gBACjD;gBACA,OAAOF;YACT;QACF;IACF;AACF;AAQA;;;;;;;;;;;;;;;CAeC,GACD,OAAO,SAASQ,wBAAwBC,KAOvC;QAEkBA;IADjB,MAAM,EAAEC,IAAI,EAAEhC,OAAO,EAAEC,KAAK,EAAEgC,OAAO,EAAE,GAAGF;IAC1C,MAAMG,YAAWH,kBAAAA,MAAMG,QAAQ,YAAdH,kBAAkBvC;IACnC,IAAIQ,QAAQmC,MAAM,GAAG,GAAG,OAAO;QAAEC,SAAS;QAAS9B,QAAQN,OAAO,CAAC,EAAE;IAAC;IACtE,IAAIiC,SAAS,OAAO;QAAEG,SAAS;QAAS9B,QAAQ;IAAiB;IACjE,IAAIL,MAAMkC,MAAM,GAAG,GAAG,OAAO;QAAEC,SAAS;QAAW9B,QAAQL,KAAK,CAAC,EAAE;IAAC;IACpE,IAAI,CAACiC,SAASG,QAAQ,CAACL,OAAO;QAC5B,OAAO;YAAEI,SAAS;YAAW9B,QAAQ;QAAiB;IACxD;IACA,OAAO;QAAE8B,SAAS;QAAS9B,QAAQ;IAAY;AACjD;AAEA;;;;;;;;;;;;;CAaC,GACD,OAAO,MAAMgC,oBAAoB,IAAG;AA0BpC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoDC,GACD,OAAO,SAASC,oBAAoBR,KAOnC;QACmBA;IAAlB,MAAMS,aAAYT,0BAAAA,MAAMU,gBAAgB,YAAtBV,0BAA0BO;IAC5C,MAAMI,UAAUX,MAAMY,mBAAmB;IACzC,IAAID,YAAYE,aAAaF,YAAY,MAAM;QAC7C,OAAO;YAAEG,SAAS;YAAWC,YAAY;YAAMxC,QAAQ;QAAiB;IAC1E;IACA,IAAI,OAAOoC,YAAY,YAAY,CAACK,OAAOC,QAAQ,CAACN,UAAU;QAC5D,OAAO;YACLG,SAAS;YACTC,YAAY;YACZxC,QAAQ;QACV;IACF;IACA,0EAA0E;IAC1E,2EAA2E;IAC3E,uCAAuC;IACvC,IAAIoC,WAAW,GAAG;QAChB,OAAO;YACLG,SAAS;YACTC,YAAY;YACZxC,QAAQ;QACV;IACF;IACA,MAAM2C,WAAWlB,MAAMmB,YAAY;IACnC,IAAI,OAAOD,aAAa,YAAY,CAACF,OAAOC,QAAQ,CAACC,aAAaA,YAAY,GAAG;QAC/E,OAAO;YAAEJ,SAAS;YAAWC,YAAY;YAAMxC,QAAQ;QAAoB;IAC7E;IACA,MAAMwC,aAAaG,WAAW,OAAOP;IACrC,uEAAuE;IACvE,yEAAyE;IACzE,gDAAgD;IAChD,IAAII,aAAa,CAACN,WAAW;QAC3B,OAAO;YAAEK,SAAS;YAAWC,YAAY;YAAMxC,QAAQ;QAAa;IACtE;IACA,OAAO;QACLuC,SAASC,aAAaN,YAAY,YAAY;QAC9CM;QACAxC,QAAQ;IACV;AACF;AAEA;;;;;;;;;;;;;;;CAeC,GACD,OAAO,SAAS6C,2BACdC,aAAmD,EACnDlB,WAA8B1C,uBAAuB;IAErD,IAAI,CAAC4D,eAAe,OAAO,EAAE;IAC7B,IAAIA,cAAcf,QAAQ,CAAC,MAAM,OAAO,EAAE;IAC1C,MAAMgB,UAAU,IAAI7C,IAAI4C;IACxB,OAAOlB,SAASoB,MAAM,CAAC,CAACC,QAAU,CAACF,QAAQ5B,GAAG,CAAC8B,QAAQC,IAAI;AAC7D"}
|
|
@@ -167,6 +167,15 @@ export interface AglynHost extends AglynDocument {
|
|
|
167
167
|
* `seo.entity.logo`, which is publisher-semantic JSON-LD.
|
|
168
168
|
*/
|
|
169
169
|
logoUrl?: string;
|
|
170
|
+
/**
|
|
171
|
+
* The site logo for the DARK scheme (AGL-3400). A wordmark drawn for a
|
|
172
|
+
* light ground — dark ink on a transparent PNG — disappears on the dark
|
|
173
|
+
* loader scrim and error screens, so a site may name a second mark for
|
|
174
|
+
* them. Unset means {@link logoUrl} serves both schemes. Read only where a
|
|
175
|
+
* scheme is known; emails, `{{host.logo}}` and the install icon stay on
|
|
176
|
+
* `logoUrl`, because a mail client paints a light ground.
|
|
177
|
+
*/
|
|
178
|
+
logoDarkUrl?: string;
|
|
170
179
|
seo?: {
|
|
171
180
|
title?: string;
|
|
172
181
|
description?: string;
|
|
@@ -340,6 +349,21 @@ export interface AglynHost extends AglynDocument {
|
|
|
340
349
|
/** How an agent should call the site — endpoints, etiquette, limits. */
|
|
341
350
|
howToUse?: string;
|
|
342
351
|
};
|
|
352
|
+
/**
|
|
353
|
+
* Search engine ownership tokens (AGL-3399), emitted by the tenant as
|
|
354
|
+
* `<meta name="google-site-verification">` and `<meta name="msvalidate.01">`
|
|
355
|
+
* in the head of every published page.
|
|
356
|
+
*
|
|
357
|
+
* The HTML-tag method is the only one a site on a platform subdomain can
|
|
358
|
+
* use: it has no DNS of its own and cannot upload a file to the origin.
|
|
359
|
+
* Only the tag's `content` value is stored, and the tenant re-checks it
|
|
360
|
+
* against `SEARCH_ENGINE_VERIFICATION_TOKEN_PATTERN` before emitting, so a
|
|
361
|
+
* stored value is never echoed into the head as written.
|
|
362
|
+
*/
|
|
363
|
+
verification?: {
|
|
364
|
+
google?: string;
|
|
365
|
+
bing?: string;
|
|
366
|
+
};
|
|
343
367
|
};
|
|
344
368
|
/**
|
|
345
369
|
* Customer-configured third-party analytics (AGL-138/661). Long written by
|
|
@@ -69,7 +69,8 @@ export var HostRedirectParams = /*#__PURE__*/ function(HostRedirectParams) {
|
|
|
69
69
|
*/ export const HOST_CLIENT_WRITABLE_FIELDS = {
|
|
70
70
|
displayName: 'The site name shown in the console. Cosmetic, org-scoped, and decides ' + 'nothing — the public address is `subdomain`/`cname`, both denied.',
|
|
71
71
|
logoUrl: 'The site brand mark rendered by the tenant nav (AGL-594). Authored ' + 'content pointing at already-public media; no gate reads it.',
|
|
72
|
-
|
|
72
|
+
logoDarkUrl: 'The dark-scheme variant of `logoUrl` (AGL-3400). Same reasoning: ' + 'authored content pointing at already-public media; no gate reads it.',
|
|
73
|
+
seo: 'Title, description, favicon, app icon, social card, search engine ' + 'verification tokens and the AGL-1263 `discourageSearchEngines` switch. ' + 'All of it is authoring: the values end up in the page the editor is ' + 'already free to write.',
|
|
73
74
|
// `screens` LEFT this map in AGL-2334 and is now classified as denied,
|
|
74
75
|
// following the `disabledPlugins` precedent: it is TIERED, not freely
|
|
75
76
|
// client-writable. The reason it used to carry — "an editor who may add a
|