@storystick/preview 0.0.0-stage → 0.2.0-rc.0

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 (245) hide show
  1. package/LICENSE +112 -0
  2. package/PUBLIC-API.md +55 -0
  3. package/README.md +42 -2
  4. package/dist/.source-hash +1 -0
  5. package/dist/authoring/runtime-contract.d.ts +89 -0
  6. package/dist/authoring/runtime-contract.js +28 -0
  7. package/dist/channel-taxonomy.d.ts +17 -0
  8. package/dist/channel-taxonomy.js +46 -0
  9. package/dist/context-command.d.ts +18 -0
  10. package/dist/context-command.js +38 -0
  11. package/dist/dev/suspicious-codeword-detector.d.ts +15 -0
  12. package/dist/dev/suspicious-codeword-detector.js +220 -0
  13. package/dist/dev/suspicious-codeword-detector.noop.d.ts +3 -0
  14. package/dist/dev/suspicious-codeword-detector.noop.js +4 -0
  15. package/dist/framework/browser-context.d.ts +2 -0
  16. package/dist/framework/browser-context.js +2 -0
  17. package/dist/framework/generation-refresh.d.ts +23 -0
  18. package/dist/framework/generation-refresh.js +82 -0
  19. package/dist/framework/generation-signal.d.ts +8 -0
  20. package/dist/framework/generation-signal.js +17 -0
  21. package/dist/framework/projection-generation-watch.d.ts +5 -0
  22. package/dist/framework/projection-generation-watch.js +53 -0
  23. package/dist/framework/render-receipt.d.ts +8 -0
  24. package/dist/framework/render-receipt.js +10 -0
  25. package/dist/framework/runtime-adapter-store.d.ts +30 -0
  26. package/dist/framework/runtime-adapter-store.js +81 -0
  27. package/dist/grant-coordinator.d.ts +53 -0
  28. package/dist/grant-coordinator.js +329 -0
  29. package/dist/grant-lifecycle.d.ts +52 -0
  30. package/dist/grant-lifecycle.js +74 -0
  31. package/dist/identity/__test-adapter.d.ts +34 -0
  32. package/dist/identity/__test-adapter.js +59 -0
  33. package/dist/identity/browser-context-gate.d.ts +4 -0
  34. package/dist/identity/browser-context-gate.js +22 -0
  35. package/dist/identity/browser-context.d.ts +13 -0
  36. package/dist/identity/browser-context.js +22 -0
  37. package/dist/identity/carriers/postmessage.d.ts +84 -0
  38. package/dist/identity/carriers/postmessage.js +177 -0
  39. package/dist/identity/carriers/storage-state.d.ts +41 -0
  40. package/dist/identity/carriers/storage-state.js +99 -0
  41. package/dist/identity/carriers/storage.d.ts +39 -0
  42. package/dist/identity/carriers/storage.js +182 -0
  43. package/dist/identity/carriers/url.d.ts +19 -0
  44. package/dist/identity/carriers/url.js +38 -0
  45. package/dist/identity/identity-state-machine.d.ts +70 -0
  46. package/dist/identity/identity-state-machine.js +244 -0
  47. package/dist/identity/identity-types.d.ts +192 -0
  48. package/dist/identity/identity-types.js +22 -0
  49. package/dist/identity/index.d.ts +18 -0
  50. package/dist/identity/index.js +16 -0
  51. package/dist/identity/preview-cookie.d.ts +9 -0
  52. package/dist/identity/preview-cookie.js +12 -0
  53. package/dist/identity/preview-draft.d.ts +6 -0
  54. package/dist/identity/preview-draft.js +25 -0
  55. package/dist/identity/preview-identity.d.ts +28 -0
  56. package/dist/identity/preview-identity.js +84 -0
  57. package/dist/identity/preview-scope.d.ts +28 -0
  58. package/dist/identity/preview-scope.js +47 -0
  59. package/dist/identity/resolver.d.ts +110 -0
  60. package/dist/identity/resolver.js +334 -0
  61. package/dist/identity/session-query.d.ts +4 -0
  62. package/dist/identity/session-query.js +19 -0
  63. package/dist/index.d.ts +90 -0
  64. package/dist/index.js +788 -0
  65. package/dist/interaction-mode.d.ts +58 -0
  66. package/dist/interaction-mode.js +93 -0
  67. package/dist/nav-handler.d.ts +95 -0
  68. package/dist/nav-handler.js +84 -0
  69. package/dist/overlay-label.d.ts +79 -0
  70. package/dist/overlay-label.js +73 -0
  71. package/dist/provenance/anchor-map.d.ts +95 -0
  72. package/dist/provenance/anchor-map.js +177 -0
  73. package/dist/provenance/carrier-plan.d.ts +51 -0
  74. package/dist/provenance/carrier-plan.js +90 -0
  75. package/dist/provenance/coded-text.d.ts +20 -0
  76. package/dist/provenance/coded-text.js +302 -0
  77. package/dist/provenance/direct-edit-locator.d.ts +29 -0
  78. package/dist/provenance/direct-edit-locator.js +218 -0
  79. package/dist/provenance/editable-leaf.d.ts +134 -0
  80. package/dist/provenance/editable-leaf.js +483 -0
  81. package/dist/provenance/editor-target.d.ts +29 -0
  82. package/dist/provenance/editor-target.js +46 -0
  83. package/dist/provenance/field-leaf-walker.d.ts +21 -0
  84. package/dist/provenance/field-leaf-walker.js +98 -0
  85. package/dist/provenance/frame-dependencies.d.ts +212 -0
  86. package/dist/provenance/frame-dependencies.js +550 -0
  87. package/dist/provenance/index.d.ts +26 -0
  88. package/dist/provenance/index.js +26 -0
  89. package/dist/provenance/local-events.d.ts +1 -0
  90. package/dist/provenance/local-events.js +1 -0
  91. package/dist/provenance/markers.d.ts +21 -0
  92. package/dist/provenance/markers.js +70 -0
  93. package/dist/provenance/multi-frame-anchor-map.d.ts +54 -0
  94. package/dist/provenance/multi-frame-anchor-map.js +109 -0
  95. package/dist/provenance/normalize.d.ts +2 -0
  96. package/dist/provenance/normalize.js +19 -0
  97. package/dist/provenance/overlay-focus-echo.d.ts +17 -0
  98. package/dist/provenance/overlay-focus-echo.js +16 -0
  99. package/dist/provenance/overlay-projection.d.ts +65 -0
  100. package/dist/provenance/overlay-projection.js +201 -0
  101. package/dist/provenance/owner-map.d.ts +55 -0
  102. package/dist/provenance/owner-map.js +239 -0
  103. package/dist/provenance/read-carrier.d.ts +22 -0
  104. package/dist/provenance/read-carrier.js +92 -0
  105. package/dist/provenance/recorder.d.ts +38 -0
  106. package/dist/provenance/recorder.js +82 -0
  107. package/dist/provenance/render-context.d.ts +106 -0
  108. package/dist/provenance/render-context.js +338 -0
  109. package/dist/provenance/reveal-contract.d.ts +44 -0
  110. package/dist/provenance/reveal-contract.js +1 -0
  111. package/dist/provenance/richtext-slice-embed-address.d.ts +20 -0
  112. package/dist/provenance/richtext-slice-embed-address.js +35 -0
  113. package/dist/provenance/runtime-script.d.ts +1 -0
  114. package/dist/provenance/runtime-script.js +2217 -0
  115. package/dist/provenance/sidecar-store.d.ts +63 -0
  116. package/dist/provenance/sidecar-store.js +151 -0
  117. package/dist/provenance/sidecar.d.ts +34 -0
  118. package/dist/provenance/sidecar.js +227 -0
  119. package/dist/provenance/slice-model.d.ts +28 -0
  120. package/dist/provenance/slice-model.js +86 -0
  121. package/dist/provenance/source-identity.d.ts +3 -0
  122. package/dist/provenance/source-identity.js +7 -0
  123. package/dist/provenance/source-map-materializer.d.ts +76 -0
  124. package/dist/provenance/source-map-materializer.js +286 -0
  125. package/dist/provenance/source-registry.d.ts +103 -0
  126. package/dist/provenance/source-registry.js +562 -0
  127. package/dist/provenance/transformed-output-gate.d.ts +161 -0
  128. package/dist/provenance/transformed-output-gate.js +171 -0
  129. package/dist/provenance/types.d.ts +102 -0
  130. package/dist/provenance/types.js +1 -0
  131. package/dist/refresh-policy.d.ts +11 -0
  132. package/dist/refresh-policy.js +14 -0
  133. package/dist/reveal-target.d.ts +8 -0
  134. package/dist/reveal-target.js +47 -0
  135. package/dist/route-paths.d.ts +11 -0
  136. package/dist/route-paths.js +14 -0
  137. package/dist/router-subscriber.d.ts +92 -0
  138. package/dist/router-subscriber.js +44 -0
  139. package/dist/runtime/anchor-types.d.ts +5 -0
  140. package/dist/runtime/anchor-types.js +1 -0
  141. package/dist/runtime/authority-store.d.ts +31 -0
  142. package/dist/runtime/authority-store.js +56 -0
  143. package/dist/runtime/boot.d.ts +52 -0
  144. package/dist/runtime/boot.js +379 -0
  145. package/dist/runtime/content-refresh.d.ts +30 -0
  146. package/dist/runtime/content-refresh.js +68 -0
  147. package/dist/runtime/emit-runtime-error.d.ts +51 -0
  148. package/dist/runtime/emit-runtime-error.js +64 -0
  149. package/dist/runtime/field-proxy.d.ts +112 -0
  150. package/dist/runtime/field-proxy.js +471 -0
  151. package/dist/runtime/form-contract-reporter.d.ts +99 -0
  152. package/dist/runtime/form-contract-reporter.js +391 -0
  153. package/dist/runtime/invalidation-adapter.d.ts +17 -0
  154. package/dist/runtime/invalidation-adapter.js +42 -0
  155. package/dist/runtime/live-query-overlay.d.ts +39 -0
  156. package/dist/runtime/live-query-overlay.js +345 -0
  157. package/dist/runtime/momentary-navigate.d.ts +16 -0
  158. package/dist/runtime/momentary-navigate.js +106 -0
  159. package/dist/runtime/observation-scheduler.d.ts +15 -0
  160. package/dist/runtime/observation-scheduler.js +42 -0
  161. package/dist/runtime/perf-metrics.d.ts +28 -0
  162. package/dist/runtime/perf-metrics.js +90 -0
  163. package/dist/runtime/preview-interaction-policy.d.ts +1 -0
  164. package/dist/runtime/preview-interaction-policy.js +103 -0
  165. package/dist/runtime/preview-overlay-painter.d.ts +27 -0
  166. package/dist/runtime/preview-overlay-painter.js +1732 -0
  167. package/dist/runtime/preview-overlay-sidecar.d.ts +26 -0
  168. package/dist/runtime/preview-overlay-sidecar.js +45 -0
  169. package/dist/runtime/preview-perspective-alignment.d.ts +15 -0
  170. package/dist/runtime/preview-perspective-alignment.js +40 -0
  171. package/dist/runtime/preview-perspective-intent-state.d.ts +23 -0
  172. package/dist/runtime/preview-perspective-intent-state.js +5 -0
  173. package/dist/runtime/preview-transition-state.d.ts +142 -0
  174. package/dist/runtime/preview-transition-state.js +257 -0
  175. package/dist/runtime/render-mode-state.d.ts +51 -0
  176. package/dist/runtime/render-mode-state.js +76 -0
  177. package/dist/runtime/request-id.d.ts +4 -0
  178. package/dist/runtime/request-id.js +8 -0
  179. package/dist/runtime/resolve-schema-field.d.ts +99 -0
  180. package/dist/runtime/resolve-schema-field.js +356 -0
  181. package/dist/runtime/runtime-error-dispatch.d.ts +23 -0
  182. package/dist/runtime/runtime-error-dispatch.js +75 -0
  183. package/dist/runtime/schema-store.d.ts +121 -0
  184. package/dist/runtime/schema-store.js +667 -0
  185. package/dist/runtime/snapshot-assembly.d.ts +104 -0
  186. package/dist/runtime/snapshot-assembly.js +553 -0
  187. package/dist/runtime/snapshot-reporter.d.ts +55 -0
  188. package/dist/runtime/snapshot-reporter.js +375 -0
  189. package/dist/runtime/standalone-overlay-controller.d.ts +33 -0
  190. package/dist/runtime/standalone-overlay-controller.js +281 -0
  191. package/dist/runtime/store.d.ts +307 -0
  192. package/dist/runtime/store.js +1026 -0
  193. package/dist/runtime/windowed-minimap-projection.d.ts +28 -0
  194. package/dist/runtime/windowed-minimap-projection.js +189 -0
  195. package/dist/runtime/workspace-shortcuts.d.ts +30 -0
  196. package/dist/runtime/workspace-shortcuts.js +188 -0
  197. package/dist/sdk-error-copy.d.ts +48 -0
  198. package/dist/sdk-error-copy.js +182 -0
  199. package/dist/select-cursor.d.ts +2 -0
  200. package/dist/select-cursor.js +23 -0
  201. package/dist/server/admission-cookie.d.ts +38 -0
  202. package/dist/server/admission-cookie.js +122 -0
  203. package/dist/server/control-client.d.ts +29 -0
  204. package/dist/server/control-client.js +22 -0
  205. package/dist/server/control-errors.d.ts +22 -0
  206. package/dist/server/control-errors.js +165 -0
  207. package/dist/server/draft-route.d.ts +12 -0
  208. package/dist/server/draft-route.js +45 -0
  209. package/dist/server/extract-token.d.ts +30 -0
  210. package/dist/server/extract-token.js +98 -0
  211. package/dist/server/grant.d.ts +128 -0
  212. package/dist/server/grant.js +395 -0
  213. package/dist/server/ordinary-read-provenance.d.ts +11 -0
  214. package/dist/server/ordinary-read-provenance.js +124 -0
  215. package/dist/server/preview-read-metadata.d.ts +10 -0
  216. package/dist/server/preview-read-metadata.js +27 -0
  217. package/dist/server/provenance-frame-builder.d.ts +53 -0
  218. package/dist/server/provenance-frame-builder.js +102 -0
  219. package/dist/server/provenance-runtime.d.ts +70 -0
  220. package/dist/server/provenance-runtime.js +46 -0
  221. package/dist/server/read-context-command.d.ts +11 -0
  222. package/dist/server/read-context-command.js +53 -0
  223. package/dist/server/read-identity.d.ts +3 -0
  224. package/dist/server/read-identity.js +63 -0
  225. package/dist/server/read-lifecycle.d.ts +17 -0
  226. package/dist/server/read-lifecycle.js +52 -0
  227. package/dist/server/render-frame-provenance.d.ts +133 -0
  228. package/dist/server/render-frame-provenance.js +676 -0
  229. package/dist/server/render-id.d.ts +15 -0
  230. package/dist/server/render-id.js +22 -0
  231. package/dist/server/route-document-provenance.d.ts +77 -0
  232. package/dist/server/route-document-provenance.js +1327 -0
  233. package/dist/server/route-session-cookie.d.ts +33 -0
  234. package/dist/server/route-session-cookie.js +54 -0
  235. package/dist/session-projections.d.ts +33 -0
  236. package/dist/session-projections.js +43 -0
  237. package/dist/session-subscriber.d.ts +124 -0
  238. package/dist/session-subscriber.js +79 -0
  239. package/dist/sha256-fallback.d.ts +30 -0
  240. package/dist/sha256-fallback.js +126 -0
  241. package/dist/types.d.ts +446 -0
  242. package/dist/types.js +85 -0
  243. package/dist/validators.d.ts +61 -0
  244. package/dist/validators.js +110 -0
  245. package/package.json +206 -3
package/dist/index.js ADDED
@@ -0,0 +1,788 @@
1
+ import { readStandalonePreviewSessionId } from './identity/browser-context.js';
2
+ import { createPreviewRequestId } from './runtime/request-id.js';
3
+ /**
4
+ * @storystick/preview — Visual editing overlay
5
+ *
6
+ * Framework-independent preview system for Storystick.
7
+ * Used by @storystick/react (and future @storystick/astro, etc.)
8
+ * via dynamic import — only loaded in preview mode.
9
+ *
10
+ * This package provides:
11
+ * - Proxy field tracking (wraps data, records field access)
12
+ * - DOM scanning (MutationObserver matches values to elements)
13
+ * - Overlay rendering (selection, drag handles, "+" buttons)
14
+ * - PostMessage bridge (iframe ↔ Studio communication)
15
+ * - Preview session management
16
+ */
17
+ import { startSnapshotReporter, } from './runtime/snapshot-reporter.js';
18
+ import { installPreviewWorkspaceShortcuts } from './runtime/workspace-shortcuts.js';
19
+ import { previewRuntime } from './runtime/store.js';
20
+ import { previewPerfNow, recordPreviewPerfMeasure, recordPreviewPerfMetric } from './runtime/perf-metrics.js';
21
+ import { startStandaloneOverlayController } from './runtime/standalone-overlay-controller.js';
22
+ /**
23
+ * SHA-256 hex of `routeUrl`, shared with the SDK document-observation
24
+ * tap and Studio route scoping.
25
+ *
26
+ * Web Crypto is the fast path. When `crypto.subtle` is unavailable —
27
+ * specifically in dev environments where the embedder chain is not a
28
+ * secure context (Studio on `http://storystick.dojo` embedding an
29
+ * iframe at `http://localhost` ; secure context inherits from the
30
+ * chain per W3C Secure Contexts §4.3) — we fall back to the pure-JS
31
+ * SHA-256 in `sha256-fallback.ts`. Output is byte-equivalent to Web
32
+ * Crypto's SHA-256 hex.
33
+ *
34
+ * The fallback is unit-tested for FIPS compliance ; it's a transcription
35
+ * of FIPS PUB 180-4 §6.2. Production paths (HTTPS↔HTTPS) always hit
36
+ * the Web Crypto branch.
37
+ */
38
+ async function computeRouteHash(routeUrl) {
39
+ // crypto.subtle returns undefined in non-secure contexts ; guard
40
+ // explicitly rather than letting `crypto.subtle.digest` throw a
41
+ // `TypeError: Cannot read properties of undefined` every snapshot tick.
42
+ if (typeof crypto !== 'undefined' &&
43
+ crypto.subtle !== undefined &&
44
+ typeof crypto.subtle.digest === 'function') {
45
+ const bytes = new TextEncoder().encode(routeUrl);
46
+ const digest = await crypto.subtle.digest('SHA-256', bytes);
47
+ return Array.from(new Uint8Array(digest))
48
+ .map((b) => b.toString(16).padStart(2, '0'))
49
+ .join('');
50
+ }
51
+ const { sha256HexFallback } = await import('./sha256-fallback.js');
52
+ return sha256HexFallback(routeUrl);
53
+ }
54
+ import { startFormContractReporter, } from './runtime/form-contract-reporter.js';
55
+ import { validateNavigate, validateOverlayClear, validateOverlayMap, } from './validators.js';
56
+ import { applyPreviewOverlayMap, clearPreviewOverlayPaint, installPreviewEditInteractionGuard, } from './runtime/preview-overlay-painter.js';
57
+ import { getAnchorFromPreviewClickEvent, installMomentaryNavigateKeyRelay, normalizeMomentaryNavigateLinkClick, } from './runtime/momentary-navigate.js';
58
+ import { getNavigateHandler, getReloadHandler, hasNavigateHandler, } from './nav-handler.js';
59
+ import { setRouterSubscriber, } from './router-subscriber.js';
60
+ import { getInteractionMode, setInteractionMode } from './interaction-mode.js';
61
+ import { PREVIEW_NAMESPACE, PREVIEW_PROTOCOL_VERSION, validatePreviewEnvelope, LOCATION_PATHNAME_MAX, LOCATION_SEARCH_MAX, LOCATION_HASH_MAX, ROUTE_RENDERED_CODE_VERSION_MAX, } from '@storystick/transport-allowlist';
62
+ export { isIframeToStudio, isStudioToIframe } from './types.js';
63
+ // ─────────────────────────────────────────────
64
+ // Runtime — in-iframe SDK pieces (PR2', ADR-059 §Technical commitments)
65
+ // ─────────────────────────────────────────────
66
+ // ADR-129 — the atomic-snapshot reporter started by `activate()`.
67
+ export { startSnapshotReporter, } from './runtime/snapshot-reporter.js';
68
+ export { assembleSnapshot, } from './runtime/snapshot-assembly.js';
69
+ export { startFormContractReporter, getStorystickViewport, FORM_PREVIEW_ATTR, } from './runtime/form-contract-reporter.js';
70
+ export { getRenderMode, setRenderMode, subscribeRenderMode, debounceForRenderMode, } from './runtime/render-mode-state.js';
71
+ // ─────────────────────────────────────────────
72
+ // Preview grant lifecycle + control-plane surface
73
+ // ─────────────────────────────────────────────
74
+ //
75
+ // The lifecycle, coordinator, validators, nav-handler, and session-subscriber
76
+ // are split into their own
77
+ // files (per-/plan-eng-review CQ1). The package re-exports the
78
+ // public surface so `@storystick/react`'s provider has a single
79
+ // import.
80
+ export { validateNavigate, validateOverlayClear, validateOverlayMap, } from './validators.js';
81
+ export { legacyPreviewAuthState, transitionPreviewGrantLifecycle, } from './grant-lifecycle.js';
82
+ export { PREVIEW_TOKEN_ROUTE, PREVIEW_GRANT_RETRY_BACKOFF_MS, PreviewGrantRequestError, acquirePreviewBrowserGrant, createPreviewGrantCoordinator, } from './grant-coordinator.js';
83
+ export { setNavigateHandler, getNavigateHandler, setReloadHandler, getReloadHandler, _resetNavigateHandlerForTests, } from './nav-handler.js';
84
+ // ─── ADR-108 — router-subscriber + interaction-mode ──────────────
85
+ export { setRouterSubscriber, getRouterSubscriber, notifyRouterTransition, _resetRouterSubscriberForTests, } from './router-subscriber.js';
86
+ export { getInteractionMode, setInteractionMode, _resetInteractionModeForTests, } from './interaction-mode.js';
87
+ export { subscribePreviewSession, mapPreviewSessionError, } from './session-subscriber.js';
88
+ export { wrapWithProxy, getTrackedFields, clearTrackedFields, collectFieldModel,
89
+ // ADR-126 — RSC render-path field-model island plumbing.
90
+ collectFieldModelFromRoots, registerIslandRoots, clearIslandRoots, isFieldTrackingActive, __setFieldTrackingForcedForTests, } from './runtime/field-proxy.js';
91
+ // ─────────────────────────────────────────────
92
+ // Activate — entry point for framework SDKs
93
+ // ─────────────────────────────────────────────
94
+ /**
95
+ * Activate the preview overlay system.
96
+ * Returns a cleanup function to tear down on unmount.
97
+ *
98
+ * Called by StorystickProvider when preview mode is detected.
99
+ * Auto-starts the in-iframe snapshot reporter so consumers get the
100
+ * postMessage bridge for free.
101
+ *
102
+ * Idempotency: a module-level refcount ensures repeated calls share
103
+ * a single reporter instance. The first activate() starts the
104
+ * reporter; subsequent calls only increment a counter. Each returned
105
+ * cleanup decrements; the reporter stops once the count reaches zero.
106
+ * This makes the function safe under React StrictMode's double-mount
107
+ * pattern and tolerant of accidental multi-Provider setups.
108
+ *
109
+ * A top-level page is eligible only when its scrubbed URL carries an explicit
110
+ * `_ss_session` selector. The reporter starts after the same-origin token route
111
+ * proves the HttpOnly capability; the public selector alone cannot paint
112
+ * overlays. In standalone mode the reporter stays local, without postMessage.
113
+ * Ordinary customer pages remain inert and do not load this package through
114
+ * the public adapter.
115
+ */
116
+ let activeReporter = null;
117
+ let activeFormContractReporter = null;
118
+ let activeStandaloneOverlayCleanup = null;
119
+ let activeRefCount = 0;
120
+ // A document's Navigation Timing entry outlives activate/cleanup (including
121
+ // StrictMode and grant renewal). Its initial history proof is considered once.
122
+ let initialNavigationObserved = false;
123
+ /**
124
+ * ADR-108 D1 — strip Storystick-internal preview params from a
125
+ * search string before serialising into a `location` envelope OR
126
+ * before comparing observed vs expected tuples for the
127
+ * pendingNavigate match. `_ss_session` is the load-bearing preview
128
+ * gate (drives `detectPreviewMode()`), `_ss_variant` is the
129
+ * variant-shadowing param. Customer-app params (`?utm=...`,
130
+ * `?id=42`) are preserved as-is so the chrome URL display and
131
+ * reverse doc lookup see the actual customer URL.
132
+ *
133
+ * Input: search WITHOUT the leading `?`. Output: same form, with
134
+ * the internal params removed. Empty string → empty string.
135
+ */
136
+ function stripStorystickPreviewParams(search) {
137
+ if (search === '')
138
+ return '';
139
+ // Defensive: callers normalise to "no leading ?" but tolerate
140
+ // both for the comparison path.
141
+ const cleaned = search.startsWith('?') ? search.slice(1) : search;
142
+ if (cleaned === '')
143
+ return '';
144
+ const params = new URLSearchParams(cleaned);
145
+ for (const key of [...params.keys()]) {
146
+ // Includes the one-shot `_ss_activation` capability. It must never be
147
+ // reflected into Studio location state, navigation commands, or logs.
148
+ if (key.startsWith('_ss_'))
149
+ params.delete(key);
150
+ }
151
+ return params.toString();
152
+ }
153
+ function navigationStartPayloadFromBrowseClick(event, target, win = window) {
154
+ if (event.defaultPrevented)
155
+ return null;
156
+ if (event.button !== 0)
157
+ return null;
158
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
159
+ return null;
160
+ }
161
+ if (target.hasAttribute('download'))
162
+ return null;
163
+ if (target.target.length > 0 && target.target !== '_self')
164
+ return null;
165
+ let url;
166
+ try {
167
+ url = new URL(target.href, win.location.href);
168
+ }
169
+ catch {
170
+ return null;
171
+ }
172
+ if (url.origin !== win.location.origin)
173
+ return null;
174
+ const search = stripStorystickPreviewParams(url.search.replace(/^\?/, ''));
175
+ const currentSearch = stripStorystickPreviewParams(win.location.search.replace(/^\?/, ''));
176
+ if (url.pathname === win.location.pathname &&
177
+ search === currentSearch) {
178
+ return null;
179
+ }
180
+ return {
181
+ pathname: url.pathname.slice(0, LOCATION_PATHNAME_MAX),
182
+ search: search.slice(0, LOCATION_SEARCH_MAX),
183
+ hash: url.hash.slice(0, LOCATION_HASH_MAX),
184
+ trigger: 'link-click',
185
+ };
186
+ }
187
+ /**
188
+ * PR-1 — best-effort read of the customer page's code-bundle version,
189
+ * mirroring the same window global the snapshot/overlay path reads
190
+ * (`__STORYSTICK_CODE_VERSION__`). Returns `undefined` when unset so
191
+ * the route-rendered payload omits the optional field rather than
192
+ * sending an empty string the validator would reject.
193
+ */
194
+ function readCodeVersion() {
195
+ if (typeof window === 'undefined')
196
+ return undefined;
197
+ const v = window
198
+ .__STORYSTICK_CODE_VERSION__;
199
+ return typeof v === 'string' && v.length > 0 ? v : undefined;
200
+ }
201
+ export function activate(config) {
202
+ if (typeof window === 'undefined') {
203
+ return () => {
204
+ // No-op cleanup when there is no DOM (SSR).
205
+ };
206
+ }
207
+ const standalone = readStandalonePreviewSessionId(config.projectId) === config.sessionId && config.standaloneCapability === true;
208
+ if (window.parent === window && !standalone) {
209
+ return () => {
210
+ // No-op cleanup when not embedded — postMessage would loop.
211
+ };
212
+ }
213
+ if (standalone && activeStandaloneOverlayCleanup === null) {
214
+ activeStandaloneOverlayCleanup = startStandaloneOverlayController({
215
+ sessionId: config.sessionId,
216
+ });
217
+ }
218
+ if (!standalone && activeReporter === null) {
219
+ // ADR-129 — atomic slice/field snapshot reporting.
220
+ // The route hash is cached per href to avoid recomputing SHA-256 on
221
+ // every observer-driven publish.
222
+ let cachedRouteHash = null;
223
+ activeReporter = startSnapshotReporter({
224
+ routeHashRef: () => {
225
+ if (typeof window === 'undefined')
226
+ return null;
227
+ const href = window.location.href;
228
+ if (cachedRouteHash !== null && cachedRouteHash.href === href) {
229
+ return cachedRouteHash.hash;
230
+ }
231
+ // Compute lazily on the next microtask ; first publish under
232
+ // a brand-new URL will see null (no overlay) until the SHA
233
+ // resolves, then subsequent ones hit the cache.
234
+ void computeRouteHash(href).then((hash) => {
235
+ cachedRouteHash = { href, hash };
236
+ // Wake the reporter once routeHash settles so Studio receives
237
+ // the scoped snapshot without waiting for another mutation.
238
+ activeReporter?.kick('route-hash-ready');
239
+ });
240
+ return null;
241
+ },
242
+ routeUrlRef: () => typeof window !== 'undefined' ? window.location.href : null,
243
+ });
244
+ }
245
+ // ADR-119 D2/D3 — Form-preview viewport-contract reporter. Same
246
+ // refcount + iframe-gate as the siblings: every framework SDK gets
247
+ // the `data-storystick-form-preview` contract + height ACK for free.
248
+ if (!standalone && activeFormContractReporter === null) {
249
+ activeFormContractReporter = startFormContractReporter();
250
+ }
251
+ activeRefCount += 1;
252
+ // The postMessage channel is a secret-free control plane. Browser grants are
253
+ // acquired independently by runtime/store through the same-origin token route.
254
+ let controlCleanup = null;
255
+ let aborted = false;
256
+ if (!standalone && config.projectId) {
257
+ const projectId = config.projectId;
258
+ const parentOriginLock = config.parentOrigin ?? null;
259
+ let teardownEditInteractionGuard = null;
260
+ let teardownMomentaryNavigateKeyRelay = null;
261
+ const navigations = new Map();
262
+ let pendingNavigate = null;
263
+ let nativeNavigation = null;
264
+ let lastNavigateDispatch = null;
265
+ /**
266
+ * Emit a `storystick.preview.location` envelope to the parent
267
+ * Studio. Strips `_ss_session`, `_ss_variant`, and `_ss_release` from the search
268
+ * before sending (preview-channel internals, not customer-app
269
+ * query state). Defensive length-cap pre-truncation via slice —
270
+ * Studio also validates, this is belt-and-braces.
271
+ */
272
+ function emitLocation(pathname, search, hash, commandId) {
273
+ if (parentOriginLock === null)
274
+ return;
275
+ const sanitizedSearch = stripStorystickPreviewParams(search);
276
+ const envelope = {
277
+ namespace: PREVIEW_NAMESPACE,
278
+ version: PREVIEW_PROTOCOL_VERSION,
279
+ type: 'storystick.preview.location',
280
+ sessionId: config.sessionId,
281
+ projectId,
282
+ requestId: createPreviewRequestId(),
283
+ nonce: createPreviewRequestId(),
284
+ sentAt: Date.now(),
285
+ payload: {
286
+ pathname: pathname.slice(0, LOCATION_PATHNAME_MAX),
287
+ search: sanitizedSearch.slice(0, LOCATION_SEARCH_MAX),
288
+ hash: hash.slice(0, LOCATION_HASH_MAX),
289
+ ...(commandId !== undefined && { commandId }),
290
+ },
291
+ };
292
+ window.parent.postMessage(envelope, parentOriginLock);
293
+ }
294
+ function emitNavigationStart(payload) {
295
+ if (parentOriginLock === null)
296
+ return;
297
+ const envelope = {
298
+ namespace: PREVIEW_NAMESPACE,
299
+ version: PREVIEW_PROTOCOL_VERSION,
300
+ type: 'storystick.preview.navigation-start',
301
+ sessionId: config.sessionId,
302
+ projectId,
303
+ requestId: createPreviewRequestId(),
304
+ nonce: createPreviewRequestId(),
305
+ sentAt: Date.now(),
306
+ payload,
307
+ };
308
+ window.parent.postMessage(envelope, parentOriginLock);
309
+ }
310
+ /**
311
+ * PR-1 — emit a `storystick.preview.route-rendered` ack for a
312
+ * Studio-commanded navigation that has reached AND painted its
313
+ * destination. Always carries the matched `commandId` (the caller
314
+ * only invokes this when a commandId was attached). `aborted` +
315
+ * `parentOriginLock` guards mirror `emitLocation` so a deferred
316
+ * emit after teardown is a silent no-op.
317
+ */
318
+ function emitRouteRendered(commandId, presentation) {
319
+ if (aborted)
320
+ return;
321
+ if (lastNavigateDispatch?.commandId === commandId)
322
+ lastNavigateDispatch.rendered = true;
323
+ if (parentOriginLock === null)
324
+ return;
325
+ // codeVersion is optional telemetry. If the customer page set a
326
+ // pathologically long `__STORYSTICK_CODE_VERSION__`, including it
327
+ // would push the payload past the Studio validator's bound and
328
+ // DROP the whole envelope — losing the load-bearing commandId ack.
329
+ // Omit it instead so the confirmation signal always survives
330
+ // (codex PR-1 review §P2).
331
+ const codeVersionRaw = readCodeVersion();
332
+ const codeVersion = codeVersionRaw !== undefined &&
333
+ codeVersionRaw.length <= ROUTE_RENDERED_CODE_VERSION_MAX
334
+ ? codeVersionRaw
335
+ : undefined;
336
+ const envelope = {
337
+ namespace: PREVIEW_NAMESPACE,
338
+ version: PREVIEW_PROTOCOL_VERSION,
339
+ type: 'storystick.preview.route-rendered',
340
+ sessionId: config.sessionId,
341
+ projectId,
342
+ requestId: createPreviewRequestId(),
343
+ nonce: createPreviewRequestId(),
344
+ sentAt: Date.now(),
345
+ payload: {
346
+ commandId,
347
+ ...(presentation ? { presentation } : {}),
348
+ ...(codeVersion !== undefined && { codeVersion }),
349
+ },
350
+ };
351
+ window.parent.postMessage(envelope, parentOriginLock);
352
+ }
353
+ /**
354
+ * Defer the route-rendered ack until after the browser has painted
355
+ * the committed route. Uses a DOUBLE rAF: the second callback runs
356
+ * after the paint of the frame the first callback observed, so the
357
+ * ack is post-paint REGARDLESS of how the adapter delivered the
358
+ * transition — from inside its own rAF (Next App Router), from a
359
+ * router event (Pages Router `routeChangeComplete`), or
360
+ * synchronously during a commit (a future adapter). A single rAF is
361
+ * post-paint ONLY when the transition was itself delivered from a
362
+ * prior rAF, so it could clear the stale mask pre-paint on the other
363
+ * delivery paths (codex PR-1 review §P1). SSR / jsdom-without-rAF
364
+ * emits synchronously so tests stay deterministic.
365
+ */
366
+ function scheduleRouteRenderedAck(commandId, presentation) {
367
+ if (parentOriginLock === null)
368
+ return;
369
+ if (typeof window === 'undefined' ||
370
+ typeof window.requestAnimationFrame !== 'function') {
371
+ emitRouteRendered(commandId, presentation);
372
+ return;
373
+ }
374
+ const startedAt = previewPerfNow(window);
375
+ window.requestAnimationFrame(() => {
376
+ window.requestAnimationFrame(() => {
377
+ if (!aborted)
378
+ recordPreviewPerfMeasure(window, 'preview.navigate.paintAck', startedAt, { hasPresentation: presentation !== undefined });
379
+ emitRouteRendered(commandId, presentation);
380
+ });
381
+ });
382
+ }
383
+ function emitOverlayReady(payload) {
384
+ if (parentOriginLock === null)
385
+ return;
386
+ const envelope = {
387
+ namespace: PREVIEW_NAMESPACE,
388
+ version: PREVIEW_PROTOCOL_VERSION,
389
+ type: 'storystick.preview.overlay-ready',
390
+ sessionId: config.sessionId,
391
+ projectId,
392
+ requestId: createPreviewRequestId(),
393
+ nonce: createPreviewRequestId(),
394
+ sentAt: Date.now(),
395
+ payload,
396
+ };
397
+ window.parent.postMessage(envelope, parentOriginLock);
398
+ }
399
+ function emitOverlayClick(payload) {
400
+ if (parentOriginLock === null)
401
+ return;
402
+ const envelope = {
403
+ namespace: PREVIEW_NAMESPACE,
404
+ version: PREVIEW_PROTOCOL_VERSION,
405
+ type: 'storystick.preview.overlay-click',
406
+ sessionId: config.sessionId,
407
+ projectId,
408
+ requestId: createPreviewRequestId(),
409
+ nonce: createPreviewRequestId(),
410
+ sentAt: Date.now(),
411
+ payload,
412
+ };
413
+ window.parent.postMessage(envelope, parentOriginLock);
414
+ }
415
+ function emitIframeInteraction(payload) {
416
+ if (parentOriginLock === null)
417
+ return;
418
+ const envelope = {
419
+ namespace: PREVIEW_NAMESPACE,
420
+ version: PREVIEW_PROTOCOL_VERSION,
421
+ type: 'storystick.preview.iframe-interaction',
422
+ sessionId: config.sessionId,
423
+ projectId,
424
+ requestId: createPreviewRequestId(),
425
+ nonce: createPreviewRequestId(),
426
+ sentAt: Date.now(),
427
+ payload,
428
+ };
429
+ window.parent.postMessage(envelope, parentOriginLock);
430
+ }
431
+ let lastRouterLocation = {
432
+ pathname: window.location.pathname,
433
+ search: stripStorystickPreviewParams(window.location.search),
434
+ hash: window.location.hash,
435
+ };
436
+ function onRouterTransition(transition) {
437
+ const committed = {
438
+ pathname: transition.pathname,
439
+ search: stripStorystickPreviewParams(transition.search),
440
+ hash: transition.hash,
441
+ };
442
+ if (transition.commandId !== undefined) {
443
+ navigations.delete(transition.commandId);
444
+ if (pendingNavigate?.commandId === transition.commandId)
445
+ pendingNavigate = null;
446
+ lastRouterLocation = committed;
447
+ emitLocation(transition.pathname, transition.search, transition.hash, transition.commandId);
448
+ scheduleRouteRenderedAck(transition.commandId, transition.presentation);
449
+ return;
450
+ }
451
+ const matchesNative = nativeNavigation !== null &&
452
+ (nativeNavigation.pathname === committed.pathname || nativeNavigation.pathname === transition.browserPathname) &&
453
+ nativeNavigation.search === committed.search &&
454
+ nativeNavigation.hash === committed.hash;
455
+ const matches = (command) => command.expectedPathname === committed.pathname &&
456
+ stripStorystickPreviewParams(command.expectedSearch) === committed.search &&
457
+ command.expectedHash === committed.hash;
458
+ const matched = matchesNative ? undefined :
459
+ [...navigations.values()].reverse().find(matches);
460
+ const command = matchesNative ? null : matched ?? pendingNavigate;
461
+ const commandId = command?.commandId;
462
+ if (matchesNative) {
463
+ // Browser gestures include a framework basePath; adapter commits use
464
+ // customer route coordinates. Correlate the normalized intent too.
465
+ if (nativeNavigation.pathname !== committed.pathname) {
466
+ emitNavigationStart({ ...committed, trigger: nativeNavigation.trigger });
467
+ }
468
+ nativeNavigation = null;
469
+ pendingNavigate = null;
470
+ }
471
+ else if (matched?.commandId === pendingNavigate?.commandId) {
472
+ pendingNavigate = null;
473
+ }
474
+ if (matched !== undefined)
475
+ navigations.delete(matched.commandId);
476
+ const changed = committed.pathname !== lastRouterLocation.pathname ||
477
+ committed.search !== lastRouterLocation.search ||
478
+ committed.hash !== lastRouterLocation.hash;
479
+ lastRouterLocation = committed;
480
+ if (changed && commandId === undefined && !matchesNative) {
481
+ emitNavigationStart({ ...committed, trigger: 'router-transition' });
482
+ }
483
+ emitLocation(transition.pathname, transition.search, transition.hash, commandId);
484
+ // A redirect retains provenance but does not confirm the requested URL.
485
+ if (matched !== undefined)
486
+ scheduleRouteRenderedAck(matched.commandId);
487
+ }
488
+ setRouterSubscriber(onRouterTransition);
489
+ const onBrowseLinkClick = (event) => {
490
+ if (event.defaultPrevented)
491
+ return;
492
+ const target = getAnchorFromPreviewClickEvent(event, window);
493
+ if (target === null)
494
+ return;
495
+ if (getInteractionMode() !== 'browse')
496
+ return;
497
+ normalizeMomentaryNavigateLinkClick(event, target, window);
498
+ const navigationStart = navigationStartPayloadFromBrowseClick(event, target, window);
499
+ if (navigationStart !== null) {
500
+ nativeNavigation = navigationStart;
501
+ pendingNavigate = null;
502
+ emitNavigationStart(navigationStart);
503
+ }
504
+ };
505
+ window.document.addEventListener('click', onBrowseLinkClick, true);
506
+ const onHistoryNavigation = () => {
507
+ nativeNavigation = {
508
+ pathname: window.location.pathname,
509
+ search: stripStorystickPreviewParams(window.location.search),
510
+ hash: window.location.hash,
511
+ trigger: 'router-transition',
512
+ };
513
+ pendingNavigate = null;
514
+ emitNavigationStart(nativeNavigation);
515
+ // History already committed the browser URL. A restored React tree may
516
+ // not rerender, so no adapter callback is guaranteed to report it again.
517
+ emitLocation(nativeNavigation.pathname, nativeNavigation.search, nativeNavigation.hash, undefined);
518
+ };
519
+ window.addEventListener('popstate', onHistoryNavigation);
520
+ const onPageShow = (event) => {
521
+ if (event.persisted)
522
+ onHistoryNavigation();
523
+ };
524
+ window.addEventListener('pageshow', onPageShow);
525
+ teardownEditInteractionGuard = installPreviewEditInteractionGuard(window, {
526
+ onInteraction: emitIframeInteraction,
527
+ });
528
+ const teardownWorkspaceShortcuts = installPreviewWorkspaceShortcuts(window, () => {
529
+ const state = previewRuntime.getState();
530
+ return state.previewEnded ? null : state.identity;
531
+ }, undefined, previewRuntime.subscribeIdentity);
532
+ teardownMomentaryNavigateKeyRelay = installMomentaryNavigateKeyRelay(window, {
533
+ getTargetOrigin: () => parentOriginLock,
534
+ });
535
+ let initialHistoryNavigation = false;
536
+ if (!initialNavigationObserved && parentOriginLock !== null) {
537
+ initialNavigationObserved = true;
538
+ const navigation = window.performance.getEntriesByType?.('navigation')?.[0];
539
+ if (navigation?.type === 'back_forward') {
540
+ try {
541
+ const loaded = new URL(navigation.name);
542
+ // A late activation after SPA navigation cannot lend an older
543
+ // document's history proof to its new address. Internal selectors
544
+ // and the current fragment do not change the loaded document.
545
+ initialHistoryNavigation = loaded.origin === window.location.origin &&
546
+ loaded.pathname === window.location.pathname &&
547
+ stripStorystickPreviewParams(loaded.search) === stripStorystickPreviewParams(window.location.search);
548
+ }
549
+ catch { /* An unavailable browser proof leaves boot observational. */ }
550
+ }
551
+ }
552
+ if (initialHistoryNavigation)
553
+ onHistoryNavigation();
554
+ else
555
+ emitLocation(window.location.pathname, window.location.search.replace(/^\?/, ''), window.location.hash, undefined);
556
+ // Listen for cross-frame `navigate` messages. The validator
557
+ // rejects pre-lock messages outright (codex Lane D round 1 P1
558
+ // fix) so a same-host script with a valid envelope shape can't
559
+ // drive `history.pushState` before the legitimate Studio
560
+ // handshake finishes.
561
+ //
562
+ // ADR-108 — same listener also routes `interaction-mode` envelopes
563
+ // (Studio→iframe click-routing flag).
564
+ const onMessage = (event) => {
565
+ const base = validatePreviewEnvelope(event.data);
566
+ if (base.ok &&
567
+ base.message.type === 'storystick.preview.session-broadcast' &&
568
+ parentOriginLock !== null &&
569
+ event.origin === parentOriginLock &&
570
+ base.message.sessionId === config.sessionId &&
571
+ base.message.projectId === projectId) {
572
+ config.onSessionBroadcast?.();
573
+ return;
574
+ }
575
+ if (window.__storystickPreviewOverlayMapOwner !== 'boot') {
576
+ const overlayMap = validateOverlayMap(event.data, event.origin, config.sessionId, projectId, parentOriginLock);
577
+ if (overlayMap.ok) {
578
+ const result = applyPreviewOverlayMap(overlayMap.envelope.payload, window, {
579
+ onClick: emitOverlayClick,
580
+ });
581
+ emitOverlayReady({
582
+ generation: result.generation,
583
+ paintedCount: result.paintedCount,
584
+ missingSnapshotItemKeys: result.missingSnapshotItemKeys,
585
+ routeHash: overlayMap.envelope.payload.routeHash,
586
+ codeVersion: overlayMap.envelope.payload.codeVersion,
587
+ renderId: overlayMap.envelope.payload.renderId,
588
+ });
589
+ return;
590
+ }
591
+ const overlayClear = validateOverlayClear(event.data, event.origin, config.sessionId, projectId, parentOriginLock);
592
+ if (overlayClear.ok) {
593
+ clearPreviewOverlayPaint();
594
+ return;
595
+ }
596
+ }
597
+ // ADR-108 D2 — interaction-mode envelope routed first because
598
+ // it has its own discriminator + parentOrigin lock guard. The
599
+ // `validateNavigate` path below short-circuits on the wrong
600
+ // type so this ordering doesn't conflict.
601
+ if (base.ok &&
602
+ base.message.type === 'storystick.preview.interaction-mode') {
603
+ if (parentOriginLock === null)
604
+ return;
605
+ if (event.origin !== parentOriginLock)
606
+ return;
607
+ if (base.message.sessionId !== config.sessionId)
608
+ return;
609
+ if (base.message.projectId !== projectId)
610
+ return;
611
+ setInteractionMode(base.message.payload.mode);
612
+ return;
613
+ }
614
+ if (base.ok && base.message.type === 'storystick.preview.reload') {
615
+ if (parentOriginLock === null)
616
+ return;
617
+ if (event.origin !== parentOriginLock)
618
+ return;
619
+ if (base.message.sessionId !== config.sessionId)
620
+ return;
621
+ if (base.message.projectId !== projectId)
622
+ return;
623
+ const handler = getReloadHandler();
624
+ handler(base.message.payload.mode, {
625
+ commandId: base.message.payload.commandId,
626
+ });
627
+ // C3 — a soft reload is commonly implemented by framework
628
+ // adapters as `router.refresh()`. The displayed URL does not
629
+ // change in that case, so the router-location subscriber has no
630
+ // transition to report. Studio deliberately clears its observed
631
+ // renderer state before dispatching reload; explicitly re-observe
632
+ // the current route so a later overlay-ready ACK can be matched to
633
+ // the unchanged displayed epoch instead of being rejected as
634
+ // route-drift. This is route evidence only — paint readiness still
635
+ // requires the generation-scoped overlay-ready ACK.
636
+ if (base.message.payload.mode === 'soft') {
637
+ emitLocation(window.location.pathname, window.location.search.replace(/^\?/, ''), window.location.hash, undefined);
638
+ }
639
+ return;
640
+ }
641
+ const check = validateNavigate(event.data, event.origin, config.sessionId, projectId, parentOriginLock);
642
+ if (!check.ok)
643
+ return;
644
+ // ADR-108 D3 — capture the expected-destination tuple BEFORE
645
+ // invoking the framework adapter's handler so the next
646
+ // observed router transition can match-and-attach. The
647
+ // navigate envelope's `pathname` may include `?...#...`
648
+ // (validator allows /path?q=1#h); split it into the three
649
+ // parts so the subscriber-reported tuple can be compared
650
+ // directly.
651
+ const rawPath = check.envelope.payload.pathname;
652
+ const hashIdx = rawPath.indexOf('#');
653
+ const hash = hashIdx >= 0 ? rawPath.slice(hashIdx) : '';
654
+ const pathWithQuery = hashIdx >= 0 ? rawPath.slice(0, hashIdx) : rawPath;
655
+ const queryIdx = pathWithQuery.indexOf('?');
656
+ const expectedPathname = queryIdx >= 0 ? pathWithQuery.slice(0, queryIdx) : pathWithQuery;
657
+ const expectedSearch = queryIdx >= 0 ? pathWithQuery.slice(queryIdx + 1) : '';
658
+ const handler = getNavigateHandler();
659
+ if (lastNavigateDispatch?.commandId === check.envelope.payload.commandId &&
660
+ lastNavigateDispatch.handler === handler) {
661
+ // A lost ACK may cause an already-rendered command to be delivered again.
662
+ // Re-ack it without re-arming a navigation that could capture a future Browse.
663
+ if (lastNavigateDispatch.rendered && lastRouterLocation.pathname === expectedPathname &&
664
+ lastRouterLocation.search === stripStorystickPreviewParams(expectedSearch) &&
665
+ lastRouterLocation.hash === hash) {
666
+ emitLocation(lastRouterLocation.pathname, lastRouterLocation.search, hash, lastNavigateDispatch.commandId);
667
+ scheduleRouteRenderedAck(lastNavigateDispatch.commandId);
668
+ }
669
+ return;
670
+ }
671
+ pendingNavigate = {
672
+ commandId: check.envelope.payload.commandId,
673
+ expectedPathname,
674
+ expectedSearch,
675
+ expectedHash: hash,
676
+ };
677
+ navigations.set(pendingNavigate.commandId, pendingNavigate);
678
+ while (navigations.size > 64)
679
+ navigations.delete(navigations.keys().next().value);
680
+ nativeNavigation = null;
681
+ // Studio retries until route-rendered because postMessage acceptance is
682
+ // not delivery proof. Do not restart a slow framework navigation for the
683
+ // same adapter; do retry when no adapter was ready yet or HMR installed a
684
+ // new handler function for the still-pending command.
685
+ if (!hasNavigateHandler())
686
+ return;
687
+ lastNavigateDispatch = {
688
+ commandId: check.envelope.payload.commandId,
689
+ handler,
690
+ rendered: false,
691
+ };
692
+ recordPreviewPerfMetric(window, { name: 'preview.navigate.received', detail: {
693
+ refresh: check.envelope.payload.refresh === true,
694
+ contextCommand: check.envelope.payload.contextCommand !== undefined,
695
+ } });
696
+ // ADR-111 — forward the optional `refresh` flag so App Router
697
+ // adapters can invalidate the client Router Cache on layout-
698
+ // preview-state changes. Validator guarantees `refresh` is a
699
+ // boolean when present; we coerce to a strict boolean here so
700
+ // adapters never see `undefined`-as-truthy.
701
+ const refresh = check.envelope.payload.refresh === true;
702
+ handler(check.envelope.payload.pathname, {
703
+ refresh,
704
+ scope: check.envelope.payload.scope,
705
+ commandId: check.envelope.payload.commandId,
706
+ ...(check.envelope.payload.contextCommand ? {
707
+ contextCommand: check.envelope.payload.contextCommand,
708
+ } : {}),
709
+ });
710
+ };
711
+ window.addEventListener('message', onMessage);
712
+ controlCleanup = () => {
713
+ aborted = true;
714
+ window.removeEventListener('message', onMessage);
715
+ window.document.removeEventListener('click', onBrowseLinkClick, true);
716
+ window.removeEventListener('popstate', onHistoryNavigation);
717
+ window.removeEventListener('pageshow', onPageShow);
718
+ teardownWorkspaceShortcuts();
719
+ teardownMomentaryNavigateKeyRelay?.();
720
+ teardownMomentaryNavigateKeyRelay = null;
721
+ teardownEditInteractionGuard?.();
722
+ teardownEditInteractionGuard = null;
723
+ // ADR-108 — clear router subscriber so StrictMode remount or
724
+ // future activate() doesn't see a stale handler from the
725
+ // teardown lane.
726
+ setRouterSubscriber(null);
727
+ };
728
+ }
729
+ let released = false;
730
+ return () => {
731
+ if (released)
732
+ return;
733
+ released = true;
734
+ if (controlCleanup !== null) {
735
+ controlCleanup();
736
+ controlCleanup = null;
737
+ }
738
+ activeRefCount -= 1;
739
+ if (activeRefCount <= 0) {
740
+ activeRefCount = 0;
741
+ activeReporter?.stop();
742
+ activeReporter = null;
743
+ activeFormContractReporter?.stop();
744
+ activeFormContractReporter = null;
745
+ activeStandaloneOverlayCleanup?.();
746
+ activeStandaloneOverlayCleanup = null;
747
+ clearPreviewOverlayPaint();
748
+ }
749
+ };
750
+ }
751
+ /**
752
+ * Test-only hook to drop the singleton between cases. Vitest modules
753
+ * are isolated per file, but a single file with multiple `activate`
754
+ * calls would share state without this reset.
755
+ */
756
+ export function _resetActivateForTests() {
757
+ activeReporter?.stop();
758
+ activeReporter = null;
759
+ activeFormContractReporter?.stop();
760
+ activeFormContractReporter = null;
761
+ activeStandaloneOverlayCleanup?.();
762
+ activeStandaloneOverlayCleanup = null;
763
+ clearPreviewOverlayPaint();
764
+ activeRefCount = 0;
765
+ initialNavigationObserved = false;
766
+ }
767
+ /**
768
+ * Emit a `PreviewNavMessage` to the parent Studio. No-op when not
769
+ * embedded (top-level window or SSR) — same guard as `activate`.
770
+ *
771
+ * Origin :
772
+ * - When `targetOrigin` is omitted, defaults to `window.location.origin`.
773
+ * Per ADR-041 the studio + preview share a cookie domain, which means
774
+ * they share an origin in practice — same-origin posting works.
775
+ * - Pass an explicit `targetOrigin` if a future deployment puts the
776
+ * preview on a different origin (cross-domain) ; the studio listener
777
+ * would then need a documented allowlist on its side.
778
+ */
779
+ export function postNavigateToParent(msg, targetOrigin) {
780
+ if (typeof window === 'undefined')
781
+ return;
782
+ if (window.parent === window)
783
+ return;
784
+ const origin = targetOrigin ?? window.location.origin;
785
+ window.parent.postMessage(msg, origin);
786
+ }
787
+ // The Proxy tracking surface is implemented in `runtime/field-proxy.ts`
788
+ // and re-exported near the top of this module.