@aglyn/aglyn 1.0.0-beta.233 → 1.0.0-beta.235
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/activity-labels.d.ts +117 -0
- package/src/lib/app-utils/activity-labels.js +343 -0
- package/src/lib/app-utils/activity-labels.js.map +1 -0
- package/src/lib/app-utils/admin-audit-index.d.ts +11 -0
- package/src/lib/app-utils/admin-audit-index.js +12 -1
- package/src/lib/app-utils/admin-audit-index.js.map +1 -1
- package/src/lib/app-utils/advertising-consent.d.ts +111 -0
- package/src/lib/app-utils/advertising-consent.js +216 -0
- package/src/lib/app-utils/advertising-consent.js.map +1 -0
- package/src/lib/app-utils/advertising-events.d.ts +98 -0
- package/src/lib/app-utils/advertising-events.js +340 -0
- package/src/lib/app-utils/advertising-events.js.map +1 -0
- package/src/lib/app-utils/advertising-tag-mounts.js +15 -8
- package/src/lib/app-utils/advertising-tag-mounts.js.map +1 -1
- package/src/lib/app-utils/advertising-tags.d.ts +54 -8
- package/src/lib/app-utils/advertising-tags.js +126 -28
- package/src/lib/app-utils/advertising-tags.js.map +1 -1
- package/src/lib/app-utils/analytics-events.d.ts +15 -86
- package/src/lib/app-utils/analytics-events.js +21 -6
- package/src/lib/app-utils/analytics-events.js.map +1 -1
- package/src/lib/app-utils/consent-banner-ui.d.ts +14 -0
- package/src/lib/app-utils/consent-banner-ui.js +23 -2
- package/src/lib/app-utils/consent-banner-ui.js.map +1 -1
- package/src/lib/app-utils/docs-help-section-excerpt-text.d.ts +14 -0
- package/src/lib/app-utils/docs-help-section-excerpt-text.js +31 -0
- package/src/lib/app-utils/docs-help-section-excerpt-text.js.map +1 -0
- package/src/lib/app-utils/docs-help-section-excerpt.d.ts +13 -0
- package/src/lib/app-utils/docs-help-section-excerpt.js +37 -0
- package/src/lib/app-utils/docs-help-section-excerpt.js.map +1 -0
- package/src/lib/app-utils/docs-help-sections.generated.d.ts +31 -0
- package/src/lib/app-utils/docs-help-sections.generated.js +660 -0
- package/src/lib/app-utils/docs-help-sections.generated.js.map +1 -0
- package/src/lib/app-utils/docs-help.d.ts +16 -4
- package/src/lib/app-utils/docs-help.generated.d.ts +171 -34
- package/src/lib/app-utils/docs-help.generated.js +1104 -3
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-help.js +25 -6
- package/src/lib/app-utils/docs-help.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +1008 -91
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/health-report.js +12 -0
- package/src/lib/app-utils/health-report.js.map +1 -1
- package/src/lib/app-utils/plugin-release-flags.generated.d.ts +1 -1
- package/src/lib/app-utils/plugin-release-flags.generated.js +7 -1
- package/src/lib/app-utils/plugin-release-flags.generated.js.map +1 -1
- package/src/lib/app-utils/realm-host-surface.generated.js +1 -0
- package/src/lib/app-utils/realm-host-surface.generated.js.map +1 -1
- package/src/lib/app-utils/variables.d.ts +2 -0
- package/src/lib/app-utils/variables.js +3 -0
- package/src/lib/app-utils/variables.js.map +1 -1
- package/src/lib/app-utils/visitor-consent.d.ts +34 -0
- package/src/lib/app-utils/visitor-consent.js +49 -2
- package/src/lib/app-utils/visitor-consent.js.map +1 -1
- package/src/lib/app-utils/where-used-summary.d.ts +41 -0
- package/src/lib/app-utils/where-used-summary.js +33 -0
- package/src/lib/app-utils/where-used-summary.js.map +1 -0
- package/src/lib/app-utils/where-used.d.ts +2 -20
- package/src/lib/app-utils/where-used.js +13 -12
- package/src/lib/app-utils/where-used.js.map +1 -1
- package/src/lib/foundation/definitions/platform.types.d.ts +33 -0
- package/src/lib/foundation/definitions/platform.types.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.d.ts +7 -0
- package/src/lib/plugin-manager/first-party-plugins.generated.js +166 -2
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
- package/src/lib/plugin-manager/plugin-activity-actions.d.ts +51 -0
- package/src/lib/plugin-manager/plugin-activity-actions.js +43 -0
- package/src/lib/plugin-manager/plugin-activity-actions.js.map +1 -1
- package/src/lib/plugin-manager/plugin-advertising-conversions.d.ts +84 -0
- package/src/lib/plugin-manager/plugin-advertising-conversions.js +86 -0
- package/src/lib/plugin-manager/plugin-advertising-conversions.js.map +1 -0
- package/src/lib/plugin-manager/plugin-config.d.ts +23 -0
- package/src/lib/plugin-manager/plugin-config.js.map +1 -1
- package/src/lib/plugin-manager/plugin-local-deliveries.d.ts +161 -0
- package/src/lib/plugin-manager/plugin-local-deliveries.js +46 -0
- package/src/lib/plugin-manager/plugin-local-deliveries.js.map +1 -0
- package/src/lib/plugin-manager/plugin-media-ingest.d.ts +87 -0
- package/src/lib/plugin-manager/plugin-media-ingest.js +34 -0
- package/src/lib/plugin-manager/plugin-media-ingest.js.map +1 -0
- package/src/lib/plugin-manager/plugin-order-email-copies.d.ts +93 -0
- package/src/lib/plugin-manager/plugin-order-email-copies.js +104 -0
- package/src/lib/plugin-manager/plugin-order-email-copies.js.map +1 -0
- package/src/lib/plugin-manager/plugin-shipment-records.d.ts +7 -0
- package/src/lib/plugin-manager/plugin-shipment-records.js.map +1 -1
- package/src/lib/plugin-manager/plugin-site-csp.d.ts +58 -0
- package/src/lib/plugin-manager/plugin-site-csp.js +103 -0
- package/src/lib/plugin-manager/plugin-site-csp.js.map +1 -0
- package/src/lib/plugin-manager/realm-host-aglyn.generated.js +1 -0
- package/src/lib/plugin-manager/realm-host-aglyn.generated.js.map +1 -1
- package/src/lib/plugin-manager/site-page-hooks.d.ts +13 -0
- package/src/lib/plugin-manager/site-page-hooks.js +31 -0
- package/src/lib/plugin-manager/site-page-hooks.js.map +1 -1
- package/src/lib/plugin-manager/stock-photo-provider.d.ts +141 -0
- package/src/lib/plugin-manager/stock-photo-provider.js +59 -0
- package/src/lib/plugin-manager/stock-photo-provider.js.map +1 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/admin-audit-index.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n isPluginStaffAuditAccess,\n pluginStaffAuditActionGroup,\n} from '../plugin-manager/plugin-activity-actions'\nimport { nameSearchTokens } from './name-search'\n\n/*\n * WHAT THE STAFF AUDIT LOG IS QUERIED BY, WRITTEN WITH EVERY ROW (AGL-3321).\n *\n * The staff audit page and the audit tables on a staff account's page filter\n * `adminAudit` by fields no writer used to store — the action's group, the\n * kind of thing acted on, the site, whether the act only looked — and search\n * it. All of that used to be answered by reading the log in batches and\n * keeping the rows that matched, and a match read that way stops wherever\n * the batches stop, so an entry past them was reported as not there. On an\n * audit trail that is the one wrong answer that matters.\n *\n * So each is stored on the row as it is written, and the query asks for it:\n *\n * actionGroup the facet's group for the action, answered by the plugin\n * activity registry (`pluginStaffAuditActionGroup`): a\n * registered group by code or by `staffAuditPrefixes`,\n * otherwise the action's leading namespace.\n * kind `access` for an act that only looked (a core read action,\n * or one a plugin declares in `staffAuditAccessActions`),\n * `change` for everything else.\n * targetKind the kind of record acted on: the first segment of the\n * target path (`orgs`, `users`, `hosts`, `lockdowns`).\n * targetHostId the site acted on, when the target is a site or a record\n * under one (`hosts/{id}/…`, `orgs/{org}/hosts/{id}/…`);\n * null otherwise, so \"no site\" is a value a query can ask for.\n * searchTokens word-prefix tokens (`nameSearchTokens`) of the fields a\n * reviewer searches by, for `array-contains` on one word.\n *\n * Every write goes through `withAdminAuditIndex`, on the server through\n * `addAdminAudit` / `setAdminAudit` / `recordAdminAudit` in\n * `@aglyn/tenant-data-admin`. `apps/console/specs/admin-audit-writes-are-stamped.spec.ts`\n * refuses a write to the collection anywhere else, and\n * `apps/console/specs/admin-audit-action-groups.spec.ts` pins the registry's\n * groups and reads to `tools/scripts/lib/admin-audit-index.fixtures.json`,\n * the mapping `tools/scripts/backfill-admin-audit-index.mjs` restamps old\n * rows with. A plugin adding a code, a prefix or a read action therefore\n * fails CI until the fixture names it, and the backfill is re-run.\n */\n\n/** The row's group, as the Action group filter asks for it. */\nexport const ADMIN_AUDIT_GROUP_FIELD = 'actionGroup'\n\n/** Whether the row only looked, as the account page's two tables ask for it. */\nexport const ADMIN_AUDIT_KIND_FIELD = 'kind'\n\n/** The kind of record acted on, as the Target type filter asks for it. */\nexport const ADMIN_AUDIT_TARGET_KIND_FIELD = 'targetKind'\n\n/** The site acted on, as the Site filter asks for it. */\nexport const ADMIN_AUDIT_SITE_FIELD = 'targetHostId'\n\n/** The row's search tokens, as the search asks for one. */\nexport const ADMIN_AUDIT_SEARCH_FIELD = 'searchTokens'\n\n/**\n * The fields a search reaches, in the order they claim the token budget:\n * what was done, who did it, what it was done to, then why.\n */\nexport const ADMIN_AUDIT_SEARCHED_FIELDS = [\n 'action',\n 'actorEmail',\n 'target',\n 'actorUid',\n 'scope',\n 'reason',\n 'note',\n] as const\n\n/**\n * The most tokens one row stores. A free-text note is the only field that\n * can run long, and it is searched last, so it is the one that loses reach\n * past the cap.\n */\nexport const ADMIN_AUDIT_SEARCH_TOKEN_LIMIT = 200\n\n/** Access looked at data; change altered something or acted on someone. */\nexport type AdminAuditKind = 'access' | 'change'\n\n/**\n * The core actions that only LOOKED.\n *\n * An exception list, not a classification of everything, and the default\n * matters more than the membership: anything absent is a `change`. A change\n * is the louder half of the console's audit card, so an action nobody has\n * classified yet gets the MORE prominent treatment rather than the quieter\n * one. The failure mode of the opposite default is an unclassified\n * impersonation rendering as routine browsing.\n *\n * An export is deliberately NOT here. Data leaving the platform is a\n * high-consequence act even though it mutates nothing, and it belongs beside\n * the impersonations rather than beside the record views.\n */\nexport const ADMIN_AUDIT_ACCESS_ACTIONS: readonly string[] = [\n 'email.message-viewed',\n // The acquisition card (AGL-3289). No longer written; the rows already in\n // the log still classify as reads.\n 'user.acquisition-viewed',\n 'org.acquisition-viewed',\n]\n\n/** What a row carries that the stamped fields are derived from. */\nexport type AdminAuditIndexSource = Partial<\n Record<(typeof ADMIN_AUDIT_SEARCHED_FIELDS)[number], unknown>\n>\n\n/** The fields the lists query. */\nexport interface AdminAuditIndexFields {\n actionGroup: string\n kind: AdminAuditKind\n targetKind: string\n targetHostId: string | null\n searchTokens: string[]\n}\n\n/** Separators inside a value: an address's `@` and `.`, a path's `/`, a code's `.`. */\nconst SEPARATORS = /[^\\p{L}\\p{N}]+/gu\n\n/**\n * The search tokens for one row.\n *\n * Each value is tokenized twice: as written, so a typed address or code\n * (`jane@acme`, `org.override`) matches from its start, and split at its\n * separators, so a reader finds `org.override` by `override` and an address\n * by its domain.\n */\nexport function adminAuditSearchTokens(entry: AdminAuditIndexSource): string[] {\n const tokens = new Set<string>()\n for (const field of ADMIN_AUDIT_SEARCHED_FIELDS) {\n const value = entry[field]\n if (typeof value !== 'string' || !value.trim()) continue\n const words = [\n ...nameSearchTokens(value),\n ...nameSearchTokens(value.replace(SEPARATORS, ' ')),\n ]\n for (const token of words) {\n tokens.add(token)\n if (tokens.size >= ADMIN_AUDIT_SEARCH_TOKEN_LIMIT) return [...tokens]\n }\n }\n return [...tokens]\n}\n\n/**\n * The group an action is filed under — the same answer the page's facet\n * offers. Empty for a row with no action.\n */\nexport function adminAuditActionGroup(action: unknown): string {\n return pluginStaffAuditActionGroup(action)\n}\n\n/**\n * An access when the platform or a plugin declares the action a read — a\n * plugin's staff card opening on an org or an account names its own read\n * actions through its activity group (AGL-2939) — and a change otherwise.\n */\nexport function adminAuditKind(action: unknown): AdminAuditKind {\n return typeof action === 'string' &&\n action &&\n (ADMIN_AUDIT_ACCESS_ACTIONS.includes(action) || isPluginStaffAuditAccess(action))\n ? 'access'\n : 'change'\n}\n\n/** The segments of a target path; empty for a target that is not one. */\nconst segmentsOf = (target: unknown): string[] =>\n typeof target === 'string' ? target.trim().split('/').filter(Boolean) : []\n\n/**\n * The kind of record acted on: the target's first segment, which for every\n * path-shaped target is its collection (`orgs/{id}` → `orgs`). A target\n * that is an identifier rather than a path (`sso-domains:acme.com`) is its\n * own type up to the first `:`. Empty for a row with no target.\n */\nexport function adminAuditTargetKind(target: unknown): string {\n const [first = ''] = segmentsOf(target)\n const colon = first.indexOf(':')\n return colon > 0 ? first.slice(0, colon) : first\n}\n\n/**\n * The site acted on: the id after a `hosts` segment in the target path —\n * `hosts/{id}` and anything under it, and a site filed under its\n * organization (`orgs/{org}/hosts/{id}`). Null when the act was not on a site.\n */\nexport function adminAuditTargetHostId(target: unknown): string | null {\n const segments = segmentsOf(target)\n for (let at = 0; at < segments.length - 1; at += 2) {\n if (segments[at] === 'hosts') return segments[at + 1] || null\n }\n return null\n}\n\n/** Every stamped field for one row. */\nexport function adminAuditIndexFields(\n entry: AdminAuditIndexSource,\n): AdminAuditIndexFields {\n return {\n actionGroup: adminAuditActionGroup(entry.action),\n kind: adminAuditKind(entry.action),\n targetKind: adminAuditTargetKind(entry.target),\n targetHostId: adminAuditTargetHostId(entry.target),\n searchTokens: adminAuditSearchTokens(entry),\n }\n}\n\n/**\n * The row as it is stored: the entry, with the fields its lists query.\n * Every write to `adminAudit` passes its data through this.\n */\nexport function withAdminAuditIndex<Entry extends AdminAuditIndexSource>(\n entry: Entry,\n): Entry & AdminAuditIndexFields & { scope: unknown } {\n /*\n * `scope` stored on EVERY row, null when the writer has none (AGL-3680):\n * the audit page sorts by it, and an `orderBy` drops every document that\n * lacks the field. Only the scope-aware writers name one; the rows written\n * before are stamped by `tools/scripts/backfill-staff-list-sort-fields.mjs`.\n */\n return { ...entry, scope: entry.scope ?? null, ...adminAuditIndexFields(entry) }\n}\n"],"names":["isPluginStaffAuditAccess","pluginStaffAuditActionGroup","nameSearchTokens","ADMIN_AUDIT_GROUP_FIELD","ADMIN_AUDIT_KIND_FIELD","ADMIN_AUDIT_TARGET_KIND_FIELD","ADMIN_AUDIT_SITE_FIELD","ADMIN_AUDIT_SEARCH_FIELD","ADMIN_AUDIT_SEARCHED_FIELDS","ADMIN_AUDIT_SEARCH_TOKEN_LIMIT","ADMIN_AUDIT_ACCESS_ACTIONS","SEPARATORS","adminAuditSearchTokens","entry","tokens","Set","field","value","trim","words","replace","token","add","size","adminAuditActionGroup","action","adminAuditKind","includes","segmentsOf","target","split","filter","Boolean","adminAuditTargetKind","first","colon","indexOf","slice","adminAuditTargetHostId","segments","at","length","adminAuditIndexFields","actionGroup","kind","targetKind","targetHostId","searchTokens","withAdminAuditIndex","scope"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/admin-audit-index.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n isPluginStaffAuditAccess,\n pluginStaffAuditActionGroup,\n} from '../plugin-manager/plugin-activity-actions'\nimport { nameSearchTokens } from './name-search'\n\n/*\n * WHAT THE STAFF AUDIT LOG IS QUERIED BY, WRITTEN WITH EVERY ROW (AGL-3321).\n *\n * The staff audit page and the audit tables on a staff account's page filter\n * `adminAudit` by fields no writer used to store — the action's group, the\n * kind of thing acted on, the site, whether the act only looked — and search\n * it. All of that used to be answered by reading the log in batches and\n * keeping the rows that matched, and a match read that way stops wherever\n * the batches stop, so an entry past them was reported as not there. On an\n * audit trail that is the one wrong answer that matters.\n *\n * So each is stored on the row as it is written, and the query asks for it:\n *\n * actionGroup the facet's group for the action, answered by the plugin\n * activity registry (`pluginStaffAuditActionGroup`): a\n * registered group by code or by `staffAuditPrefixes`,\n * otherwise the action's leading namespace.\n * kind `access` for an act that only looked (a core read action,\n * or one a plugin declares in `staffAuditAccessActions`),\n * `change` for everything else.\n * targetKind the kind of record acted on: the first segment of the\n * target path (`orgs`, `users`, `hosts`, `lockdowns`).\n * targetHostId the site acted on, when the target is a site or a record\n * under one (`hosts/{id}/…`, `orgs/{org}/hosts/{id}/…`);\n * null otherwise, so \"no site\" is a value a query can ask for.\n * searchTokens word-prefix tokens (`nameSearchTokens`) of the fields a\n * reviewer searches by, for `array-contains` on one word.\n *\n * Every write goes through `withAdminAuditIndex`, on the server through\n * `addAdminAudit` / `setAdminAudit` / `recordAdminAudit` in\n * `@aglyn/tenant-data-admin`. `apps/console/specs/admin-audit-writes-are-stamped.spec.ts`\n * refuses a write to the collection anywhere else, and\n * `apps/console/specs/admin-audit-action-groups.spec.ts` pins the registry's\n * groups and reads to `tools/scripts/lib/admin-audit-index.fixtures.json`,\n * the mapping `tools/scripts/backfill-admin-audit-index.mjs` restamps old\n * rows with. A plugin adding a code, a prefix or a read action therefore\n * fails CI until the fixture names it, and the backfill is re-run.\n */\n\n/** The row's group, as the Action group filter asks for it. */\nexport const ADMIN_AUDIT_GROUP_FIELD = 'actionGroup'\n\n/** Whether the row only looked, as the account page's two tables ask for it. */\nexport const ADMIN_AUDIT_KIND_FIELD = 'kind'\n\n/** The kind of record acted on, as the Target type filter asks for it. */\nexport const ADMIN_AUDIT_TARGET_KIND_FIELD = 'targetKind'\n\n/** The site acted on, as the Site filter asks for it. */\nexport const ADMIN_AUDIT_SITE_FIELD = 'targetHostId'\n\n/** The row's search tokens, as the search asks for one. */\nexport const ADMIN_AUDIT_SEARCH_FIELD = 'searchTokens'\n\n/**\n * The fields a search reaches, in the order they claim the token budget:\n * what was done, who did it, what it was done to, then why.\n */\nexport const ADMIN_AUDIT_SEARCHED_FIELDS = [\n 'action',\n 'actorEmail',\n 'target',\n 'actorUid',\n 'scope',\n 'reason',\n 'note',\n] as const\n\n/**\n * The most tokens one row stores. A free-text note is the only field that\n * can run long, and it is searched last, so it is the one that loses reach\n * past the cap.\n */\nexport const ADMIN_AUDIT_SEARCH_TOKEN_LIMIT = 200\n\n/** Access looked at data; change altered something or acted on someone. */\nexport type AdminAuditKind = 'access' | 'change'\n\n/**\n * The core actions that only LOOKED.\n *\n * An exception list, not a classification of everything, and the default\n * matters more than the membership: anything absent is a `change`. A change\n * is the louder half of the console's audit card, so an action nobody has\n * classified yet gets the MORE prominent treatment rather than the quieter\n * one. The failure mode of the opposite default is an unclassified\n * impersonation rendering as routine browsing.\n *\n * An export is deliberately NOT here. Data leaving the platform is a\n * high-consequence act even though it mutates nothing, and it belongs beside\n * the impersonations rather than beside the record views.\n */\n/**\n * Staff opened a workspace's or a site's media library — one row per page\n * read — on the staff organization or site page. The target is the library\n * (`orgs/{id}/media`, `hosts/{id}/media`).\n */\nexport const ADMIN_AUDIT_MEDIA_LIBRARY_VIEWED = 'media.library-viewed'\n\n/**\n * Staff opened one asset of a media library — its preview, storage path,\n * owner and usage. The target is the asset (`orgs/{id}/media/{mediaId}`).\n */\nexport const ADMIN_AUDIT_MEDIA_ASSET_VIEWED = 'media.asset-viewed'\n\nexport const ADMIN_AUDIT_ACCESS_ACTIONS: readonly string[] = [\n 'email.message-viewed',\n ADMIN_AUDIT_MEDIA_LIBRARY_VIEWED,\n ADMIN_AUDIT_MEDIA_ASSET_VIEWED,\n // The acquisition card (AGL-3289). No longer written; the rows already in\n // the log still classify as reads.\n 'user.acquisition-viewed',\n 'org.acquisition-viewed',\n]\n\n/** What a row carries that the stamped fields are derived from. */\nexport type AdminAuditIndexSource = Partial<\n Record<(typeof ADMIN_AUDIT_SEARCHED_FIELDS)[number], unknown>\n>\n\n/** The fields the lists query. */\nexport interface AdminAuditIndexFields {\n actionGroup: string\n kind: AdminAuditKind\n targetKind: string\n targetHostId: string | null\n searchTokens: string[]\n}\n\n/** Separators inside a value: an address's `@` and `.`, a path's `/`, a code's `.`. */\nconst SEPARATORS = /[^\\p{L}\\p{N}]+/gu\n\n/**\n * The search tokens for one row.\n *\n * Each value is tokenized twice: as written, so a typed address or code\n * (`jane@acme`, `org.override`) matches from its start, and split at its\n * separators, so a reader finds `org.override` by `override` and an address\n * by its domain.\n */\nexport function adminAuditSearchTokens(entry: AdminAuditIndexSource): string[] {\n const tokens = new Set<string>()\n for (const field of ADMIN_AUDIT_SEARCHED_FIELDS) {\n const value = entry[field]\n if (typeof value !== 'string' || !value.trim()) continue\n const words = [\n ...nameSearchTokens(value),\n ...nameSearchTokens(value.replace(SEPARATORS, ' ')),\n ]\n for (const token of words) {\n tokens.add(token)\n if (tokens.size >= ADMIN_AUDIT_SEARCH_TOKEN_LIMIT) return [...tokens]\n }\n }\n return [...tokens]\n}\n\n/**\n * The group an action is filed under — the same answer the page's facet\n * offers. Empty for a row with no action.\n */\nexport function adminAuditActionGroup(action: unknown): string {\n return pluginStaffAuditActionGroup(action)\n}\n\n/**\n * An access when the platform or a plugin declares the action a read — a\n * plugin's staff card opening on an org or an account names its own read\n * actions through its activity group (AGL-2939) — and a change otherwise.\n */\nexport function adminAuditKind(action: unknown): AdminAuditKind {\n return typeof action === 'string' &&\n action &&\n (ADMIN_AUDIT_ACCESS_ACTIONS.includes(action) || isPluginStaffAuditAccess(action))\n ? 'access'\n : 'change'\n}\n\n/** The segments of a target path; empty for a target that is not one. */\nconst segmentsOf = (target: unknown): string[] =>\n typeof target === 'string' ? target.trim().split('/').filter(Boolean) : []\n\n/**\n * The kind of record acted on: the target's first segment, which for every\n * path-shaped target is its collection (`orgs/{id}` → `orgs`). A target\n * that is an identifier rather than a path (`sso-domains:acme.com`) is its\n * own type up to the first `:`. Empty for a row with no target.\n */\nexport function adminAuditTargetKind(target: unknown): string {\n const [first = ''] = segmentsOf(target)\n const colon = first.indexOf(':')\n return colon > 0 ? first.slice(0, colon) : first\n}\n\n/**\n * The site acted on: the id after a `hosts` segment in the target path —\n * `hosts/{id}` and anything under it, and a site filed under its\n * organization (`orgs/{org}/hosts/{id}`). Null when the act was not on a site.\n */\nexport function adminAuditTargetHostId(target: unknown): string | null {\n const segments = segmentsOf(target)\n for (let at = 0; at < segments.length - 1; at += 2) {\n if (segments[at] === 'hosts') return segments[at + 1] || null\n }\n return null\n}\n\n/** Every stamped field for one row. */\nexport function adminAuditIndexFields(\n entry: AdminAuditIndexSource,\n): AdminAuditIndexFields {\n return {\n actionGroup: adminAuditActionGroup(entry.action),\n kind: adminAuditKind(entry.action),\n targetKind: adminAuditTargetKind(entry.target),\n targetHostId: adminAuditTargetHostId(entry.target),\n searchTokens: adminAuditSearchTokens(entry),\n }\n}\n\n/**\n * The row as it is stored: the entry, with the fields its lists query.\n * Every write to `adminAudit` passes its data through this.\n */\nexport function withAdminAuditIndex<Entry extends AdminAuditIndexSource>(\n entry: Entry,\n): Entry & AdminAuditIndexFields & { scope: unknown } {\n /*\n * `scope` stored on EVERY row, null when the writer has none (AGL-3680):\n * the audit page sorts by it, and an `orderBy` drops every document that\n * lacks the field. Only the scope-aware writers name one; the rows written\n * before are stamped by `tools/scripts/backfill-staff-list-sort-fields.mjs`.\n */\n return { ...entry, scope: entry.scope ?? null, ...adminAuditIndexFields(entry) }\n}\n"],"names":["isPluginStaffAuditAccess","pluginStaffAuditActionGroup","nameSearchTokens","ADMIN_AUDIT_GROUP_FIELD","ADMIN_AUDIT_KIND_FIELD","ADMIN_AUDIT_TARGET_KIND_FIELD","ADMIN_AUDIT_SITE_FIELD","ADMIN_AUDIT_SEARCH_FIELD","ADMIN_AUDIT_SEARCHED_FIELDS","ADMIN_AUDIT_SEARCH_TOKEN_LIMIT","ADMIN_AUDIT_MEDIA_LIBRARY_VIEWED","ADMIN_AUDIT_MEDIA_ASSET_VIEWED","ADMIN_AUDIT_ACCESS_ACTIONS","SEPARATORS","adminAuditSearchTokens","entry","tokens","Set","field","value","trim","words","replace","token","add","size","adminAuditActionGroup","action","adminAuditKind","includes","segmentsOf","target","split","filter","Boolean","adminAuditTargetKind","first","colon","indexOf","slice","adminAuditTargetHostId","segments","at","length","adminAuditIndexFields","actionGroup","kind","targetKind","targetHostId","searchTokens","withAdminAuditIndex","scope"],"mappings":"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"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { type StoredVisitorConsent, type VisitorConsentHost } from './visitor-consent';
|
|
18
|
+
/**
|
|
19
|
+
* A visitor's advertising consent, carried to the SERVER (AGL-3694).
|
|
20
|
+
*
|
|
21
|
+
* ## Why the server needs it, and why it cannot look it up
|
|
22
|
+
*
|
|
23
|
+
* A Conversions API event is sent by a server, often long after the visitor
|
|
24
|
+
* left: a purchase is reported when Stripe's webhook lands, not when the
|
|
25
|
+
* shopper clicked Pay. Consent on a published site is recorded in the
|
|
26
|
+
* visitor's own browser (`visitor-consent.ts`) — ISR-cached pages cannot vary
|
|
27
|
+
* by visitor, so there is no server-side copy to consult later. The only
|
|
28
|
+
* moment the server can learn what this visitor decided is a request the
|
|
29
|
+
* visitor makes: the checkout they start, the form they submit.
|
|
30
|
+
*
|
|
31
|
+
* So those requests carry the record as it stands, and the server decides
|
|
32
|
+
* again from it — with the site's own consent settings and the request's own
|
|
33
|
+
* Global Privacy Control header — through the SAME predicate the browser gate
|
|
34
|
+
* uses ({@link advertisingConsentGranted}). The verdict is recorded with the
|
|
35
|
+
* order or the lead it was given for, and an event whose record says no, or
|
|
36
|
+
* that has no record at all, is never sent: unknown is no.
|
|
37
|
+
*
|
|
38
|
+
* ## What it carries
|
|
39
|
+
*
|
|
40
|
+
* The record's own fields (status, the advertising grant, when, the country),
|
|
41
|
+
* and the vendors' first-party browser ids — `_fbp`/`_fbc` (Meta), `_ttp` and
|
|
42
|
+
* `ttclid` (TikTok), `_epik` (Pinterest) — which are what let a vendor match a
|
|
43
|
+
* server event to the visit it came from. Nothing at all is carried for a
|
|
44
|
+
* visitor whose record does not grant advertising, or whose browser sends GPC:
|
|
45
|
+
* {@link advertisingConsentWire} answers `null` and the request goes without.
|
|
46
|
+
*/
|
|
47
|
+
/** The request-body field the wire travels in. */
|
|
48
|
+
export declare const ADVERTISING_CONSENT_FIELD = "adConsent";
|
|
49
|
+
/** The vendor browser ids a server event may carry, by the vendor's own name for each. */
|
|
50
|
+
export interface AdvertisingBrowserIds {
|
|
51
|
+
fbp?: string;
|
|
52
|
+
fbc?: string;
|
|
53
|
+
ttp?: string;
|
|
54
|
+
ttclid?: string;
|
|
55
|
+
epik?: string;
|
|
56
|
+
}
|
|
57
|
+
export interface AdvertisingConsentWire {
|
|
58
|
+
v: 1;
|
|
59
|
+
status: StoredVisitorConsent['status'];
|
|
60
|
+
advertising: boolean;
|
|
61
|
+
at: number;
|
|
62
|
+
country: string | null;
|
|
63
|
+
/** The id the browser sent a lead's pixel event under; see `advertising-events.ts`. */
|
|
64
|
+
lead?: string;
|
|
65
|
+
/** The page the event happened on, origin and path only. */
|
|
66
|
+
url?: string;
|
|
67
|
+
ids: AdvertisingBrowserIds;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The wire for this visitor on this site, or `null` when there is nothing to
|
|
71
|
+
* carry: no record, a record that does not grant advertising, or a browser
|
|
72
|
+
* sending Global Privacy Control. Browser only.
|
|
73
|
+
*
|
|
74
|
+
* `options.lead` is the id a form minted for this submission, so the server
|
|
75
|
+
* reports the lead under the same event id the pixel did.
|
|
76
|
+
*/
|
|
77
|
+
export declare function advertisingConsentWire(hostId: string | null | undefined, options?: {
|
|
78
|
+
lead?: string | null;
|
|
79
|
+
}): AdvertisingConsentWire | null;
|
|
80
|
+
/** The wire as a request body field: `{ adConsent }` or nothing at all. */
|
|
81
|
+
export declare function advertisingConsentField(hostId: string | null | undefined, options?: {
|
|
82
|
+
lead?: string | null;
|
|
83
|
+
}): {
|
|
84
|
+
adConsent?: AdvertisingConsentWire;
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* A wire from a request body, validated, or `null`. Untrusted: every field is
|
|
88
|
+
* re-checked, the record is re-derived through the browser's own parser (so a
|
|
89
|
+
* hand-edited grant counts for exactly what its status allows), and anything
|
|
90
|
+
* malformed is dropped rather than repaired.
|
|
91
|
+
*/
|
|
92
|
+
export declare function readAdvertisingConsentWire(raw: unknown): AdvertisingConsentWire | null;
|
|
93
|
+
/**
|
|
94
|
+
* The server's verdict: may an advertising event be sent for the visitor this
|
|
95
|
+
* wire came from, on this site? The browser gate's own conditions, asked of
|
|
96
|
+
* the record the visitor carried:
|
|
97
|
+
*
|
|
98
|
+
* - the site runs our consent tool and has something to ask about
|
|
99
|
+
* ({@link hostConsentRequired}) — a site on its own CMP has no answer of
|
|
100
|
+
* ours, and no answer is no;
|
|
101
|
+
* - the site asks about advertising and the record grants it
|
|
102
|
+
* ({@link advertisingGrantedByRecord}), which carries every regional rule
|
|
103
|
+
* and every refusal the browser applies;
|
|
104
|
+
* - the request did not carry Global Privacy Control (`Sec-GPC: 1`), which
|
|
105
|
+
* outranks any record.
|
|
106
|
+
*/
|
|
107
|
+
export declare function advertisingConsentGranted(host: VisitorConsentHost | null | undefined, wire: AdvertisingConsentWire | null | undefined, request: {
|
|
108
|
+
gpc: boolean;
|
|
109
|
+
}): boolean;
|
|
110
|
+
/** Whether a request's headers carry Global Privacy Control. */
|
|
111
|
+
export declare function requestSendsGlobalPrivacyControl(get: (name: string) => string | null | undefined): boolean;
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import { advertisingGrantedByRecord, hasGlobalPrivacyControl, hostConsentRequired, parseStoredVisitorConsent, readStoredVisitorConsent } from "./visitor-consent.js";
|
|
18
|
+
/**
|
|
19
|
+
* A visitor's advertising consent, carried to the SERVER (AGL-3694).
|
|
20
|
+
*
|
|
21
|
+
* ## Why the server needs it, and why it cannot look it up
|
|
22
|
+
*
|
|
23
|
+
* A Conversions API event is sent by a server, often long after the visitor
|
|
24
|
+
* left: a purchase is reported when Stripe's webhook lands, not when the
|
|
25
|
+
* shopper clicked Pay. Consent on a published site is recorded in the
|
|
26
|
+
* visitor's own browser (`visitor-consent.ts`) — ISR-cached pages cannot vary
|
|
27
|
+
* by visitor, so there is no server-side copy to consult later. The only
|
|
28
|
+
* moment the server can learn what this visitor decided is a request the
|
|
29
|
+
* visitor makes: the checkout they start, the form they submit.
|
|
30
|
+
*
|
|
31
|
+
* So those requests carry the record as it stands, and the server decides
|
|
32
|
+
* again from it — with the site's own consent settings and the request's own
|
|
33
|
+
* Global Privacy Control header — through the SAME predicate the browser gate
|
|
34
|
+
* uses ({@link advertisingConsentGranted}). The verdict is recorded with the
|
|
35
|
+
* order or the lead it was given for, and an event whose record says no, or
|
|
36
|
+
* that has no record at all, is never sent: unknown is no.
|
|
37
|
+
*
|
|
38
|
+
* ## What it carries
|
|
39
|
+
*
|
|
40
|
+
* The record's own fields (status, the advertising grant, when, the country),
|
|
41
|
+
* and the vendors' first-party browser ids — `_fbp`/`_fbc` (Meta), `_ttp` and
|
|
42
|
+
* `ttclid` (TikTok), `_epik` (Pinterest) — which are what let a vendor match a
|
|
43
|
+
* server event to the visit it came from. Nothing at all is carried for a
|
|
44
|
+
* visitor whose record does not grant advertising, or whose browser sends GPC:
|
|
45
|
+
* {@link advertisingConsentWire} answers `null` and the request goes without.
|
|
46
|
+
*/ /** The request-body field the wire travels in. */ export const ADVERTISING_CONSENT_FIELD = 'adConsent';
|
|
47
|
+
/** Cookie name → wire key, for the ids above. */ const BROWSER_ID_COOKIES = [
|
|
48
|
+
[
|
|
49
|
+
'_fbp',
|
|
50
|
+
'fbp'
|
|
51
|
+
],
|
|
52
|
+
[
|
|
53
|
+
'_fbc',
|
|
54
|
+
'fbc'
|
|
55
|
+
],
|
|
56
|
+
[
|
|
57
|
+
'_ttp',
|
|
58
|
+
'ttp'
|
|
59
|
+
],
|
|
60
|
+
[
|
|
61
|
+
'ttclid',
|
|
62
|
+
'ttclid'
|
|
63
|
+
],
|
|
64
|
+
[
|
|
65
|
+
'_epik',
|
|
66
|
+
'epik'
|
|
67
|
+
]
|
|
68
|
+
];
|
|
69
|
+
const BROWSER_ID = /^[A-Za-z0-9._~:+/=-]{1,256}$/;
|
|
70
|
+
const LEAD_KEY = /^[A-Za-z0-9_.:-]{1,120}$/;
|
|
71
|
+
function readCookie(name) {
|
|
72
|
+
if (typeof document === 'undefined') return null;
|
|
73
|
+
try {
|
|
74
|
+
var _document_cookie;
|
|
75
|
+
for (const part of String((_document_cookie = document.cookie) != null ? _document_cookie : '').split(';')){
|
|
76
|
+
const at = part.indexOf('=');
|
|
77
|
+
if (at < 0) continue;
|
|
78
|
+
if (part.slice(0, at).trim() !== name) continue;
|
|
79
|
+
const value = decodeURIComponent(part.slice(at + 1).trim());
|
|
80
|
+
return BROWSER_ID.test(value) ? value : null;
|
|
81
|
+
}
|
|
82
|
+
} catch (unused) {
|
|
83
|
+
// An unreadable jar carries no ids.
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
function pageUrl() {
|
|
88
|
+
if (typeof window === 'undefined') return undefined;
|
|
89
|
+
try {
|
|
90
|
+
// Origin and path only: a query string is where a token or an address
|
|
91
|
+
// ends up, and a vendor needs neither to know which page converted.
|
|
92
|
+
return `${window.location.origin}${window.location.pathname}`.slice(0, 500);
|
|
93
|
+
} catch (unused) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The wire for this visitor on this site, or `null` when there is nothing to
|
|
99
|
+
* carry: no record, a record that does not grant advertising, or a browser
|
|
100
|
+
* sending Global Privacy Control. Browser only.
|
|
101
|
+
*
|
|
102
|
+
* `options.lead` is the id a form minted for this submission, so the server
|
|
103
|
+
* reports the lead under the same event id the pixel did.
|
|
104
|
+
*/ export function advertisingConsentWire(hostId, options = {}) {
|
|
105
|
+
var _options_lead, _stored_country;
|
|
106
|
+
if (!hostId || typeof window === 'undefined') return null;
|
|
107
|
+
if (hasGlobalPrivacyControl()) return null;
|
|
108
|
+
const stored = readStoredVisitorConsent(hostId);
|
|
109
|
+
if (!stored || stored.advertising !== true) return null;
|
|
110
|
+
const ids = {};
|
|
111
|
+
for (const [cookie, key] of BROWSER_ID_COOKIES){
|
|
112
|
+
const value = readCookie(cookie);
|
|
113
|
+
if (value) ids[key] = value;
|
|
114
|
+
}
|
|
115
|
+
const lead = String((_options_lead = options.lead) != null ? _options_lead : '');
|
|
116
|
+
const url = pageUrl();
|
|
117
|
+
return _extends({
|
|
118
|
+
v: 1,
|
|
119
|
+
status: stored.status,
|
|
120
|
+
advertising: true,
|
|
121
|
+
at: stored.at,
|
|
122
|
+
country: (_stored_country = stored.country) != null ? _stored_country : null
|
|
123
|
+
}, LEAD_KEY.test(lead) ? {
|
|
124
|
+
lead
|
|
125
|
+
} : {}, url ? {
|
|
126
|
+
url
|
|
127
|
+
} : {}, {
|
|
128
|
+
ids
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
/** The wire as a request body field: `{ adConsent }` or nothing at all. */ export function advertisingConsentField(hostId, options = {}) {
|
|
132
|
+
const wire = advertisingConsentWire(hostId, options);
|
|
133
|
+
return wire ? {
|
|
134
|
+
[ADVERTISING_CONSENT_FIELD]: wire
|
|
135
|
+
} : {};
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* A wire from a request body, validated, or `null`. Untrusted: every field is
|
|
139
|
+
* re-checked, the record is re-derived through the browser's own parser (so a
|
|
140
|
+
* hand-edited grant counts for exactly what its status allows), and anything
|
|
141
|
+
* malformed is dropped rather than repaired.
|
|
142
|
+
*/ export function readAdvertisingConsentWire(raw) {
|
|
143
|
+
var _body_ids, _stored_country;
|
|
144
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
145
|
+
const body = raw;
|
|
146
|
+
const stored = parseStoredVisitorConsent(JSON.stringify({
|
|
147
|
+
v: body['v'],
|
|
148
|
+
status: body['status'],
|
|
149
|
+
advertising: body['advertising'],
|
|
150
|
+
at: body['at'],
|
|
151
|
+
country: body['country']
|
|
152
|
+
}));
|
|
153
|
+
if (!stored) return null;
|
|
154
|
+
const ids = {};
|
|
155
|
+
const rawIds = (_body_ids = body['ids']) != null ? _body_ids : {};
|
|
156
|
+
for (const [, key] of BROWSER_ID_COOKIES){
|
|
157
|
+
const value = rawIds == null ? void 0 : rawIds[key];
|
|
158
|
+
if (typeof value === 'string' && BROWSER_ID.test(value)) ids[key] = value;
|
|
159
|
+
}
|
|
160
|
+
const lead = typeof body['lead'] === 'string' && LEAD_KEY.test(body['lead']) ? body['lead'] : null;
|
|
161
|
+
let url = null;
|
|
162
|
+
if (typeof body['url'] === 'string' && body['url'].length <= 500) {
|
|
163
|
+
try {
|
|
164
|
+
const parsed = new URL(body['url']);
|
|
165
|
+
if (parsed.protocol === 'https:' || parsed.protocol === 'http:') {
|
|
166
|
+
url = `${parsed.origin}${parsed.pathname}`;
|
|
167
|
+
}
|
|
168
|
+
} catch (unused) {
|
|
169
|
+
url = null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return _extends({
|
|
173
|
+
v: 1,
|
|
174
|
+
status: stored.status,
|
|
175
|
+
advertising: stored.advertising === true,
|
|
176
|
+
at: stored.at,
|
|
177
|
+
country: (_stored_country = stored.country) != null ? _stored_country : null
|
|
178
|
+
}, lead ? {
|
|
179
|
+
lead
|
|
180
|
+
} : {}, url ? {
|
|
181
|
+
url
|
|
182
|
+
} : {}, {
|
|
183
|
+
ids
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* The server's verdict: may an advertising event be sent for the visitor this
|
|
188
|
+
* wire came from, on this site? The browser gate's own conditions, asked of
|
|
189
|
+
* the record the visitor carried:
|
|
190
|
+
*
|
|
191
|
+
* - the site runs our consent tool and has something to ask about
|
|
192
|
+
* ({@link hostConsentRequired}) — a site on its own CMP has no answer of
|
|
193
|
+
* ours, and no answer is no;
|
|
194
|
+
* - the site asks about advertising and the record grants it
|
|
195
|
+
* ({@link advertisingGrantedByRecord}), which carries every regional rule
|
|
196
|
+
* and every refusal the browser applies;
|
|
197
|
+
* - the request did not carry Global Privacy Control (`Sec-GPC: 1`), which
|
|
198
|
+
* outranks any record.
|
|
199
|
+
*/ export function advertisingConsentGranted(host, wire, request) {
|
|
200
|
+
if (!wire || request.gpc) return false;
|
|
201
|
+
if (!hostConsentRequired(host)) return false;
|
|
202
|
+
return advertisingGrantedByRecord(host, {
|
|
203
|
+
v: 1,
|
|
204
|
+
at: wire.at,
|
|
205
|
+
status: wire.status,
|
|
206
|
+
analytics: false,
|
|
207
|
+
advertising: wire.advertising === true,
|
|
208
|
+
country: wire.country
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
/** Whether a request's headers carry Global Privacy Control. */ export function requestSendsGlobalPrivacyControl(get) {
|
|
212
|
+
var _get;
|
|
213
|
+
return String((_get = get('sec-gpc')) != null ? _get : '').trim() === '1';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
//# sourceMappingURL=advertising-consent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/advertising-consent.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 advertisingGrantedByRecord,\n hasGlobalPrivacyControl,\n hostConsentRequired,\n parseStoredVisitorConsent,\n readStoredVisitorConsent,\n type StoredVisitorConsent,\n type VisitorConsentHost,\n} from './visitor-consent'\n\n/**\n * A visitor's advertising consent, carried to the SERVER (AGL-3694).\n *\n * ## Why the server needs it, and why it cannot look it up\n *\n * A Conversions API event is sent by a server, often long after the visitor\n * left: a purchase is reported when Stripe's webhook lands, not when the\n * shopper clicked Pay. Consent on a published site is recorded in the\n * visitor's own browser (`visitor-consent.ts`) — ISR-cached pages cannot vary\n * by visitor, so there is no server-side copy to consult later. The only\n * moment the server can learn what this visitor decided is a request the\n * visitor makes: the checkout they start, the form they submit.\n *\n * So those requests carry the record as it stands, and the server decides\n * again from it — with the site's own consent settings and the request's own\n * Global Privacy Control header — through the SAME predicate the browser gate\n * uses ({@link advertisingConsentGranted}). The verdict is recorded with the\n * order or the lead it was given for, and an event whose record says no, or\n * that has no record at all, is never sent: unknown is no.\n *\n * ## What it carries\n *\n * The record's own fields (status, the advertising grant, when, the country),\n * and the vendors' first-party browser ids — `_fbp`/`_fbc` (Meta), `_ttp` and\n * `ttclid` (TikTok), `_epik` (Pinterest) — which are what let a vendor match a\n * server event to the visit it came from. Nothing at all is carried for a\n * visitor whose record does not grant advertising, or whose browser sends GPC:\n * {@link advertisingConsentWire} answers `null` and the request goes without.\n */\n\n/** The request-body field the wire travels in. */\nexport const ADVERTISING_CONSENT_FIELD = 'adConsent'\n\n/** The vendor browser ids a server event may carry, by the vendor's own name for each. */\nexport interface AdvertisingBrowserIds {\n fbp?: string\n fbc?: string\n ttp?: string\n ttclid?: string\n epik?: string\n}\n\nexport interface AdvertisingConsentWire {\n v: 1\n status: StoredVisitorConsent['status']\n advertising: boolean\n at: number\n country: string | null\n /** The id the browser sent a lead's pixel event under; see `advertising-events.ts`. */\n lead?: string\n /** The page the event happened on, origin and path only. */\n url?: string\n ids: AdvertisingBrowserIds\n}\n\n/** Cookie name → wire key, for the ids above. */\nconst BROWSER_ID_COOKIES: ReadonlyArray<[string, keyof AdvertisingBrowserIds]> = [\n ['_fbp', 'fbp'],\n ['_fbc', 'fbc'],\n ['_ttp', 'ttp'],\n ['ttclid', 'ttclid'],\n ['_epik', 'epik'],\n]\n\nconst BROWSER_ID = /^[A-Za-z0-9._~:+/=-]{1,256}$/\nconst LEAD_KEY = /^[A-Za-z0-9_.:-]{1,120}$/\n\nfunction readCookie(name: string): string | null {\n if (typeof document === 'undefined') return null\n try {\n for (const part of String(document.cookie ?? '').split(';')) {\n const at = part.indexOf('=')\n if (at < 0) continue\n if (part.slice(0, at).trim() !== name) continue\n const value = decodeURIComponent(part.slice(at + 1).trim())\n return BROWSER_ID.test(value) ? value : null\n }\n } catch {\n // An unreadable jar carries no ids.\n }\n return null\n}\n\nfunction pageUrl(): string | undefined {\n if (typeof window === 'undefined') return undefined\n try {\n // Origin and path only: a query string is where a token or an address\n // ends up, and a vendor needs neither to know which page converted.\n return `${window.location.origin}${window.location.pathname}`.slice(0, 500)\n } catch {\n return undefined\n }\n}\n\n/**\n * The wire for this visitor on this site, or `null` when there is nothing to\n * carry: no record, a record that does not grant advertising, or a browser\n * sending Global Privacy Control. Browser only.\n *\n * `options.lead` is the id a form minted for this submission, so the server\n * reports the lead under the same event id the pixel did.\n */\nexport function advertisingConsentWire(\n hostId: string | null | undefined,\n options: { lead?: string | null } = {},\n): AdvertisingConsentWire | null {\n if (!hostId || typeof window === 'undefined') return null\n if (hasGlobalPrivacyControl()) return null\n const stored = readStoredVisitorConsent(hostId)\n if (!stored || stored.advertising !== true) return null\n const ids: AdvertisingBrowserIds = {}\n for (const [cookie, key] of BROWSER_ID_COOKIES) {\n const value = readCookie(cookie)\n if (value) ids[key] = value\n }\n const lead = String(options.lead ?? '')\n const url = pageUrl()\n return {\n v: 1,\n status: stored.status,\n advertising: true,\n at: stored.at,\n country: stored.country ?? null,\n ...(LEAD_KEY.test(lead) ? { lead } : {}),\n ...(url ? { url } : {}),\n ids,\n }\n}\n\n/** The wire as a request body field: `{ adConsent }` or nothing at all. */\nexport function advertisingConsentField(\n hostId: string | null | undefined,\n options: { lead?: string | null } = {},\n): { adConsent?: AdvertisingConsentWire } {\n const wire = advertisingConsentWire(hostId, options)\n return wire ? { [ADVERTISING_CONSENT_FIELD]: wire } : {}\n}\n\n/**\n * A wire from a request body, validated, or `null`. Untrusted: every field is\n * re-checked, the record is re-derived through the browser's own parser (so a\n * hand-edited grant counts for exactly what its status allows), and anything\n * malformed is dropped rather than repaired.\n */\nexport function readAdvertisingConsentWire(raw: unknown): AdvertisingConsentWire | null {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null\n const body = raw as Record<string, unknown>\n const stored = parseStoredVisitorConsent(\n JSON.stringify({\n v: body['v'],\n status: body['status'],\n advertising: body['advertising'],\n at: body['at'],\n country: body['country'],\n }),\n )\n if (!stored) return null\n const ids: AdvertisingBrowserIds = {}\n const rawIds = (body['ids'] ?? {}) as Record<string, unknown>\n for (const [, key] of BROWSER_ID_COOKIES) {\n const value = rawIds?.[key]\n if (typeof value === 'string' && BROWSER_ID.test(value)) ids[key] = value\n }\n const lead = typeof body['lead'] === 'string' && LEAD_KEY.test(body['lead']) ? body['lead'] : null\n let url: string | null = null\n if (typeof body['url'] === 'string' && body['url'].length <= 500) {\n try {\n const parsed = new URL(body['url'])\n if (parsed.protocol === 'https:' || parsed.protocol === 'http:') {\n url = `${parsed.origin}${parsed.pathname}`\n }\n } catch {\n url = null\n }\n }\n return {\n v: 1,\n status: stored.status,\n advertising: stored.advertising === true,\n at: stored.at,\n country: stored.country ?? null,\n ...(lead ? { lead } : {}),\n ...(url ? { url } : {}),\n ids,\n }\n}\n\n/**\n * The server's verdict: may an advertising event be sent for the visitor this\n * wire came from, on this site? The browser gate's own conditions, asked of\n * the record the visitor carried:\n *\n * - the site runs our consent tool and has something to ask about\n * ({@link hostConsentRequired}) — a site on its own CMP has no answer of\n * ours, and no answer is no;\n * - the site asks about advertising and the record grants it\n * ({@link advertisingGrantedByRecord}), which carries every regional rule\n * and every refusal the browser applies;\n * - the request did not carry Global Privacy Control (`Sec-GPC: 1`), which\n * outranks any record.\n */\nexport function advertisingConsentGranted(\n host: VisitorConsentHost | null | undefined,\n wire: AdvertisingConsentWire | null | undefined,\n request: { gpc: boolean },\n): boolean {\n if (!wire || request.gpc) return false\n if (!hostConsentRequired(host)) return false\n return advertisingGrantedByRecord(host, {\n v: 1,\n at: wire.at,\n status: wire.status,\n analytics: false,\n advertising: wire.advertising === true,\n country: wire.country,\n })\n}\n\n/** Whether a request's headers carry Global Privacy Control. */\nexport function requestSendsGlobalPrivacyControl(\n get: (name: string) => string | null | undefined,\n): boolean {\n return String(get('sec-gpc') ?? '').trim() === '1'\n}\n"],"names":["advertisingGrantedByRecord","hasGlobalPrivacyControl","hostConsentRequired","parseStoredVisitorConsent","readStoredVisitorConsent","ADVERTISING_CONSENT_FIELD","BROWSER_ID_COOKIES","BROWSER_ID","LEAD_KEY","readCookie","name","document","part","String","cookie","split","at","indexOf","slice","trim","value","decodeURIComponent","test","pageUrl","window","undefined","location","origin","pathname","advertisingConsentWire","hostId","options","stored","advertising","ids","key","lead","url","v","status","country","advertisingConsentField","wire","readAdvertisingConsentWire","raw","body","Array","isArray","JSON","stringify","rawIds","length","parsed","URL","protocol","advertisingConsentGranted","host","request","gpc","analytics","requestSendsGlobalPrivacyControl","get"],"mappings":"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"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Conversion events for a site's OWN advertising tags (AGL-3694).
|
|
19
|
+
*
|
|
20
|
+
* ## What this is
|
|
21
|
+
*
|
|
22
|
+
* The analytics taxonomy (`analytics-events.ts`) already says what a visitor
|
|
23
|
+
* did — `purchase`, `generate_lead`, `begin_checkout`, `add_to_cart`,
|
|
24
|
+
* `view_item` — and delivers it to the site's Google tag. A merchant who runs
|
|
25
|
+
* a Meta pixel, a TikTok pixel or a Pinterest tag wants the same moments in
|
|
26
|
+
* those accounts, under each vendor's own standard event name. This module is
|
|
27
|
+
* the translation and the delivery, and nothing else: it never loads a tag.
|
|
28
|
+
*
|
|
29
|
+
* ## Why it cannot fire before consent
|
|
30
|
+
*
|
|
31
|
+
* It sends only to a tag that is ALREADY IN THE DOCUMENT and carries both of
|
|
32
|
+
* this module's marks: {@link ADVERTISING_TAG_ATTRIBUTE}, which only
|
|
33
|
+
* `advertising-tag-mounts.tsx` writes and only where `resolveAdvertisingTags`
|
|
34
|
+
* said the visitor granted advertising, and {@link ADVERTISING_EVENTS_ATTRIBUTE},
|
|
35
|
+
* which the tenant writes only on a merchant's own site. No mark, no call: a
|
|
36
|
+
* visitor who did not grant has no tag, and an event raised for them goes
|
|
37
|
+
* nowhere, exactly as the Google path drops one when `gtag` is absent. A
|
|
38
|
+
* pixel a merchant pasted into Custom HTML carries neither mark and is never
|
|
39
|
+
* called — it runs on a basis that is not ours.
|
|
40
|
+
*
|
|
41
|
+
* Aglyn's own surfaces carry the first mark and never the second, so their
|
|
42
|
+
* tags keep reporting only the page views they always have.
|
|
43
|
+
*
|
|
44
|
+
* ## The event id, and why both halves derive it
|
|
45
|
+
*
|
|
46
|
+
* A purchase or a lead can reach a vendor twice: from this browser call and
|
|
47
|
+
* from the server's Conversions API (the ad-conversions plugin). Each vendor
|
|
48
|
+
* de-duplicates the pair by an event id the two sides share, so the id is
|
|
49
|
+
* DERIVED rather than minted wherever both sides can know the same key:
|
|
50
|
+
* {@link advertisingEventId} turns a purchase's transaction id, or a lead's
|
|
51
|
+
* id, into the one string both send.
|
|
52
|
+
*
|
|
53
|
+
* Kept free of the vendor descriptors on purpose: the console imports the
|
|
54
|
+
* analytics taxonomy, which imports this, and the console may not carry the
|
|
55
|
+
* module that mounts a vendor's script.
|
|
56
|
+
*/
|
|
57
|
+
/**
|
|
58
|
+
* The attribute every script element the advertising gate renders carries; its
|
|
59
|
+
* value is the vendor id. Declared here and re-exported by
|
|
60
|
+
* `advertising-tags.ts`, so the event delivery below can find a tag without
|
|
61
|
+
* importing the module that mounts one.
|
|
62
|
+
*/
|
|
63
|
+
export declare const ADVERTISING_TAG_ATTRIBUTE = "data-aglyn-ad-tag";
|
|
64
|
+
/**
|
|
65
|
+
* The second mark (AGL-3694): this tag is a SITE OWNER's, mounted on their own
|
|
66
|
+
* site, and takes the site's conversion events. Aglyn's own surfaces never
|
|
67
|
+
* write it.
|
|
68
|
+
*/
|
|
69
|
+
export declare const ADVERTISING_EVENTS_ATTRIBUTE = "data-aglyn-ad-events";
|
|
70
|
+
/** The conversions a server also reports, and so share an id with it. */
|
|
71
|
+
export type AdvertisingEventKind = 'purchase' | 'lead';
|
|
72
|
+
/**
|
|
73
|
+
* The event id the browser tag and the server's Conversions API both send for
|
|
74
|
+
* one conversion, or `null` for a key that cannot be one. `purchase` takes the
|
|
75
|
+
* order's transaction id (the Stripe Checkout Session id the order is stored
|
|
76
|
+
* under); `lead` takes the id the form minted for its submission.
|
|
77
|
+
*/
|
|
78
|
+
export declare function advertisingEventId(kind: AdvertisingEventKind, key: string | null | undefined): string | null;
|
|
79
|
+
/** A fresh id for a lead, minted where the form is submitted. */
|
|
80
|
+
export declare function mintAdvertisingLeadKey(): string;
|
|
81
|
+
type Params = Record<string, unknown>;
|
|
82
|
+
/** The vendors that take conversion events, by `analytics.adTags` key. */
|
|
83
|
+
export declare const ADVERTISING_EVENT_VENDORS: readonly string[];
|
|
84
|
+
/** Whether any merchant tag that takes conversion events is in the document. */
|
|
85
|
+
export declare function merchantAdvertisingTagsResident(): boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Sends one taxonomy event to every merchant tag in the document that takes
|
|
88
|
+
* it, and answers the vendor ids it reached. Never throws and never queues:
|
|
89
|
+
* with no marked tag — the visitor did not grant advertising, or the site runs
|
|
90
|
+
* none — it does nothing at all.
|
|
91
|
+
*
|
|
92
|
+
* `options.eventId` is the shared id for a conversion the server also reports
|
|
93
|
+
* (a lead); a purchase derives its own from `transaction_id`.
|
|
94
|
+
*/
|
|
95
|
+
export declare function sendAdvertisingEvent(name: string, params: Params, options?: {
|
|
96
|
+
eventId?: string | null;
|
|
97
|
+
}): string[];
|
|
98
|
+
export {};
|