@orion-studios/cms 0.5.7 → 0.5.9

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.
@@ -1,3 +1,5 @@
1
+ declare const ANALYTICS_READY_EVENT = "orion-analytics-ready";
2
+
1
3
  /**
2
4
  * The site-side analytics tracker: small, first-party, and zero dependency.
3
5
  * Mount <Analytics /> once in the site layout. It captures:
@@ -50,4 +52,4 @@ declare function AnalyticsNotFound(): null;
50
52
  /** Programmatic tracking for custom site components. No-op when analytics is off. */
51
53
  declare function trackEvent(name: string, meta?: Record<string, unknown>): void;
52
54
 
53
- export { ANALYTICS_CONSENT_COOKIE, ANALYTICS_CONSENT_EVENT, ANALYTICS_VISITOR_COOKIE, Analytics, type AnalyticsConsent, AnalyticsNotFound, type AnalyticsProps, EXCLUDE_FLAG, getAnalyticsConsent, setAnalyticsConsent, trackEvent };
55
+ export { ANALYTICS_CONSENT_COOKIE, ANALYTICS_CONSENT_EVENT, ANALYTICS_READY_EVENT, ANALYTICS_VISITOR_COOKIE, Analytics, type AnalyticsConsent, AnalyticsNotFound, type AnalyticsProps, EXCLUDE_FLAG, getAnalyticsConsent, setAnalyticsConsent, trackEvent };
@@ -1,5 +1,8 @@
1
1
  'use client';
2
2
  "use client";
3
+ import {
4
+ ANALYTICS_READY_EVENT
5
+ } from "../chunk-DPKXKG2S.js";
3
6
 
4
7
  // src/analytics/react.tsx
5
8
  import { useEffect, useRef, useState } from "react";
@@ -105,6 +108,7 @@ function Analytics({
105
108
  track({ type, name, path: window.location.pathname, meta });
106
109
  if (type === "form" || type === "not_found") flush();
107
110
  };
111
+ window.dispatchEvent(new Event(ANALYTICS_READY_EVENT));
108
112
  const onClick = (event) => {
109
113
  const target = event.target;
110
114
  const tagged = target?.closest("[data-analytics]");
@@ -167,6 +171,10 @@ function Analytics({
167
171
  const value = params.get(`utm_${key}`);
168
172
  if (value) utm[key] = value;
169
173
  }
174
+ if (params.has("gclid") && !utm.source) {
175
+ utm.source = "google";
176
+ utm.medium = "cpc";
177
+ }
170
178
  if (Object.keys(utm).length > 0) event.utm = utm;
171
179
  }
172
180
  track(event);
@@ -185,6 +193,7 @@ function trackEvent(name, meta) {
185
193
  export {
186
194
  ANALYTICS_CONSENT_COOKIE,
187
195
  ANALYTICS_CONSENT_EVENT,
196
+ ANALYTICS_READY_EVENT,
188
197
  ANALYTICS_VISITOR_COOKIE,
189
198
  Analytics,
190
199
  AnalyticsNotFound,
@@ -0,0 +1,8 @@
1
+ 'use client';
2
+
3
+ // src/analytics/constants.ts
4
+ var ANALYTICS_READY_EVENT = "orion-analytics-ready";
5
+
6
+ export {
7
+ ANALYTICS_READY_EVENT
8
+ };
@@ -1,7 +1,16 @@
1
1
  'use client';
2
+ import {
3
+ ANALYTICS_READY_EVENT
4
+ } from "./chunk-DPKXKG2S.js";
2
5
 
3
6
  // src/forms/react.tsx
4
- import { useEffect, useMemo, useRef, useState } from "react";
7
+ import {
8
+ useCallback,
9
+ useEffect,
10
+ useMemo,
11
+ useRef,
12
+ useState
13
+ } from "react";
5
14
 
6
15
  // src/forms/validation.ts
7
16
  var MAJOR_EMAIL_PROVIDERS = [
@@ -248,8 +257,135 @@ function getAutoReplyEmailFields(config) {
248
257
  return [...fieldTypes.entries()].filter(([, types]) => types.size === 1 && types.has("email")).map(([name]) => name);
249
258
  }
250
259
 
260
+ // src/forms/funnel.ts
261
+ var FORM_VIEW_MIN_VISIBLE_RATIO = 0.75;
262
+ var FORM_VIEW_DWELL_MS = 2e3;
263
+ function createFormFunnelSession(trackStage) {
264
+ const trackedStages = /* @__PURE__ */ new Set();
265
+ const trackOnce = (stage) => {
266
+ if (trackedStages.has(stage) || !trackStage(stage)) return false;
267
+ trackedStages.add(stage);
268
+ return true;
269
+ };
270
+ return {
271
+ hasViewed: () => trackedStages.has("viewed"),
272
+ trackStarted: () => trackOnce("start"),
273
+ trackViewed: () => trackOnce("viewed")
274
+ };
275
+ }
276
+ function isQualifiedFormVisibility(intersectionRatio, isIntersecting, visibilityState) {
277
+ return isIntersecting && intersectionRatio >= FORM_VIEW_MIN_VISIBLE_RATIO && visibilityState === "visible";
278
+ }
279
+
251
280
  // src/forms/react.tsx
252
281
  import { jsx, jsxs } from "react/jsx-runtime";
282
+ var analyticsTrack = () => window.__orionTrack;
283
+ var isActionableFieldTarget = (target) => {
284
+ if (!(target instanceof Element)) return false;
285
+ const directField = target.closest("input, select, textarea");
286
+ const labelledField = target.closest("label")?.control;
287
+ const field = directField || labelledField;
288
+ if (!(field instanceof HTMLInputElement || field instanceof HTMLSelectElement || field instanceof HTMLTextAreaElement) || field.name === HONEYPOT_FIELD_NAME || field.disabled || "readOnly" in field && field.readOnly) {
289
+ return false;
290
+ }
291
+ return true;
292
+ };
293
+ function useFormFunnel(slug, options = {}) {
294
+ const { preview = false } = options;
295
+ const [attentionTarget, setAttentionTarget] = useState(null);
296
+ const visibleRef = useRef(false);
297
+ const timerRef = useRef(null);
298
+ const clearViewTimer = useCallback(() => {
299
+ if (timerRef.current === null) return;
300
+ window.clearTimeout(timerRef.current);
301
+ timerRef.current = null;
302
+ }, []);
303
+ const trackStage = useCallback(
304
+ (stage) => {
305
+ if (preview || typeof window === "undefined") return false;
306
+ const track = analyticsTrack();
307
+ if (!track) return false;
308
+ track("form", `${slug}:${stage}`);
309
+ return true;
310
+ },
311
+ [preview, slug]
312
+ );
313
+ const session = useMemo(() => createFormFunnelSession(trackStage), [trackStage]);
314
+ const trackViewed = useCallback(() => {
315
+ if (session.trackViewed()) clearViewTimer();
316
+ }, [clearViewTimer, session]);
317
+ const startViewTimer = useCallback(() => {
318
+ if (session.hasViewed() || timerRef.current !== null || !visibleRef.current || document.visibilityState !== "visible" || !analyticsTrack()) {
319
+ return;
320
+ }
321
+ timerRef.current = window.setTimeout(() => {
322
+ timerRef.current = null;
323
+ if (visibleRef.current && document.visibilityState === "visible") trackViewed();
324
+ }, FORM_VIEW_DWELL_MS);
325
+ }, [session, trackViewed]);
326
+ useEffect(() => {
327
+ visibleRef.current = false;
328
+ clearViewTimer();
329
+ if (preview || !attentionTarget || typeof IntersectionObserver === "undefined") return;
330
+ const observer = new IntersectionObserver(
331
+ ([entry]) => {
332
+ visibleRef.current = Boolean(
333
+ entry && entry.isIntersecting && entry.intersectionRatio >= FORM_VIEW_MIN_VISIBLE_RATIO
334
+ );
335
+ if (entry && isQualifiedFormVisibility(
336
+ entry.intersectionRatio,
337
+ entry.isIntersecting,
338
+ document.visibilityState
339
+ )) {
340
+ startViewTimer();
341
+ } else clearViewTimer();
342
+ },
343
+ { threshold: FORM_VIEW_MIN_VISIBLE_RATIO }
344
+ );
345
+ const onVisibilityChange = () => {
346
+ if (document.visibilityState !== "visible") {
347
+ clearViewTimer();
348
+ return;
349
+ }
350
+ if (visibleRef.current) startViewTimer();
351
+ };
352
+ const onAnalyticsReady = () => {
353
+ if (visibleRef.current) startViewTimer();
354
+ };
355
+ observer.observe(attentionTarget);
356
+ document.addEventListener("visibilitychange", onVisibilityChange);
357
+ window.addEventListener(ANALYTICS_READY_EVENT, onAnalyticsReady);
358
+ return () => {
359
+ observer.disconnect();
360
+ document.removeEventListener("visibilitychange", onVisibilityChange);
361
+ window.removeEventListener(ANALYTICS_READY_EVENT, onAnalyticsReady);
362
+ clearViewTimer();
363
+ };
364
+ }, [attentionTarget, clearViewTimer, preview, slug, startViewTimer]);
365
+ const onFieldPointerDownCapture = useCallback(
366
+ (event) => {
367
+ if (!event.nativeEvent.isTrusted || !isActionableFieldTarget(event.target)) return;
368
+ trackViewed();
369
+ },
370
+ [trackViewed]
371
+ );
372
+ const onFieldKeyDownCapture = useCallback(
373
+ (event) => {
374
+ if (!event.nativeEvent.isTrusted || !isActionableFieldTarget(event.target)) return;
375
+ trackViewed();
376
+ },
377
+ [trackViewed]
378
+ );
379
+ const trackStarted = useCallback(() => {
380
+ session.trackStarted();
381
+ }, [session]);
382
+ return {
383
+ attentionRef: setAttentionTarget,
384
+ onFieldKeyDownCapture,
385
+ onFieldPointerDownCapture,
386
+ trackStarted
387
+ };
388
+ }
253
389
  var DEFAULT_CLASSES = {
254
390
  form: "ocf-form",
255
391
  field: "ocf-field",
@@ -291,26 +427,17 @@ function FormRenderer({
291
427
  const [state, setState] = useState("idle");
292
428
  const [honeypot, setHoneypot] = useState("");
293
429
  const renderedAt = useRef(Date.now());
294
- const startedRef = useRef(false);
295
- const funnel = (stage) => {
296
- if (preview || typeof window === "undefined") return;
297
- window.__orionTrack?.(
298
- "form",
299
- `${slug}:${stage}`
300
- );
301
- };
302
- useEffect(() => {
303
- const timer = setTimeout(() => funnel("view"), 100);
304
- return () => clearTimeout(timer);
305
- }, [slug]);
430
+ const {
431
+ attentionRef,
432
+ onFieldKeyDownCapture,
433
+ onFieldPointerDownCapture,
434
+ trackStarted
435
+ } = useFormFunnel(slug, { preview });
306
436
  useEffect(() => {
307
437
  setStepIndex((current) => Math.min(current, Math.max(steps.length - 1, 0)));
308
438
  }, [steps.length]);
309
439
  const setValue = (name, next, type) => {
310
- if (!startedRef.current) {
311
- startedRef.current = true;
312
- funnel("start");
313
- }
440
+ trackStarted();
314
441
  setValues((current) => ({
315
442
  ...current,
316
443
  [name]: type === "phone" && typeof next === "string" ? formatPhoneUS(next) : next
@@ -393,6 +520,7 @@ function FormRenderer({
393
520
  }
394
521
  const isLastStep = stepIndex >= steps.length - 1;
395
522
  const visibleSteps = steps.length > 0 ? [steps[stepIndex]] : [];
523
+ const firstActionableFieldKey = visibleSteps.flatMap((step) => step.fields || []).map((field, index) => ({ field, key: fieldKey(field, index) })).find(({ field }) => inferFieldType(field) !== "hidden")?.key;
396
524
  const renderField = (field, index) => {
397
525
  const name = fieldKey(field, index);
398
526
  const type = inferFieldType(field);
@@ -402,15 +530,23 @@ function FormRenderer({
402
530
  const id = `ocf-${slug}-${name}`;
403
531
  const value = values[name];
404
532
  if (type === "hidden") return null;
405
- const wrap = (control) => /* @__PURE__ */ jsxs("div", { className: `${cls.field}${error ? ` ${cls.fieldError}` : ""}`, children: [
406
- type !== "checkbox" || options.length > 0 ? /* @__PURE__ */ jsxs("label", { className: cls.label, htmlFor: id, children: [
407
- label,
408
- field.required ? " *" : ""
409
- ] }) : null,
410
- control,
411
- field.help ? /* @__PURE__ */ jsx("p", { className: "ocf-help", children: field.help }) : null,
412
- error ? /* @__PURE__ */ jsx("p", { className: cls.errorText, role: "alert", children: error }) : null
413
- ] }, name);
533
+ const wrap = (control) => /* @__PURE__ */ jsxs(
534
+ "div",
535
+ {
536
+ className: `${cls.field}${error ? ` ${cls.fieldError}` : ""}`,
537
+ ref: name === firstActionableFieldKey ? attentionRef : void 0,
538
+ children: [
539
+ type !== "checkbox" || options.length > 0 ? /* @__PURE__ */ jsxs("label", { className: cls.label, htmlFor: id, children: [
540
+ label,
541
+ field.required ? " *" : ""
542
+ ] }) : null,
543
+ control,
544
+ field.help ? /* @__PURE__ */ jsx("p", { className: "ocf-help", children: field.help }) : null,
545
+ error ? /* @__PURE__ */ jsx("p", { className: cls.errorText, role: "alert", children: error }) : null
546
+ ]
547
+ },
548
+ name
549
+ );
414
550
  if (type === "textarea") {
415
551
  return wrap(
416
552
  /* @__PURE__ */ jsx(
@@ -480,22 +616,30 @@ function FormRenderer({
480
616
  );
481
617
  }
482
618
  if (type === "checkbox") {
483
- return /* @__PURE__ */ jsxs("div", { className: `${cls.field}${error ? ` ${cls.fieldError}` : ""}`, children: [
484
- /* @__PURE__ */ jsxs("label", { className: cls.checkbox, htmlFor: id, children: [
485
- /* @__PURE__ */ jsx(
486
- "input",
487
- {
488
- checked: value === true,
489
- id,
490
- onChange: (event) => setValue(name, event.target.checked, type),
491
- type: "checkbox"
492
- }
493
- ),
494
- label,
495
- field.required ? " *" : ""
496
- ] }),
497
- error ? /* @__PURE__ */ jsx("p", { className: cls.errorText, role: "alert", children: error }) : null
498
- ] }, name);
619
+ return /* @__PURE__ */ jsxs(
620
+ "div",
621
+ {
622
+ className: `${cls.field}${error ? ` ${cls.fieldError}` : ""}`,
623
+ ref: name === firstActionableFieldKey ? attentionRef : void 0,
624
+ children: [
625
+ /* @__PURE__ */ jsxs("label", { className: cls.checkbox, htmlFor: id, children: [
626
+ /* @__PURE__ */ jsx(
627
+ "input",
628
+ {
629
+ checked: value === true,
630
+ id,
631
+ onChange: (event) => setValue(name, event.target.checked, type),
632
+ type: "checkbox"
633
+ }
634
+ ),
635
+ label,
636
+ field.required ? " *" : ""
637
+ ] }),
638
+ error ? /* @__PURE__ */ jsx("p", { className: cls.errorText, role: "alert", children: error }) : null
639
+ ]
640
+ },
641
+ name
642
+ );
499
643
  }
500
644
  const inputType = type === "email" ? "email" : type === "date" ? "date" : type === "number" ? "text" : "text";
501
645
  const inputMode = type === "phone" ? "tel" : type === "email" ? "email" : type === "number" ? "decimal" : void 0;
@@ -514,40 +658,51 @@ function FormRenderer({
514
658
  )
515
659
  );
516
660
  };
517
- return /* @__PURE__ */ jsxs("form", { className: cls.form, noValidate: true, onSubmit: submit, children: [
518
- title,
519
- intro,
520
- /* @__PURE__ */ jsx(
521
- "input",
522
- {
523
- "aria-hidden": "true",
524
- autoComplete: "off",
525
- className: "ocf-honeypot",
526
- name: HONEYPOT_FIELD_NAME,
527
- onChange: (event) => setHoneypot(event.target.value),
528
- style: { position: "absolute", left: "-9999px", height: 0, width: 0, opacity: 0 },
529
- tabIndex: -1,
530
- value: honeypot
531
- }
532
- ),
533
- steps.length > 1 ? /* @__PURE__ */ jsxs("p", { className: cls.stepTitle, children: [
534
- "Step ",
535
- stepIndex + 1,
536
- " of ",
537
- steps.length,
538
- steps[stepIndex]?.title ? ` \u2014 ${steps[stepIndex].title}` : ""
539
- ] }) : null,
540
- visibleSteps.map((step) => (step.fields || []).map(renderField)),
541
- state === "error" ? /* @__PURE__ */ jsx("p", { className: cls.formError, role: "alert", children: "The request could not be sent. Please try again." }) : null,
542
- /* @__PURE__ */ jsxs("div", { className: "ocf-actions", children: [
543
- steps.length > 1 && stepIndex > 0 ? /* @__PURE__ */ jsx("button", { className: cls.button, onClick: () => setStepIndex(stepIndex - 1), type: "button", children: "Back" }) : null,
544
- !isLastStep ? /* @__PURE__ */ jsx("button", { className: cls.button, onClick: nextStep, type: "button", children: "Next" }) : /* @__PURE__ */ jsx("button", { className: cls.button, disabled: state === "submitting" || preview, type: "submit", children: state === "submitting" ? "Sending\u2026" : submitLabel })
545
- ] })
546
- ] });
661
+ return /* @__PURE__ */ jsxs(
662
+ "form",
663
+ {
664
+ className: cls.form,
665
+ noValidate: true,
666
+ onKeyDownCapture: onFieldKeyDownCapture,
667
+ onPointerDownCapture: onFieldPointerDownCapture,
668
+ onSubmit: submit,
669
+ children: [
670
+ title,
671
+ intro,
672
+ /* @__PURE__ */ jsx(
673
+ "input",
674
+ {
675
+ "aria-hidden": "true",
676
+ autoComplete: "off",
677
+ className: "ocf-honeypot",
678
+ name: HONEYPOT_FIELD_NAME,
679
+ onChange: (event) => setHoneypot(event.target.value),
680
+ style: { position: "absolute", left: "-9999px", height: 0, width: 0, opacity: 0 },
681
+ tabIndex: -1,
682
+ value: honeypot
683
+ }
684
+ ),
685
+ steps.length > 1 ? /* @__PURE__ */ jsxs("p", { className: cls.stepTitle, children: [
686
+ "Step ",
687
+ stepIndex + 1,
688
+ " of ",
689
+ steps.length,
690
+ steps[stepIndex]?.title ? ` \u2014 ${steps[stepIndex].title}` : ""
691
+ ] }) : null,
692
+ visibleSteps.map((step) => (step.fields || []).map(renderField)),
693
+ state === "error" ? /* @__PURE__ */ jsx("p", { className: cls.formError, role: "alert", children: "The request could not be sent. Please try again." }) : null,
694
+ /* @__PURE__ */ jsxs("div", { className: "ocf-actions", children: [
695
+ steps.length > 1 && stepIndex > 0 ? /* @__PURE__ */ jsx("button", { className: cls.button, onClick: () => setStepIndex(stepIndex - 1), type: "button", children: "Back" }) : null,
696
+ !isLastStep ? /* @__PURE__ */ jsx("button", { className: cls.button, onClick: nextStep, type: "button", children: "Next" }) : /* @__PURE__ */ jsx("button", { className: cls.button, disabled: state === "submitting" || preview, type: "submit", children: state === "submitting" ? "Sending\u2026" : submitLabel })
697
+ ] })
698
+ ]
699
+ }
700
+ );
547
701
  }
548
702
 
549
703
  export {
550
704
  FORM_FIELD_TYPES,
551
705
  getAutoReplyEmailFields,
706
+ useFormFunnel,
552
707
  FormRenderer
553
708
  };
@@ -1,7 +1,17 @@
1
1
  import * as react from 'react';
2
- import { ReactNode } from 'react';
2
+ import { KeyboardEvent, PointerEvent, ReactNode } from 'react';
3
3
  import { F as FormConfig } from '../submission-CKZgx1h7.js';
4
4
 
5
+ type FormFunnelOptions = {
6
+ preview?: boolean;
7
+ };
8
+ type FormFunnelTracking = {
9
+ attentionRef: (node: HTMLElement | null) => void;
10
+ onFieldKeyDownCapture: (event: KeyboardEvent<HTMLElement>) => void;
11
+ onFieldPointerDownCapture: (event: PointerEvent<HTMLElement>) => void;
12
+ trackStarted: () => void;
13
+ };
14
+ declare function useFormFunnel(slug: string, options?: FormFunnelOptions): FormFunnelTracking;
5
15
  /**
6
16
  * The shared, config-driven form renderer: the same component renders a form
7
17
  * in the Studio's form editor preview and on the public site, from the same
@@ -42,4 +52,4 @@ type FormRendererProps = {
42
52
  };
43
53
  declare function FormRenderer({ slug, config, successMessage, basePath, classNames, title, intro, submitLabel, preview, onSuccess, }: FormRendererProps): react.JSX.Element;
44
54
 
45
- export { FormRenderer, type FormRendererClassNames, type FormRendererProps };
55
+ export { type FormFunnelOptions, type FormFunnelTracking, FormRenderer, type FormRendererClassNames, type FormRendererProps, useFormFunnel };
@@ -1,8 +1,11 @@
1
1
  'use client';
2
2
  "use client";
3
3
  import {
4
- FormRenderer
5
- } from "../chunk-ULE565KD.js";
4
+ FormRenderer,
5
+ useFormFunnel
6
+ } from "../chunk-Z52R6XR7.js";
7
+ import "../chunk-DPKXKG2S.js";
6
8
  export {
7
- FormRenderer
9
+ FormRenderer,
10
+ useFormFunnel
8
11
  };
@@ -77,6 +77,8 @@ type CmsRoutesOptions = {
77
77
  projectRef?: string;
78
78
  /** Purpose-specific secret for analytics visitor and session hashing. */
79
79
  analyticsSecret?: string;
80
+ /** IANA timezone used for analytics dates and hourly buckets. Defaults to UTC. */
81
+ analyticsTimeZone?: string;
80
82
  /** Purpose-specific secret for auto-reply recipient hashing. */
81
83
  autoReplyHashSecret?: string;
82
84
  /** Cloudflare Turnstile secret — when set, public submits must include a valid `_turnstileToken`. */
@@ -183,8 +185,8 @@ declare function getPreviewPage(client: SupabaseClient, token: string, keys: Pre
183
185
  * and memory backends and is trivially testable.
184
186
  *
185
187
  * Call and email taps are useful interaction counts, but browser events are
186
- * forgeable. A conversion requires a server-verified event, currently a form
187
- * submission recorded after the authoritative lead write succeeds.
188
+ * forgeable. Form conversions come from authoritative submission rows. Any
189
+ * other conversion requires a server-verified event.
188
190
  */
189
191
  type StoredEvent = {
190
192
  id?: number | string;
@@ -202,6 +204,13 @@ type StoredEvent = {
202
204
  server_verified?: boolean;
203
205
  created_at: string;
204
206
  };
207
+ /** Minimal authoritative form record used by aggregate analytics. */
208
+ type StoredFormSubmission = {
209
+ id?: number | string;
210
+ form: string;
211
+ session_key: string;
212
+ created_at: string;
213
+ };
205
214
  type AnalyticsKpis = {
206
215
  visitors: number;
207
216
  identifiedVisitors: number;
@@ -215,8 +224,11 @@ type AnalyticsKpis = {
215
224
  portalClicks: number;
216
225
  conversions: number;
217
226
  conversionRate: number;
227
+ attributedSubmits: number;
228
+ unattributedSubmits: number;
218
229
  };
219
230
  type AnalyticsSummary = {
231
+ timeZone: string;
220
232
  range: {
221
233
  from: string;
222
234
  to: string;
@@ -267,7 +279,7 @@ type AnalyticsSummary = {
267
279
  declare function aggregateAnalytics(events: StoredEvent[], previousEvents: StoredEvent[], range: {
268
280
  from: string;
269
281
  to: string;
270
- }): AnalyticsSummary;
282
+ }, submissions?: StoredFormSubmission[], previousSubmissions?: StoredFormSubmission[], timeZone?: string): AnalyticsSummary;
271
283
 
272
284
  /**
273
285
  * Server-side analytics ingest: validation, bot filtering, and the
@@ -301,7 +313,7 @@ type AnalyticsEventRow = {
301
313
  * Daily-rotating visitor hash: same visitor+day → same key, next day → a new
302
314
  * unrelated key. Orders one visit into a path; can't track anyone over time.
303
315
  */
304
- declare function sessionKeyFor(ip: string, userAgent: string, secret: string, now?: Date): string;
316
+ declare function sessionKeyFor(ip: string, userAgent: string, secret: string, now?: Date, timeZone?: string): string;
305
317
  /** One-way server hash for a consented first-party visitor cookie. */
306
318
  declare function visitorKeyFor(visitorId: unknown, secret: string): string;
307
319
  /** True when the request looks like an automated client, not a visitor. */
@@ -489,4 +501,4 @@ declare function createMemoryCms(): MemoryCms;
489
501
  /** Process-wide singleton for Next.js dev servers (module state survives HMR via globalThis). */
490
502
  declare function getMemoryCms(): MemoryCms;
491
503
 
492
- export { type AnalyticsEventType, type AnalyticsKpis, type AnalyticsSummary, type CmsAction, type CmsEnv, type CmsRole, type CmsRoutesOptions, type CmsUser, type EmailMessage, type EmailSender, type IncomingEvent, MEMORY_DEV_TOKEN, type MemoryCms, PREVIEW_SESSION_COOKIE, PREVIEW_TOKEN_MAX_USES, PREVIEW_TOKEN_TTL_MS, type PreviewGrantClaims, type PreviewPage, type ResendSenderOptions, type StoredEvent, type SyncFormInput, type SyncGlobalInput, type SyncInput, type SyncMediaInput, type SyncOperation, type SyncOptions, type SyncPageInput, type SyncPlan, type SyncResult, aggregateAnalytics, can, createCmsRoutes, createDurableRateLimitStore, createMemoryCms, createPreviewToken, createResendSender, deviceFrom, formatSubmissionText, geoFrom, getMemoryCms, getPreviewPage, getServiceClient, isBotRequest, isStructuralChange, notifySubmission, parseEventBatch, readCmsEnv, resolveUser, runContentSync, sessionKeyFor, setServiceClientForTesting, validateCmsEnv, verifyPreviewGrantToken, verifyPreviewToken, visitorKeyFor };
504
+ export { type AnalyticsEventType, type AnalyticsKpis, type AnalyticsSummary, type CmsAction, type CmsEnv, type CmsRole, type CmsRoutesOptions, type CmsUser, type EmailMessage, type EmailSender, type IncomingEvent, MEMORY_DEV_TOKEN, type MemoryCms, PREVIEW_SESSION_COOKIE, PREVIEW_TOKEN_MAX_USES, PREVIEW_TOKEN_TTL_MS, type PreviewGrantClaims, type PreviewPage, type ResendSenderOptions, type StoredEvent, type StoredFormSubmission, type SyncFormInput, type SyncGlobalInput, type SyncInput, type SyncMediaInput, type SyncOperation, type SyncOptions, type SyncPageInput, type SyncPlan, type SyncResult, aggregateAnalytics, can, createCmsRoutes, createDurableRateLimitStore, createMemoryCms, createPreviewToken, createResendSender, deviceFrom, formatSubmissionText, geoFrom, getMemoryCms, getPreviewPage, getServiceClient, isBotRequest, isStructuralChange, notifySubmission, parseEventBatch, readCmsEnv, resolveUser, runContentSync, sessionKeyFor, setServiceClientForTesting, validateCmsEnv, verifyPreviewGrantToken, verifyPreviewToken, visitorKeyFor };