@aglyn/aglyn 1.0.0-beta.229 → 1.0.0-beta.230

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/analytics-events.d.ts +18 -0
  3. package/src/lib/app-utils/analytics-events.js +2 -0
  4. package/src/lib/app-utils/analytics-events.js.map +1 -1
  5. package/src/lib/app-utils/docs-help.generated.d.ts +31 -7
  6. package/src/lib/app-utils/docs-help.generated.js +63 -2
  7. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  8. package/src/lib/app-utils/docs-index.generated.js +295 -43
  9. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  10. package/src/lib/app-utils/plan-entitlements.js +20 -0
  11. package/src/lib/app-utils/plan-entitlements.js.map +1 -1
  12. package/src/lib/app-utils/plugin-host-events.generated.d.ts +1 -1
  13. package/src/lib/app-utils/plugin-host-events.generated.js +15 -0
  14. package/src/lib/app-utils/plugin-host-events.generated.js.map +1 -1
  15. package/src/lib/app-utils/site-journey.d.ts +143 -0
  16. package/src/lib/app-utils/site-journey.js +282 -0
  17. package/src/lib/app-utils/site-journey.js.map +1 -0
  18. package/src/lib/foundation/definitions/org-billing.types.d.ts +16 -0
  19. package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
  20. package/src/lib/plugin-manager/feature-plugins.d.ts +85 -0
  21. package/src/lib/plugin-manager/feature-plugins.js +18 -1
  22. package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
  23. package/src/lib/plugin-manager/first-party-plugins.generated.js +29 -0
  24. package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
  25. package/src/lib/plugin-manager/plugin-ai-capabilities.d.ts +192 -0
  26. package/src/lib/plugin-manager/plugin-ai-capabilities.js +157 -0
  27. package/src/lib/plugin-manager/plugin-ai-capabilities.js.map +1 -0
  28. package/src/lib/plugin-manager/plugin-events.d.ts +21 -0
  29. package/src/lib/plugin-manager/plugin-events.js.map +1 -1
@@ -8,5 +8,5 @@
8
8
  */
9
9
  import type { HostEventDeclaration } from './host-events';
10
10
  /** Every host event a plugin declares. */
11
- export type PluginHostEventType = 'formSubmission' | 'memberSignUp' | 'memberSignIn' | 'memberSignOut' | 'lead' | 'booking' | 'taskCompleted' | 'contactCreated' | 'contactStageChanged' | 'dealStageChanged' | 'dealWon' | 'dealLost';
11
+ export type PluginHostEventType = 'formSubmission' | 'memberSignUp' | 'memberSignIn' | 'memberSignOut' | 'lead' | 'booking' | 'taskCompleted' | 'contactCreated' | 'contactStageChanged' | 'dealStageChanged' | 'dealWon' | 'dealLost' | 'funnelLeft';
12
12
  export declare const PLUGIN_HOST_EVENTS: readonly HostEventDeclaration[];
@@ -172,6 +172,21 @@
172
172
  "companyId",
173
173
  "lostReason"
174
174
  ]
175
+ },
176
+ {
177
+ "pluginId": "funnels",
178
+ "type": "funnelLeft",
179
+ "order": 150,
180
+ "label": "Left a funnel",
181
+ "payloadKeys": [
182
+ "funnelId",
183
+ "funnelName",
184
+ "step",
185
+ "stepLabel",
186
+ "nextStepLabel",
187
+ "afterHours",
188
+ "email"
189
+ ]
175
190
  }
176
191
  ];
177
192
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/plugin-host-events.generated.ts"],"sourcesContent":["/**\n * GENERATED FILE — do not edit. Regenerate with:\n * node tools/scripts/generate-plugin-manifests.mjs\n *\n * The host events plugins declare (AGL-3080): each plugin's `hostEvents`\n * in plugins.config.json, in `order`. Core's `host-events.ts` folds them\n * in beside the platform's own.\n */\n\nimport type { HostEventDeclaration } from './host-events'\n\n/** Every host event a plugin declares. */\nexport type PluginHostEventType =\n | 'formSubmission'\n | 'memberSignUp'\n | 'memberSignIn'\n | 'memberSignOut'\n | 'lead'\n | 'booking'\n | 'taskCompleted'\n | 'contactCreated'\n | 'contactStageChanged'\n | 'dealStageChanged'\n | 'dealWon'\n | 'dealLost'\n\nexport const PLUGIN_HOST_EVENTS: readonly HostEventDeclaration[] = [\n {\n \"pluginId\": \"forms\",\n \"type\": \"formSubmission\",\n \"order\": 10,\n \"label\": \"Form submitted\",\n \"payloadKeys\": [\n \"formId\",\n \"formName\",\n \"path\",\n \"and every submitted field by name\"\n ],\n \"recipientActed\": true\n },\n {\n \"pluginId\": \"commerce\",\n \"type\": \"memberSignUp\",\n \"order\": 30,\n \"label\": \"Member signed up\",\n \"payloadKeys\": [\n \"email\"\n ],\n \"recipientActed\": true\n },\n {\n \"pluginId\": \"commerce\",\n \"type\": \"memberSignIn\",\n \"order\": 40,\n \"label\": \"Member signed in\",\n \"payloadKeys\": [\n \"email\"\n ]\n },\n {\n \"pluginId\": \"commerce\",\n \"type\": \"memberSignOut\",\n \"order\": 50,\n \"label\": \"Member signed out\"\n },\n {\n \"pluginId\": \"crm\",\n \"type\": \"lead\",\n \"order\": 60,\n \"label\": \"New lead\",\n \"payloadKeys\": [\n \"leadId\",\n \"email\",\n \"name\",\n \"source\",\n \"hostId\",\n \"formId\",\n \"campaignIds\"\n ],\n \"recipientActed\": true\n },\n {\n \"pluginId\": \"bookings\",\n \"type\": \"booking\",\n \"order\": 70,\n \"label\": \"New booking\",\n \"payloadKeys\": [\n \"serviceName\",\n \"email\",\n \"startsAtMs\"\n ],\n \"recipientActed\": true\n },\n {\n \"pluginId\": \"crm\",\n \"type\": \"taskCompleted\",\n \"order\": 80,\n \"label\": \"CRM task completed\",\n \"payloadKeys\": [\n \"taskId\",\n \"title\",\n \"kind\",\n \"priority\",\n \"dueAtMs\",\n \"completedAtMs\",\n \"completedByUid\",\n \"assigneeUid\",\n \"createdByUid\",\n \"contactId\",\n \"companyId\",\n \"dealId\",\n \"taskHostId\"\n ]\n },\n {\n \"pluginId\": \"crm\",\n \"type\": \"contactCreated\",\n \"order\": 90,\n \"label\": \"Contact created\",\n \"payloadKeys\": [\n \"contactId\",\n \"email\",\n \"name\",\n \"source\",\n \"hostId\",\n \"lifecycleStage\",\n \"campaignIds\",\n \"formId\"\n ]\n },\n {\n \"pluginId\": \"crm\",\n \"type\": \"contactStageChanged\",\n \"order\": 100,\n \"label\": \"Contact changed stage\",\n \"payloadKeys\": [\n \"contactId\",\n \"email\",\n \"lifecycleStage\",\n \"previousStage\"\n ]\n },\n {\n \"pluginId\": \"crm\",\n \"type\": \"dealStageChanged\",\n \"order\": 110,\n \"label\": \"Deal moved\",\n \"payloadKeys\": [\n \"dealId\",\n \"title\",\n \"amountCents\",\n \"currency\",\n \"stageId\",\n \"previousStageId\",\n \"ownerUid\",\n \"contactId\",\n \"companyId\"\n ]\n },\n {\n \"pluginId\": \"crm\",\n \"type\": \"dealWon\",\n \"order\": 120,\n \"label\": \"Deal won\",\n \"payloadKeys\": [\n \"dealId\",\n \"title\",\n \"amountCents\",\n \"currency\",\n \"stageId\",\n \"previousStageId\",\n \"ownerUid\",\n \"contactId\",\n \"companyId\"\n ]\n },\n {\n \"pluginId\": \"crm\",\n \"type\": \"dealLost\",\n \"order\": 130,\n \"label\": \"Deal lost\",\n \"payloadKeys\": [\n \"dealId\",\n \"title\",\n \"amountCents\",\n \"currency\",\n \"stageId\",\n \"previousStageId\",\n \"ownerUid\",\n \"contactId\",\n \"companyId\",\n \"lostReason\"\n ]\n }\n]\n"],"names":["PLUGIN_HOST_EVENTS"],"mappings":"AAAA;;;;;;;CAOC,GAmBD,OAAO,MAAMA,qBAAsD;IACjE;QACE,YAAY;QACZ,QAAQ;QACR,SAAS;QACT,SAAS;QACT,eAAe;YACb;YACA;YACA;YACA;SACD;QACD,kBAAkB;IACpB;IACA;QACE,YAAY;QACZ,QAAQ;QACR,SAAS;QACT,SAAS;QACT,eAAe;YACb;SACD;QACD,kBAAkB;IACpB;IACA;QACE,YAAY;QACZ,QAAQ;QACR,SAAS;QACT,SAAS;QACT,eAAe;YACb;SACD;IACH;IACA;QACE,YAAY;QACZ,QAAQ;QACR,SAAS;QACT,SAAS;IACX;IACA;QACE,YAAY;QACZ,QAAQ;QACR,SAAS;QACT,SAAS;QACT,eAAe;YACb;YACA;YACA;YACA;YACA;YACA;YACA;SACD;QACD,kBAAkB;IACpB;IACA;QACE,YAAY;QACZ,QAAQ;QACR,SAAS;QACT,SAAS;QACT,eAAe;YACb;YACA;YACA;SACD;QACD,kBAAkB;IACpB;IACA;QACE,YAAY;QACZ,QAAQ;QACR,SAAS;QACT,SAAS;QACT,eAAe;YACb;YACA;YACA;YACA;YACA;YACA;YACA;YACA;YACA;YACA;YACA;YACA;YACA;SACD;IACH;IACA;QACE,YAAY;QACZ,QAAQ;QACR,SAAS;QACT,SAAS;QACT,eAAe;YACb;YACA;YACA;YACA;YACA;YACA;YACA;YACA;SACD;IACH;IACA;QACE,YAAY;QACZ,QAAQ;QACR,SAAS;QACT,SAAS;QACT,eAAe;YACb;YACA;YACA;YACA;SACD;IACH;IACA;QACE,YAAY;QACZ,QAAQ;QACR,SAAS;QACT,SAAS;QACT,eAAe;YACb;YACA;YACA;YACA;YACA;YACA;YACA;YACA;YACA;SACD;IACH;IACA;QACE,YAAY;QACZ,QAAQ;QACR,SAAS;QACT,SAAS;QACT,eAAe;YACb;YACA;YACA;YACA;YACA;YACA;YACA;YACA;YACA;SACD;IACH;IACA;QACE,YAAY;QACZ,QAAQ;QACR,SAAS;QACT,SAAS;QACT,eAAe;YACb;YACA;YACA;YACA;YACA;YACA;YACA;YACA;YACA;YACA;SACD;IACH;CACD,CAAA"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/plugin-host-events.generated.ts"],"sourcesContent":["/**\n * GENERATED FILE — do not edit. Regenerate with:\n * node tools/scripts/generate-plugin-manifests.mjs\n *\n * The host events plugins declare (AGL-3080): each plugin's `hostEvents`\n * in plugins.config.json, in `order`. Core's `host-events.ts` folds them\n * in beside the platform's own.\n */\n\nimport type { HostEventDeclaration } from './host-events'\n\n/** Every host event a plugin declares. */\nexport type PluginHostEventType =\n | 'formSubmission'\n | 'memberSignUp'\n | 'memberSignIn'\n | 'memberSignOut'\n | 'lead'\n | 'booking'\n | 'taskCompleted'\n | 'contactCreated'\n | 'contactStageChanged'\n | 'dealStageChanged'\n | 'dealWon'\n | 'dealLost'\n | 'funnelLeft'\n\nexport const PLUGIN_HOST_EVENTS: readonly HostEventDeclaration[] = [\n {\n \"pluginId\": \"forms\",\n \"type\": \"formSubmission\",\n \"order\": 10,\n \"label\": \"Form submitted\",\n \"payloadKeys\": [\n \"formId\",\n \"formName\",\n \"path\",\n \"and every submitted field by name\"\n ],\n \"recipientActed\": true\n },\n {\n \"pluginId\": \"commerce\",\n \"type\": \"memberSignUp\",\n \"order\": 30,\n \"label\": \"Member signed up\",\n \"payloadKeys\": [\n \"email\"\n ],\n \"recipientActed\": true\n },\n {\n \"pluginId\": \"commerce\",\n \"type\": \"memberSignIn\",\n \"order\": 40,\n \"label\": \"Member signed in\",\n \"payloadKeys\": [\n \"email\"\n ]\n },\n {\n \"pluginId\": \"commerce\",\n \"type\": \"memberSignOut\",\n \"order\": 50,\n \"label\": \"Member signed out\"\n },\n {\n \"pluginId\": \"crm\",\n \"type\": \"lead\",\n \"order\": 60,\n \"label\": \"New lead\",\n \"payloadKeys\": [\n \"leadId\",\n \"email\",\n \"name\",\n \"source\",\n \"hostId\",\n \"formId\",\n \"campaignIds\"\n ],\n \"recipientActed\": true\n },\n {\n \"pluginId\": \"bookings\",\n \"type\": \"booking\",\n \"order\": 70,\n \"label\": \"New booking\",\n \"payloadKeys\": [\n \"serviceName\",\n \"email\",\n \"startsAtMs\"\n ],\n \"recipientActed\": true\n },\n {\n \"pluginId\": \"crm\",\n \"type\": \"taskCompleted\",\n \"order\": 80,\n \"label\": \"CRM task completed\",\n \"payloadKeys\": [\n \"taskId\",\n \"title\",\n \"kind\",\n \"priority\",\n \"dueAtMs\",\n \"completedAtMs\",\n \"completedByUid\",\n \"assigneeUid\",\n \"createdByUid\",\n \"contactId\",\n \"companyId\",\n \"dealId\",\n \"taskHostId\"\n ]\n },\n {\n \"pluginId\": \"crm\",\n \"type\": \"contactCreated\",\n \"order\": 90,\n \"label\": \"Contact created\",\n \"payloadKeys\": [\n \"contactId\",\n \"email\",\n \"name\",\n \"source\",\n \"hostId\",\n \"lifecycleStage\",\n \"campaignIds\",\n \"formId\"\n ]\n },\n {\n \"pluginId\": \"crm\",\n \"type\": \"contactStageChanged\",\n \"order\": 100,\n \"label\": \"Contact changed stage\",\n \"payloadKeys\": [\n \"contactId\",\n \"email\",\n \"lifecycleStage\",\n \"previousStage\"\n ]\n },\n {\n \"pluginId\": \"crm\",\n \"type\": \"dealStageChanged\",\n \"order\": 110,\n \"label\": \"Deal moved\",\n \"payloadKeys\": [\n \"dealId\",\n \"title\",\n \"amountCents\",\n \"currency\",\n \"stageId\",\n \"previousStageId\",\n \"ownerUid\",\n \"contactId\",\n \"companyId\"\n ]\n },\n {\n \"pluginId\": \"crm\",\n \"type\": \"dealWon\",\n \"order\": 120,\n \"label\": \"Deal won\",\n \"payloadKeys\": [\n \"dealId\",\n \"title\",\n \"amountCents\",\n \"currency\",\n \"stageId\",\n \"previousStageId\",\n \"ownerUid\",\n \"contactId\",\n \"companyId\"\n ]\n },\n {\n \"pluginId\": \"crm\",\n \"type\": \"dealLost\",\n \"order\": 130,\n \"label\": \"Deal lost\",\n \"payloadKeys\": [\n \"dealId\",\n \"title\",\n \"amountCents\",\n \"currency\",\n \"stageId\",\n \"previousStageId\",\n \"ownerUid\",\n \"contactId\",\n \"companyId\",\n \"lostReason\"\n ]\n },\n {\n \"pluginId\": \"funnels\",\n \"type\": \"funnelLeft\",\n \"order\": 150,\n \"label\": \"Left a funnel\",\n \"payloadKeys\": [\n \"funnelId\",\n \"funnelName\",\n \"step\",\n \"stepLabel\",\n \"nextStepLabel\",\n \"afterHours\",\n \"email\"\n ]\n }\n]\n"],"names":["PLUGIN_HOST_EVENTS"],"mappings":"AAAA;;;;;;;CAOC,GAoBD,OAAO,MAAMA,qBAAsD;IACjE;QACE,YAAY;QACZ,QAAQ;QACR,SAAS;QACT,SAAS;QACT,eAAe;YACb;YACA;YACA;YACA;SACD;QACD,kBAAkB;IACpB;IACA;QACE,YAAY;QACZ,QAAQ;QACR,SAAS;QACT,SAAS;QACT,eAAe;YACb;SACD;QACD,kBAAkB;IACpB;IACA;QACE,YAAY;QACZ,QAAQ;QACR,SAAS;QACT,SAAS;QACT,eAAe;YACb;SACD;IACH;IACA;QACE,YAAY;QACZ,QAAQ;QACR,SAAS;QACT,SAAS;IACX;IACA;QACE,YAAY;QACZ,QAAQ;QACR,SAAS;QACT,SAAS;QACT,eAAe;YACb;YACA;YACA;YACA;YACA;YACA;YACA;SACD;QACD,kBAAkB;IACpB;IACA;QACE,YAAY;QACZ,QAAQ;QACR,SAAS;QACT,SAAS;QACT,eAAe;YACb;YACA;YACA;SACD;QACD,kBAAkB;IACpB;IACA;QACE,YAAY;QACZ,QAAQ;QACR,SAAS;QACT,SAAS;QACT,eAAe;YACb;YACA;YACA;YACA;YACA;YACA;YACA;YACA;YACA;YACA;YACA;YACA;YACA;SACD;IACH;IACA;QACE,YAAY;QACZ,QAAQ;QACR,SAAS;QACT,SAAS;QACT,eAAe;YACb;YACA;YACA;YACA;YACA;YACA;YACA;YACA;SACD;IACH;IACA;QACE,YAAY;QACZ,QAAQ;QACR,SAAS;QACT,SAAS;QACT,eAAe;YACb;YACA;YACA;YACA;SACD;IACH;IACA;QACE,YAAY;QACZ,QAAQ;QACR,SAAS;QACT,SAAS;QACT,eAAe;YACb;YACA;YACA;YACA;YACA;YACA;YACA;YACA;YACA;SACD;IACH;IACA;QACE,YAAY;QACZ,QAAQ;QACR,SAAS;QACT,SAAS;QACT,eAAe;YACb;YACA;YACA;YACA;YACA;YACA;YACA;YACA;YACA;SACD;IACH;IACA;QACE,YAAY;QACZ,QAAQ;QACR,SAAS;QACT,SAAS;QACT,eAAe;YACb;YACA;YACA;YACA;YACA;YACA;YACA;YACA;YACA;YACA;SACD;IACH;IACA;QACE,YAAY;QACZ,QAAQ;QACR,SAAS;QACT,SAAS;QACT,eAAe;YACb;YACA;YACA;YACA;YACA;YACA;YACA;SACD;IACH;CACD,CAAA"}
@@ -0,0 +1,143 @@
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
+ * A VISIT'S STEPS, IN ORDER (AGL-3605): the one per-visit record the site
19
+ * collector keeps, so a funnel can ask "of the visits that did A, how many
20
+ * then did B".
21
+ *
22
+ * Every other first-party counter is a daily aggregate with no identifier at
23
+ * all (`visit-claim.ts` says why), and an aggregate cannot be ordered: a day
24
+ * with 100 pricing views and 10 sign-ups says nothing about whether the
25
+ * sign-ups viewed pricing. Ordering needs something that ties two steps to one
26
+ * visit, and this module is that and nothing more.
27
+ *
28
+ * ## What identifies a visit
29
+ *
30
+ * A random id held in `sessionStorage`: one browser tab, until it closes. Not
31
+ * a cookie, not `localStorage`, never derived from the address or the
32
+ * browser, never shared between sites (the stored id names the site it was
33
+ * minted for, and another site's page mints its own). Two tabs are two
34
+ * visits; closing the tab and coming back tomorrow is a new visit. Nothing
35
+ * here links a visit to a person: only a door that already verified who the
36
+ * visitor is — a form they submitted — may hand the id to the server beside
37
+ * the address they typed ({@link siteJourneyField}), so a drop-off follow-up
38
+ * reaches a person who identified themselves and never an anonymous visit.
39
+ *
40
+ * ## Consent
41
+ *
42
+ * The id leaves the browser, so it is `analytics` storage and waits for the
43
+ * same grant the site's analytics tag waits for. The page tells this module
44
+ * the verdict on every render ({@link configureSiteJourney}): until it grants
45
+ * — and for a visitor who declines, opts out or sends GPC — nothing is minted
46
+ * and every step is DROPPED, never queued, for the reason
47
+ * `analytics-events.ts` gives. The one step recorded at the moment of a grant
48
+ * is the page the visitor is on right then, which is a step they are taking
49
+ * now, not a replay. Withdrawing the grant deletes the stored id, so a later
50
+ * grant starts a new visit rather than continuing the old one.
51
+ *
52
+ * ## When it runs at all
53
+ *
54
+ * Only on a site that records journeys (`host.funnelRecording === true`,
55
+ * which the plugin that reads them stamps when a site has something to read
56
+ * them for), only on a production surface without the internal-traffic
57
+ * opt-in (the beacon's own gate), and at most {@link SITE_JOURNEY_MAX_STEPS}
58
+ * steps per visit.
59
+ *
60
+ * Kept free of the `@aglyn/aglyn` barrel and of any plugin: the published
61
+ * page's measurement mount imports it (the AGL-1550 independence rule), and
62
+ * plugins report their steps here without knowing who counts them.
63
+ */
64
+ /** The body field that marks a collector beacon as a journey step. */
65
+ export declare const SITE_JOURNEY_BEACON_FIELD = "journey";
66
+ /**
67
+ * What a step can be — only what the platform already sees happen.
68
+ *
69
+ * - `page`: a page was viewed; key = the path.
70
+ * - `form`: a form was submitted successfully; key = the form id.
71
+ * - `booking`: a booking was made (a free one confirmed, or a paid one's
72
+ * payment settled); key = the service id.
73
+ * - `cart`: a product was added to the cart; key = the product id.
74
+ * - `order`: a storefront order was placed and paid; no key.
75
+ * - `overlay`: an announcement bar or popup was clicked; key = the overlay id.
76
+ * - `event`: a custom event an interaction fired; key = the event name.
77
+ * - `email`: a person the visit identified opened or clicked an email the
78
+ * site sent them; key = `opened` or `clicked`. Recorded by the SERVER from
79
+ * the delivery log, never by a page: a browser that sends one is refused.
80
+ */
81
+ export declare const SITE_JOURNEY_STEP_TYPES: readonly ["page", "form", "booking", "cart", "order", "overlay", "event", "email"];
82
+ export type SiteJourneyStepType = (typeof SITE_JOURNEY_STEP_TYPES)[number];
83
+ export declare function isSiteJourneyStepType(value: unknown): value is SiteJourneyStepType;
84
+ /** The step types only the server records; a page's beacon naming one is refused. */
85
+ export declare const SITE_JOURNEY_SERVER_STEP_TYPES: ReadonlySet<SiteJourneyStepType>;
86
+ /** The keys an `email` step carries. */
87
+ export declare const SITE_JOURNEY_EMAIL_KEYS: readonly ["opened", "clicked"];
88
+ /**
89
+ * The body field an identifying door (a form submission) carries the visit
90
+ * id under, so the server can tie the visit to the person the door verified.
91
+ * See {@link siteJourneyField}.
92
+ */
93
+ export declare const SITE_JOURNEY_SUBMISSION_FIELD = "journey";
94
+ /** The most steps one visit records. A visit past it records nothing more. */
95
+ export declare const SITE_JOURNEY_MAX_STEPS = 60;
96
+ /** The longest key a step carries; longer keys are cut. */
97
+ export declare const SITE_JOURNEY_KEY_MAX = 200;
98
+ /** A visit id's shape: 22 URL-safe characters (128 random bits). */
99
+ export declare const SITE_JOURNEY_ID_PATTERN: RegExp;
100
+ /** Whether a host document asks for journeys to be recorded. */
101
+ export declare function hostRecordsJourneys(host: {
102
+ funnelRecording?: unknown;
103
+ } | null | undefined): boolean;
104
+ /**
105
+ * Tells the recorder what the page knows: the site, and whether this visitor
106
+ * may be recorded right now (the site records journeys AND the visitor's
107
+ * analytics consent grants). Called on every render of the page's
108
+ * measurement mount; idempotent.
109
+ *
110
+ * `enabled: null` means "not known yet" — consent still resolving, which a
111
+ * page that remounts goes through on every navigation. Nothing is recorded
112
+ * while it is unknown, and nothing is forgotten either: only a settled `false`
113
+ * (a refusal, or the site no longer recording) deletes the stored visit.
114
+ *
115
+ * `path` is the page the visitor is on. It is recorded as a step when
116
+ * recording turns on and whenever it changes while recording is on — so the
117
+ * page steps are driven from here, once per page the visitor moves to.
118
+ */
119
+ export declare function configureSiteJourney(options: {
120
+ hostId: string | null | undefined;
121
+ enabled: boolean | null;
122
+ path?: string | null;
123
+ }): void;
124
+ /**
125
+ * Records one step of this visit, or drops it: when the page has not enabled
126
+ * recording, when the visit is full, or when the browser refuses storage.
127
+ * Never throws, never queues, never delays the caller.
128
+ */
129
+ export declare function recordSiteJourneyStep(type: SiteJourneyStepType, key?: string | null): void;
130
+ /**
131
+ * The visit id for a door that identifies the visitor — a form submission —
132
+ * as `{ journey: id }`, or nothing: when the page has not enabled recording
133
+ * (no funnel on the site, or no analytics consent), or no visit has started.
134
+ *
135
+ * The id is what lets a funnel's drop-off follow-up reach a PERSON (AGL-3605):
136
+ * the door verifies who submitted, and the server ties this visit to that
137
+ * address. Nothing is minted here; a visit with no recorded step has no id.
138
+ */
139
+ export declare function siteJourneyField(): {
140
+ journey?: string;
141
+ };
142
+ /** Test seam: forget the page's configuration. */
143
+ export declare function resetSiteJourneyForTests(): void;
@@ -0,0 +1,282 @@
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 { sendAnalyticsBeacon } from "./analytics-beacon.js";
18
+ /**
19
+ * A VISIT'S STEPS, IN ORDER (AGL-3605): the one per-visit record the site
20
+ * collector keeps, so a funnel can ask "of the visits that did A, how many
21
+ * then did B".
22
+ *
23
+ * Every other first-party counter is a daily aggregate with no identifier at
24
+ * all (`visit-claim.ts` says why), and an aggregate cannot be ordered: a day
25
+ * with 100 pricing views and 10 sign-ups says nothing about whether the
26
+ * sign-ups viewed pricing. Ordering needs something that ties two steps to one
27
+ * visit, and this module is that and nothing more.
28
+ *
29
+ * ## What identifies a visit
30
+ *
31
+ * A random id held in `sessionStorage`: one browser tab, until it closes. Not
32
+ * a cookie, not `localStorage`, never derived from the address or the
33
+ * browser, never shared between sites (the stored id names the site it was
34
+ * minted for, and another site's page mints its own). Two tabs are two
35
+ * visits; closing the tab and coming back tomorrow is a new visit. Nothing
36
+ * here links a visit to a person: only a door that already verified who the
37
+ * visitor is — a form they submitted — may hand the id to the server beside
38
+ * the address they typed ({@link siteJourneyField}), so a drop-off follow-up
39
+ * reaches a person who identified themselves and never an anonymous visit.
40
+ *
41
+ * ## Consent
42
+ *
43
+ * The id leaves the browser, so it is `analytics` storage and waits for the
44
+ * same grant the site's analytics tag waits for. The page tells this module
45
+ * the verdict on every render ({@link configureSiteJourney}): until it grants
46
+ * — and for a visitor who declines, opts out or sends GPC — nothing is minted
47
+ * and every step is DROPPED, never queued, for the reason
48
+ * `analytics-events.ts` gives. The one step recorded at the moment of a grant
49
+ * is the page the visitor is on right then, which is a step they are taking
50
+ * now, not a replay. Withdrawing the grant deletes the stored id, so a later
51
+ * grant starts a new visit rather than continuing the old one.
52
+ *
53
+ * ## When it runs at all
54
+ *
55
+ * Only on a site that records journeys (`host.funnelRecording === true`,
56
+ * which the plugin that reads them stamps when a site has something to read
57
+ * them for), only on a production surface without the internal-traffic
58
+ * opt-in (the beacon's own gate), and at most {@link SITE_JOURNEY_MAX_STEPS}
59
+ * steps per visit.
60
+ *
61
+ * Kept free of the `@aglyn/aglyn` barrel and of any plugin: the published
62
+ * page's measurement mount imports it (the AGL-1550 independence rule), and
63
+ * plugins report their steps here without knowing who counts them.
64
+ */ /** The body field that marks a collector beacon as a journey step. */ export const SITE_JOURNEY_BEACON_FIELD = 'journey';
65
+ /**
66
+ * What a step can be — only what the platform already sees happen.
67
+ *
68
+ * - `page`: a page was viewed; key = the path.
69
+ * - `form`: a form was submitted successfully; key = the form id.
70
+ * - `booking`: a booking was made (a free one confirmed, or a paid one's
71
+ * payment settled); key = the service id.
72
+ * - `cart`: a product was added to the cart; key = the product id.
73
+ * - `order`: a storefront order was placed and paid; no key.
74
+ * - `overlay`: an announcement bar or popup was clicked; key = the overlay id.
75
+ * - `event`: a custom event an interaction fired; key = the event name.
76
+ * - `email`: a person the visit identified opened or clicked an email the
77
+ * site sent them; key = `opened` or `clicked`. Recorded by the SERVER from
78
+ * the delivery log, never by a page: a browser that sends one is refused.
79
+ */ export const SITE_JOURNEY_STEP_TYPES = [
80
+ 'page',
81
+ 'form',
82
+ 'booking',
83
+ 'cart',
84
+ 'order',
85
+ 'overlay',
86
+ 'event',
87
+ 'email'
88
+ ];
89
+ export function isSiteJourneyStepType(value) {
90
+ return typeof value === 'string' && SITE_JOURNEY_STEP_TYPES.includes(value);
91
+ }
92
+ /** The step types only the server records; a page's beacon naming one is refused. */ export const SITE_JOURNEY_SERVER_STEP_TYPES = new Set([
93
+ 'email'
94
+ ]);
95
+ /** The keys an `email` step carries. */ export const SITE_JOURNEY_EMAIL_KEYS = [
96
+ 'opened',
97
+ 'clicked'
98
+ ];
99
+ /**
100
+ * The body field an identifying door (a form submission) carries the visit
101
+ * id under, so the server can tie the visit to the person the door verified.
102
+ * See {@link siteJourneyField}.
103
+ */ export const SITE_JOURNEY_SUBMISSION_FIELD = 'journey';
104
+ /** The most steps one visit records. A visit past it records nothing more. */ export const SITE_JOURNEY_MAX_STEPS = 60;
105
+ /** The longest key a step carries; longer keys are cut. */ export const SITE_JOURNEY_KEY_MAX = 200;
106
+ /** A visit id's shape: 22 URL-safe characters (128 random bits). */ export const SITE_JOURNEY_ID_PATTERN = /^[A-Za-z0-9_-]{22}$/;
107
+ const STORAGE_KEY = 'aglyn-journey';
108
+ /** Whether a host document asks for journeys to be recorded. */ export function hostRecordsJourneys(host) {
109
+ return (host == null ? void 0 : host.funnelRecording) === true;
110
+ }
111
+ const state = {
112
+ hostId: null,
113
+ enabled: false,
114
+ path: null
115
+ };
116
+ function readStored() {
117
+ try {
118
+ const raw = window.sessionStorage.getItem(STORAGE_KEY);
119
+ if (!raw) return null;
120
+ const parsed = JSON.parse(raw);
121
+ if (!parsed || !SITE_JOURNEY_ID_PATTERN.test(String(parsed.id))) return null;
122
+ return parsed;
123
+ } catch (unused) {
124
+ return null;
125
+ }
126
+ }
127
+ function writeStored(journey) {
128
+ try {
129
+ window.sessionStorage.setItem(STORAGE_KEY, JSON.stringify(journey));
130
+ return true;
131
+ } catch (unused) {
132
+ return false;
133
+ }
134
+ }
135
+ function forgetStored() {
136
+ try {
137
+ window.sessionStorage.removeItem(STORAGE_KEY);
138
+ } catch (unused) {
139
+ // Nothing stored, or storage refused: either way nothing is held.
140
+ }
141
+ }
142
+ /** 128 random bits as 22 URL-safe characters, or null without a CSPRNG. */ function mintJourneyId() {
143
+ try {
144
+ const bytes = new Uint8Array(16);
145
+ window.crypto.getRandomValues(bytes);
146
+ let binary = '';
147
+ for (const byte of bytes)binary += String.fromCharCode(byte);
148
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
149
+ } catch (unused) {
150
+ return null;
151
+ }
152
+ }
153
+ /** Where this visit came from, sent once with its first step. */ function arrivalSource() {
154
+ const source = {};
155
+ try {
156
+ const params = new URLSearchParams(window.location.search);
157
+ for (const [param, field] of [
158
+ [
159
+ 'utm_source',
160
+ 'utmSource'
161
+ ],
162
+ [
163
+ 'utm_medium',
164
+ 'utmMedium'
165
+ ],
166
+ [
167
+ 'utm_campaign',
168
+ 'utmCampaign'
169
+ ]
170
+ ]){
171
+ const value = params.get(param);
172
+ if (value) source[field] = value.slice(0, 100);
173
+ }
174
+ const referrer = document.referrer ? new URL(document.referrer) : null;
175
+ if (referrer && referrer.host && referrer.host !== window.location.host) {
176
+ source['referrerHost'] = referrer.host.slice(0, 100);
177
+ }
178
+ } catch (unused) {
179
+ // An unreadable referrer is a direct visit.
180
+ }
181
+ return source;
182
+ }
183
+ /**
184
+ * Tells the recorder what the page knows: the site, and whether this visitor
185
+ * may be recorded right now (the site records journeys AND the visitor's
186
+ * analytics consent grants). Called on every render of the page's
187
+ * measurement mount; idempotent.
188
+ *
189
+ * `enabled: null` means "not known yet" — consent still resolving, which a
190
+ * page that remounts goes through on every navigation. Nothing is recorded
191
+ * while it is unknown, and nothing is forgotten either: only a settled `false`
192
+ * (a refusal, or the site no longer recording) deletes the stored visit.
193
+ *
194
+ * `path` is the page the visitor is on. It is recorded as a step when
195
+ * recording turns on and whenever it changes while recording is on — so the
196
+ * page steps are driven from here, once per page the visitor moves to.
197
+ */ export function configureSiteJourney(options) {
198
+ if (typeof window === 'undefined') return;
199
+ const hostId = options.hostId || null;
200
+ const enabled = Boolean(hostId) && options.enabled === true;
201
+ const turnedOn = enabled && (!state.enabled || state.hostId !== hostId);
202
+ state.hostId = hostId;
203
+ state.enabled = enabled;
204
+ if (options.enabled === false) forgetStored();
205
+ const path = options.path || null;
206
+ if (enabled && path && (turnedOn || path !== state.path)) {
207
+ state.path = path;
208
+ recordSiteJourneyStep('page', path);
209
+ }
210
+ if (!enabled) state.path = null;
211
+ }
212
+ /**
213
+ * Records one step of this visit, or drops it: when the page has not enabled
214
+ * recording, when the visit is full, or when the browser refuses storage.
215
+ * Never throws, never queues, never delays the caller.
216
+ */ export function recordSiteJourneyStep(type, key) {
217
+ try {
218
+ if (typeof window === 'undefined' || !state.enabled || !state.hostId) return;
219
+ if (!isSiteJourneyStepType(type) || SITE_JOURNEY_SERVER_STEP_TYPES.has(type)) return;
220
+ const hostId = state.hostId;
221
+ const cleanKey = String(key != null ? key : '').trim().slice(0, SITE_JOURNEY_KEY_MAX);
222
+ if (type === 'page' && !cleanKey.startsWith('/')) return;
223
+ if (type === 'event' && !cleanKey) return;
224
+ const stepId = `${type}\u0000${cleanKey}`;
225
+ let journey = readStored();
226
+ let first = false;
227
+ if (!journey || journey.hostId !== hostId) {
228
+ const id = mintJourneyId();
229
+ if (!id) return;
230
+ journey = {
231
+ id,
232
+ hostId,
233
+ steps: 0
234
+ };
235
+ first = true;
236
+ }
237
+ if (journey.steps >= SITE_JOURNEY_MAX_STEPS) return;
238
+ if (journey.last === stepId) return;
239
+ journey = _extends({}, journey, {
240
+ steps: journey.steps + 1,
241
+ last: stepId
242
+ });
243
+ if (!writeStored(journey)) return;
244
+ sendAnalyticsBeacon(_extends({
245
+ hostId,
246
+ [SITE_JOURNEY_BEACON_FIELD]: journey.id,
247
+ stepType: type
248
+ }, cleanKey ? {
249
+ stepKey: cleanKey
250
+ } : {}, first ? _extends({
251
+ journeyStart: true
252
+ }, arrivalSource()) : {}));
253
+ } catch (unused) {
254
+ // A journey step never breaks the page.
255
+ }
256
+ }
257
+ /**
258
+ * The visit id for a door that identifies the visitor — a form submission —
259
+ * as `{ journey: id }`, or nothing: when the page has not enabled recording
260
+ * (no funnel on the site, or no analytics consent), or no visit has started.
261
+ *
262
+ * The id is what lets a funnel's drop-off follow-up reach a PERSON (AGL-3605):
263
+ * the door verifies who submitted, and the server ties this visit to that
264
+ * address. Nothing is minted here; a visit with no recorded step has no id.
265
+ */ export function siteJourneyField() {
266
+ try {
267
+ if (typeof window === 'undefined' || !state.enabled || !state.hostId) return {};
268
+ const journey = readStored();
269
+ return journey && journey.hostId === state.hostId ? {
270
+ [SITE_JOURNEY_SUBMISSION_FIELD]: journey.id
271
+ } : {};
272
+ } catch (unused) {
273
+ return {};
274
+ }
275
+ }
276
+ /** Test seam: forget the page's configuration. */ export function resetSiteJourneyForTests() {
277
+ state.hostId = null;
278
+ state.enabled = false;
279
+ state.path = null;
280
+ }
281
+
282
+ //# sourceMappingURL=site-journey.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/site-journey.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 { sendAnalyticsBeacon } from './analytics-beacon'\n\n/**\n * A VISIT'S STEPS, IN ORDER (AGL-3605): the one per-visit record the site\n * collector keeps, so a funnel can ask \"of the visits that did A, how many\n * then did B\".\n *\n * Every other first-party counter is a daily aggregate with no identifier at\n * all (`visit-claim.ts` says why), and an aggregate cannot be ordered: a day\n * with 100 pricing views and 10 sign-ups says nothing about whether the\n * sign-ups viewed pricing. Ordering needs something that ties two steps to one\n * visit, and this module is that and nothing more.\n *\n * ## What identifies a visit\n *\n * A random id held in `sessionStorage`: one browser tab, until it closes. Not\n * a cookie, not `localStorage`, never derived from the address or the\n * browser, never shared between sites (the stored id names the site it was\n * minted for, and another site's page mints its own). Two tabs are two\n * visits; closing the tab and coming back tomorrow is a new visit. Nothing\n * here links a visit to a person: only a door that already verified who the\n * visitor is — a form they submitted — may hand the id to the server beside\n * the address they typed ({@link siteJourneyField}), so a drop-off follow-up\n * reaches a person who identified themselves and never an anonymous visit.\n *\n * ## Consent\n *\n * The id leaves the browser, so it is `analytics` storage and waits for the\n * same grant the site's analytics tag waits for. The page tells this module\n * the verdict on every render ({@link configureSiteJourney}): until it grants\n * — and for a visitor who declines, opts out or sends GPC — nothing is minted\n * and every step is DROPPED, never queued, for the reason\n * `analytics-events.ts` gives. The one step recorded at the moment of a grant\n * is the page the visitor is on right then, which is a step they are taking\n * now, not a replay. Withdrawing the grant deletes the stored id, so a later\n * grant starts a new visit rather than continuing the old one.\n *\n * ## When it runs at all\n *\n * Only on a site that records journeys (`host.funnelRecording === true`,\n * which the plugin that reads them stamps when a site has something to read\n * them for), only on a production surface without the internal-traffic\n * opt-in (the beacon's own gate), and at most {@link SITE_JOURNEY_MAX_STEPS}\n * steps per visit.\n *\n * Kept free of the `@aglyn/aglyn` barrel and of any plugin: the published\n * page's measurement mount imports it (the AGL-1550 independence rule), and\n * plugins report their steps here without knowing who counts them.\n */\n\n/** The body field that marks a collector beacon as a journey step. */\nexport const SITE_JOURNEY_BEACON_FIELD = 'journey'\n\n/**\n * What a step can be — only what the platform already sees happen.\n *\n * - `page`: a page was viewed; key = the path.\n * - `form`: a form was submitted successfully; key = the form id.\n * - `booking`: a booking was made (a free one confirmed, or a paid one's\n * payment settled); key = the service id.\n * - `cart`: a product was added to the cart; key = the product id.\n * - `order`: a storefront order was placed and paid; no key.\n * - `overlay`: an announcement bar or popup was clicked; key = the overlay id.\n * - `event`: a custom event an interaction fired; key = the event name.\n * - `email`: a person the visit identified opened or clicked an email the\n * site sent them; key = `opened` or `clicked`. Recorded by the SERVER from\n * the delivery log, never by a page: a browser that sends one is refused.\n */\nexport const SITE_JOURNEY_STEP_TYPES = [\n 'page',\n 'form',\n 'booking',\n 'cart',\n 'order',\n 'overlay',\n 'event',\n 'email',\n] as const\n\nexport type SiteJourneyStepType = (typeof SITE_JOURNEY_STEP_TYPES)[number]\n\nexport function isSiteJourneyStepType(value: unknown): value is SiteJourneyStepType {\n return (\n typeof value === 'string' &&\n (SITE_JOURNEY_STEP_TYPES as readonly string[]).includes(value)\n )\n}\n\n/** The step types only the server records; a page's beacon naming one is refused. */\nexport const SITE_JOURNEY_SERVER_STEP_TYPES: ReadonlySet<SiteJourneyStepType> = new Set(['email'])\n\n/** The keys an `email` step carries. */\nexport const SITE_JOURNEY_EMAIL_KEYS = ['opened', 'clicked'] as const\n\n/**\n * The body field an identifying door (a form submission) carries the visit\n * id under, so the server can tie the visit to the person the door verified.\n * See {@link siteJourneyField}.\n */\nexport const SITE_JOURNEY_SUBMISSION_FIELD = 'journey'\n\n/** The most steps one visit records. A visit past it records nothing more. */\nexport const SITE_JOURNEY_MAX_STEPS = 60\n\n/** The longest key a step carries; longer keys are cut. */\nexport const SITE_JOURNEY_KEY_MAX = 200\n\n/** A visit id's shape: 22 URL-safe characters (128 random bits). */\nexport const SITE_JOURNEY_ID_PATTERN = /^[A-Za-z0-9_-]{22}$/\n\nconst STORAGE_KEY = 'aglyn-journey'\n\n/** Whether a host document asks for journeys to be recorded. */\nexport function hostRecordsJourneys(\n host: { funnelRecording?: unknown } | null | undefined,\n): boolean {\n return host?.funnelRecording === true\n}\n\ninterface StoredJourney {\n id: string\n hostId: string\n steps: number\n /** The last step sent, so a reload of the same page is one step. */\n last?: string\n}\n\ninterface JourneyState {\n hostId: string | null\n enabled: boolean\n /** The page last recorded, so a re-render records nothing new. */\n path: string | null\n}\n\nconst state: JourneyState = { hostId: null, enabled: false, path: null }\n\nfunction readStored(): StoredJourney | null {\n try {\n const raw = window.sessionStorage.getItem(STORAGE_KEY)\n if (!raw) return null\n const parsed = JSON.parse(raw) as StoredJourney\n if (!parsed || !SITE_JOURNEY_ID_PATTERN.test(String(parsed.id))) return null\n return parsed\n } catch {\n return null\n }\n}\n\nfunction writeStored(journey: StoredJourney): boolean {\n try {\n window.sessionStorage.setItem(STORAGE_KEY, JSON.stringify(journey))\n return true\n } catch {\n return false\n }\n}\n\nfunction forgetStored(): void {\n try {\n window.sessionStorage.removeItem(STORAGE_KEY)\n } catch {\n // Nothing stored, or storage refused: either way nothing is held.\n }\n}\n\n/** 128 random bits as 22 URL-safe characters, or null without a CSPRNG. */\nfunction mintJourneyId(): string | null {\n try {\n const bytes = new Uint8Array(16)\n window.crypto.getRandomValues(bytes)\n let binary = ''\n for (const byte of bytes) binary += String.fromCharCode(byte)\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\n } catch {\n return null\n }\n}\n\n/** Where this visit came from, sent once with its first step. */\nfunction arrivalSource(): Record<string, string> {\n const source: Record<string, string> = {}\n try {\n const params = new URLSearchParams(window.location.search)\n for (const [param, field] of [\n ['utm_source', 'utmSource'],\n ['utm_medium', 'utmMedium'],\n ['utm_campaign', 'utmCampaign'],\n ] as const) {\n const value = params.get(param)\n if (value) source[field] = value.slice(0, 100)\n }\n const referrer = document.referrer ? new URL(document.referrer) : null\n if (referrer && referrer.host && referrer.host !== window.location.host) {\n source['referrerHost'] = referrer.host.slice(0, 100)\n }\n } catch {\n // An unreadable referrer is a direct visit.\n }\n return source\n}\n\n/**\n * Tells the recorder what the page knows: the site, and whether this visitor\n * may be recorded right now (the site records journeys AND the visitor's\n * analytics consent grants). Called on every render of the page's\n * measurement mount; idempotent.\n *\n * `enabled: null` means \"not known yet\" — consent still resolving, which a\n * page that remounts goes through on every navigation. Nothing is recorded\n * while it is unknown, and nothing is forgotten either: only a settled `false`\n * (a refusal, or the site no longer recording) deletes the stored visit.\n *\n * `path` is the page the visitor is on. It is recorded as a step when\n * recording turns on and whenever it changes while recording is on — so the\n * page steps are driven from here, once per page the visitor moves to.\n */\nexport function configureSiteJourney(options: {\n hostId: string | null | undefined\n enabled: boolean | null\n path?: string | null\n}): void {\n if (typeof window === 'undefined') return\n const hostId = options.hostId || null\n const enabled = Boolean(hostId) && options.enabled === true\n const turnedOn = enabled && (!state.enabled || state.hostId !== hostId)\n state.hostId = hostId\n state.enabled = enabled\n if (options.enabled === false) forgetStored()\n const path = options.path || null\n if (enabled && path && (turnedOn || path !== state.path)) {\n state.path = path\n recordSiteJourneyStep('page', path)\n }\n if (!enabled) state.path = null\n}\n\n/**\n * Records one step of this visit, or drops it: when the page has not enabled\n * recording, when the visit is full, or when the browser refuses storage.\n * Never throws, never queues, never delays the caller.\n */\nexport function recordSiteJourneyStep(\n type: SiteJourneyStepType,\n key?: string | null,\n): void {\n try {\n if (typeof window === 'undefined' || !state.enabled || !state.hostId) return\n if (!isSiteJourneyStepType(type) || SITE_JOURNEY_SERVER_STEP_TYPES.has(type)) return\n const hostId = state.hostId\n const cleanKey = String(key ?? '').trim().slice(0, SITE_JOURNEY_KEY_MAX)\n if (type === 'page' && !cleanKey.startsWith('/')) return\n if (type === 'event' && !cleanKey) return\n const stepId = `${type}\\u0000${cleanKey}`\n\n let journey = readStored()\n let first = false\n if (!journey || journey.hostId !== hostId) {\n const id = mintJourneyId()\n if (!id) return\n journey = { id, hostId, steps: 0 }\n first = true\n }\n if (journey.steps >= SITE_JOURNEY_MAX_STEPS) return\n if (journey.last === stepId) return\n journey = { ...journey, steps: journey.steps + 1, last: stepId }\n if (!writeStored(journey)) return\n\n sendAnalyticsBeacon({\n hostId,\n [SITE_JOURNEY_BEACON_FIELD]: journey.id,\n stepType: type,\n ...(cleanKey ? { stepKey: cleanKey } : {}),\n ...(first ? { journeyStart: true, ...arrivalSource() } : {}),\n })\n } catch {\n // A journey step never breaks the page.\n }\n}\n\n/**\n * The visit id for a door that identifies the visitor — a form submission —\n * as `{ journey: id }`, or nothing: when the page has not enabled recording\n * (no funnel on the site, or no analytics consent), or no visit has started.\n *\n * The id is what lets a funnel's drop-off follow-up reach a PERSON (AGL-3605):\n * the door verifies who submitted, and the server ties this visit to that\n * address. Nothing is minted here; a visit with no recorded step has no id.\n */\nexport function siteJourneyField(): { journey?: string } {\n try {\n if (typeof window === 'undefined' || !state.enabled || !state.hostId) return {}\n const journey = readStored()\n return journey && journey.hostId === state.hostId ? { [SITE_JOURNEY_SUBMISSION_FIELD]: journey.id } : {}\n } catch {\n return {}\n }\n}\n\n/** Test seam: forget the page's configuration. */\nexport function resetSiteJourneyForTests(): void {\n state.hostId = null\n state.enabled = false\n state.path = null\n}\n"],"names":["sendAnalyticsBeacon","SITE_JOURNEY_BEACON_FIELD","SITE_JOURNEY_STEP_TYPES","isSiteJourneyStepType","value","includes","SITE_JOURNEY_SERVER_STEP_TYPES","Set","SITE_JOURNEY_EMAIL_KEYS","SITE_JOURNEY_SUBMISSION_FIELD","SITE_JOURNEY_MAX_STEPS","SITE_JOURNEY_KEY_MAX","SITE_JOURNEY_ID_PATTERN","STORAGE_KEY","hostRecordsJourneys","host","funnelRecording","state","hostId","enabled","path","readStored","raw","window","sessionStorage","getItem","parsed","JSON","parse","test","String","id","writeStored","journey","setItem","stringify","forgetStored","removeItem","mintJourneyId","bytes","Uint8Array","crypto","getRandomValues","binary","byte","fromCharCode","btoa","replace","arrivalSource","source","params","URLSearchParams","location","search","param","field","get","slice","referrer","document","URL","configureSiteJourney","options","Boolean","turnedOn","recordSiteJourneyStep","type","key","has","cleanKey","trim","startsWith","stepId","first","steps","last","stepType","stepKey","journeyStart","siteJourneyField","resetSiteJourneyForTests"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,mBAAmB,QAAQ,wBAAoB;AAExD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8CC,GAED,oEAAoE,GACpE,OAAO,MAAMC,4BAA4B,UAAS;AAElD;;;;;;;;;;;;;;CAcC,GACD,OAAO,MAAMC,0BAA0B;IACrC;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,CAAS;AAIV,OAAO,SAASC,sBAAsBC,KAAc;IAClD,OACE,OAAOA,UAAU,YACjB,AAACF,wBAA8CG,QAAQ,CAACD;AAE5D;AAEA,mFAAmF,GACnF,OAAO,MAAME,iCAAmE,IAAIC,IAAI;IAAC;CAAQ,EAAC;AAElG,sCAAsC,GACtC,OAAO,MAAMC,0BAA0B;IAAC;IAAU;CAAU,CAAS;AAErE;;;;CAIC,GACD,OAAO,MAAMC,gCAAgC,UAAS;AAEtD,4EAA4E,GAC5E,OAAO,MAAMC,yBAAyB,GAAE;AAExC,yDAAyD,GACzD,OAAO,MAAMC,uBAAuB,IAAG;AAEvC,kEAAkE,GAClE,OAAO,MAAMC,0BAA0B,sBAAqB;AAE5D,MAAMC,cAAc;AAEpB,8DAA8D,GAC9D,OAAO,SAASC,oBACdC,IAAsD;IAEtD,OAAOA,CAAAA,wBAAAA,KAAMC,eAAe,MAAK;AACnC;AAiBA,MAAMC,QAAsB;IAAEC,QAAQ;IAAMC,SAAS;IAAOC,MAAM;AAAK;AAEvE,SAASC;IACP,IAAI;QACF,MAAMC,MAAMC,OAAOC,cAAc,CAACC,OAAO,CAACZ;QAC1C,IAAI,CAACS,KAAK,OAAO;QACjB,MAAMI,SAASC,KAAKC,KAAK,CAACN;QAC1B,IAAI,CAACI,UAAU,CAACd,wBAAwBiB,IAAI,CAACC,OAAOJ,OAAOK,EAAE,IAAI,OAAO;QACxE,OAAOL;IACT,EAAE,eAAM;QACN,OAAO;IACT;AACF;AAEA,SAASM,YAAYC,OAAsB;IACzC,IAAI;QACFV,OAAOC,cAAc,CAACU,OAAO,CAACrB,aAAac,KAAKQ,SAAS,CAACF;QAC1D,OAAO;IACT,EAAE,eAAM;QACN,OAAO;IACT;AACF;AAEA,SAASG;IACP,IAAI;QACFb,OAAOC,cAAc,CAACa,UAAU,CAACxB;IACnC,EAAE,eAAM;IACN,kEAAkE;IACpE;AACF;AAEA,yEAAyE,GACzE,SAASyB;IACP,IAAI;QACF,MAAMC,QAAQ,IAAIC,WAAW;QAC7BjB,OAAOkB,MAAM,CAACC,eAAe,CAACH;QAC9B,IAAII,SAAS;QACb,KAAK,MAAMC,QAAQL,MAAOI,UAAUb,OAAOe,YAAY,CAACD;QACxD,OAAOE,KAAKH,QAAQI,OAAO,CAAC,OAAO,KAAKA,OAAO,CAAC,OAAO,KAAKA,OAAO,CAAC,OAAO;IAC7E,EAAE,eAAM;QACN,OAAO;IACT;AACF;AAEA,+DAA+D,GAC/D,SAASC;IACP,MAAMC,SAAiC,CAAC;IACxC,IAAI;QACF,MAAMC,SAAS,IAAIC,gBAAgB5B,OAAO6B,QAAQ,CAACC,MAAM;QACzD,KAAK,MAAM,CAACC,OAAOC,MAAM,IAAI;YAC3B;gBAAC;gBAAc;aAAY;YAC3B;gBAAC;gBAAc;aAAY;YAC3B;gBAAC;gBAAgB;aAAc;SAChC,CAAW;YACV,MAAMnD,QAAQ8C,OAAOM,GAAG,CAACF;YACzB,IAAIlD,OAAO6C,MAAM,CAACM,MAAM,GAAGnD,MAAMqD,KAAK,CAAC,GAAG;QAC5C;QACA,MAAMC,WAAWC,SAASD,QAAQ,GAAG,IAAIE,IAAID,SAASD,QAAQ,IAAI;QAClE,IAAIA,YAAYA,SAAS3C,IAAI,IAAI2C,SAAS3C,IAAI,KAAKQ,OAAO6B,QAAQ,CAACrC,IAAI,EAAE;YACvEkC,MAAM,CAAC,eAAe,GAAGS,SAAS3C,IAAI,CAAC0C,KAAK,CAAC,GAAG;QAClD;IACF,EAAE,eAAM;IACN,4CAA4C;IAC9C;IACA,OAAOR;AACT;AAEA;;;;;;;;;;;;;;CAcC,GACD,OAAO,SAASY,qBAAqBC,OAIpC;IACC,IAAI,OAAOvC,WAAW,aAAa;IACnC,MAAML,SAAS4C,QAAQ5C,MAAM,IAAI;IACjC,MAAMC,UAAU4C,QAAQ7C,WAAW4C,QAAQ3C,OAAO,KAAK;IACvD,MAAM6C,WAAW7C,WAAY,CAAA,CAACF,MAAME,OAAO,IAAIF,MAAMC,MAAM,KAAKA,MAAK;IACrED,MAAMC,MAAM,GAAGA;IACfD,MAAME,OAAO,GAAGA;IAChB,IAAI2C,QAAQ3C,OAAO,KAAK,OAAOiB;IAC/B,MAAMhB,OAAO0C,QAAQ1C,IAAI,IAAI;IAC7B,IAAID,WAAWC,QAAS4C,CAAAA,YAAY5C,SAASH,MAAMG,IAAI,AAAD,GAAI;QACxDH,MAAMG,IAAI,GAAGA;QACb6C,sBAAsB,QAAQ7C;IAChC;IACA,IAAI,CAACD,SAASF,MAAMG,IAAI,GAAG;AAC7B;AAEA;;;;CAIC,GACD,OAAO,SAAS6C,sBACdC,IAAyB,EACzBC,GAAmB;IAEnB,IAAI;QACF,IAAI,OAAO5C,WAAW,eAAe,CAACN,MAAME,OAAO,IAAI,CAACF,MAAMC,MAAM,EAAE;QACtE,IAAI,CAACf,sBAAsB+D,SAAS5D,+BAA+B8D,GAAG,CAACF,OAAO;QAC9E,MAAMhD,SAASD,MAAMC,MAAM;QAC3B,MAAMmD,WAAWvC,OAAOqC,cAAAA,MAAO,IAAIG,IAAI,GAAGb,KAAK,CAAC,GAAG9C;QACnD,IAAIuD,SAAS,UAAU,CAACG,SAASE,UAAU,CAAC,MAAM;QAClD,IAAIL,SAAS,WAAW,CAACG,UAAU;QACnC,MAAMG,SAAS,GAAGN,KAAK,MAAM,EAAEG,UAAU;QAEzC,IAAIpC,UAAUZ;QACd,IAAIoD,QAAQ;QACZ,IAAI,CAACxC,WAAWA,QAAQf,MAAM,KAAKA,QAAQ;YACzC,MAAMa,KAAKO;YACX,IAAI,CAACP,IAAI;YACTE,UAAU;gBAAEF;gBAAIb;gBAAQwD,OAAO;YAAE;YACjCD,QAAQ;QACV;QACA,IAAIxC,QAAQyC,KAAK,IAAIhE,wBAAwB;QAC7C,IAAIuB,QAAQ0C,IAAI,KAAKH,QAAQ;QAC7BvC,UAAU,aAAKA;YAASyC,OAAOzC,QAAQyC,KAAK,GAAG;YAAGC,MAAMH;;QACxD,IAAI,CAACxC,YAAYC,UAAU;QAE3BjC,oBAAoB;YAClBkB;YACA,CAACjB,0BAA0B,EAAEgC,QAAQF,EAAE;YACvC6C,UAAUV;WACNG,WAAW;YAAEQ,SAASR;QAAS,IAAI,CAAC,GACpCI,QAAQ;YAAEK,cAAc;WAAS9B,mBAAoB,CAAC;IAE9D,EAAE,eAAM;IACN,wCAAwC;IAC1C;AACF;AAEA;;;;;;;;CAQC,GACD,OAAO,SAAS+B;IACd,IAAI;QACF,IAAI,OAAOxD,WAAW,eAAe,CAACN,MAAME,OAAO,IAAI,CAACF,MAAMC,MAAM,EAAE,OAAO,CAAC;QAC9E,MAAMe,UAAUZ;QAChB,OAAOY,WAAWA,QAAQf,MAAM,KAAKD,MAAMC,MAAM,GAAG;YAAE,CAACT,8BAA8B,EAAEwB,QAAQF,EAAE;QAAC,IAAI,CAAC;IACzG,EAAE,eAAM;QACN,OAAO,CAAC;IACV;AACF;AAEA,gDAAgD,GAChD,OAAO,SAASiD;IACd/D,MAAMC,MAAM,GAAG;IACfD,MAAME,OAAO,GAAG;IAChBF,MAAMG,IAAI,GAAG;AACf"}
@@ -60,6 +60,14 @@ export interface CoreOrgFeatureFlags {
60
60
  /** A/B experiments (AGL-252); Business tier. */
61
61
  abTesting?: boolean;
62
62
  versioning?: boolean;
63
+ /**
64
+ * Reusable components WITHOUT a count: Starter and above. Since AGL-3615 a
65
+ * component's create is counted against `componentsPerHost` alone — Free
66
+ * saves one — so this flag no longer decides whether a site may make one.
67
+ * It states that the allowance is unlimited, and a per-org override that
68
+ * turns it on lifts `componentsPerHost` with it, which is how an org
69
+ * granted the feature by contract before the count existed keeps it.
70
+ */
63
71
  reusableComponents?: boolean;
64
72
  customDomain?: boolean;
65
73
  /**
@@ -472,6 +480,14 @@ export interface CoreOrgEntitlements {
472
480
  * is why a plan resolving to 0 still accepts submissions.
473
481
  */
474
482
  formsPerHost?: number;
483
+ /**
484
+ * Reusable component DEFINITIONS per host — documents under
485
+ * `hosts/{hostId}/components` (AGL-3615). Free 1; every paid plan
486
+ * `UNLIMITED`, the allowance it had when components were a boolean
487
+ * feature. Refused at the create only, so a site holding more than its
488
+ * plan now includes keeps every component.
489
+ */
490
+ componentsPerHost?: number;
475
491
  /** Component-builder caps (AGL-99): host variables. */
476
492
  variablesPerHost?: number;
477
493
  /** Component-builder caps (AGL-99): host functions. */