@tokenoftrust/storefront-runner 2.2.77 → 2.2.78

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/storefront-runner",
3
- "version": "2.2.77",
3
+ "version": "2.2.78",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "description": "World-shareable storefront runner: multi-tenant renderer on Astro/Cloudflare. No control plane.",
6
6
  "packageManager": "pnpm@11.9.0",
@@ -492,7 +492,7 @@ export type ViewerAnalyticsConsentDecision = "granted" | "denied";
492
492
  export interface ViewerAnalyticsConsentState {
493
493
  schemaVersion: 1;
494
494
  decision: ViewerAnalyticsConsentDecision;
495
- source: "tenant-default" | "default-deny" | "stored" | "global-privacy-control";
495
+ source: "tenant-default" | "default-deny" | "stored" | "stored-invalid" | "global-privacy-control";
496
496
  canGrant: boolean;
497
497
  expiresAt: number | null;
498
498
  retentionDays: typeof VIEWER_ANALYTICS_CONSENT_RETENTION_DAYS;
@@ -596,51 +596,68 @@ export function applyEarlyViewerAnalyticsConsent(
596
596
  const globalPrivacyControl = win.navigator?.globalPrivacyControl === true;
597
597
 
598
598
  let stored: { decision: ViewerAnalyticsConsentDecision; expiresAt: number } | null = null;
599
+ // A stored record that is present but unreadable may have been a denial, so it denies
600
+ // rather than falling through to a granting tenant default. Only a well-formed record
601
+ // past its retention lapses to the default.
602
+ let storedInvalid = false;
599
603
  if (override) {
600
604
  stored = { decision: override, expiresAt: now + retentionMs };
601
605
  } else {
606
+ let raw: string | null | undefined;
602
607
  try {
603
- const raw = win.localStorage?.getItem(scopedStorageKey);
604
- if (raw) {
605
- const parsed = JSON.parse(raw) as Record<string, unknown>;
606
- const keys = Object.keys(parsed).sort();
607
- const decidedAt = parsed["decidedAt"];
608
- const expiresAt = parsed["expiresAt"];
609
- const decision = parsed["decision"];
610
- if (
611
- keys.join(",") === "decidedAt,decision,environment,expiresAt,schemaVersion,siteSurface,tenantPublicId" &&
612
- parsed["schemaVersion"] === 1 &&
613
- parsed["tenantPublicId"] === tenantPublicId &&
614
- parsed["environment"] === environment &&
615
- parsed["siteSurface"] === siteSurface &&
616
- (decision === "granted" || decision === "denied") &&
617
- typeof decidedAt === "number" &&
618
- Number.isFinite(decidedAt) &&
619
- typeof expiresAt === "number" &&
620
- Number.isFinite(expiresAt) &&
621
- decidedAt <= now &&
622
- expiresAt > now &&
623
- expiresAt - decidedAt === retentionMs
624
- ) {
625
- stored = { decision, expiresAt };
626
- }
627
- }
608
+ raw = win.localStorage?.getItem(scopedStorageKey);
628
609
  } catch {
629
- // Storage denial, malformed data, and hostile access all fail closed.
610
+ // Storage denial leaves no viewer decision to read.
611
+ }
612
+ if (raw) {
613
+ let parsed: unknown = null;
614
+ try {
615
+ parsed = JSON.parse(raw);
616
+ } catch {
617
+ // Malformed data is handled as an invalid record below.
618
+ }
619
+ const record = typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
620
+ ? parsed as Record<string, unknown>
621
+ : null;
622
+ const decidedAt = record?.["decidedAt"];
623
+ const expiresAt = record?.["expiresAt"];
624
+ const decision = record?.["decision"];
625
+ if (
626
+ record !== null &&
627
+ Object.keys(record).sort().join(",") ===
628
+ "decidedAt,decision,environment,expiresAt,schemaVersion,siteSurface,tenantPublicId" &&
629
+ record["schemaVersion"] === 1 &&
630
+ record["tenantPublicId"] === tenantPublicId &&
631
+ record["environment"] === environment &&
632
+ record["siteSurface"] === siteSurface &&
633
+ (decision === "granted" || decision === "denied") &&
634
+ typeof decidedAt === "number" &&
635
+ Number.isFinite(decidedAt) &&
636
+ typeof expiresAt === "number" &&
637
+ Number.isFinite(expiresAt) &&
638
+ decidedAt <= now &&
639
+ expiresAt - decidedAt === retentionMs
640
+ ) {
641
+ if (expiresAt > now) stored = { decision, expiresAt };
642
+ } else {
643
+ storedInvalid = true;
644
+ }
630
645
  }
631
646
  }
632
647
 
633
648
  const decision: ViewerAnalyticsConsentDecision =
634
- globalPrivacyControl || !tenantApprovedAnalytics
649
+ globalPrivacyControl || !tenantApprovedAnalytics || (!stored && storedInvalid)
635
650
  ? "denied"
636
651
  : stored?.decision ?? (tenantDefaultAnalytics ? "granted" : "denied");
637
652
  const source: ViewerAnalyticsConsentState["source"] = globalPrivacyControl
638
653
  ? "global-privacy-control"
639
654
  : stored
640
655
  ? "stored"
641
- : tenantDefaultAnalytics
642
- ? "tenant-default"
643
- : "default-deny";
656
+ : storedInvalid
657
+ ? "stored-invalid"
658
+ : tenantDefaultAnalytics
659
+ ? "tenant-default"
660
+ : "default-deny";
644
661
 
645
662
  const eligible = canonicalPurposes.filter(
646
663
  (purpose) =>