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

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 (247) hide show
  1. package/LICENSE +112 -0
  2. package/PUBLIC-API.md +56 -0
  3. package/README.md +42 -2
  4. package/dist/.source-hash +1 -0
  5. package/dist/authoring/runtime-contract.d.ts +92 -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/page-presentation.d.ts +24 -0
  72. package/dist/page-presentation.js +32 -0
  73. package/dist/provenance/anchor-map.d.ts +95 -0
  74. package/dist/provenance/anchor-map.js +177 -0
  75. package/dist/provenance/carrier-plan.d.ts +51 -0
  76. package/dist/provenance/carrier-plan.js +90 -0
  77. package/dist/provenance/coded-text.d.ts +20 -0
  78. package/dist/provenance/coded-text.js +302 -0
  79. package/dist/provenance/direct-edit-locator.d.ts +29 -0
  80. package/dist/provenance/direct-edit-locator.js +218 -0
  81. package/dist/provenance/editable-leaf.d.ts +134 -0
  82. package/dist/provenance/editable-leaf.js +483 -0
  83. package/dist/provenance/editor-target.d.ts +29 -0
  84. package/dist/provenance/editor-target.js +46 -0
  85. package/dist/provenance/field-leaf-walker.d.ts +21 -0
  86. package/dist/provenance/field-leaf-walker.js +98 -0
  87. package/dist/provenance/frame-dependencies.d.ts +212 -0
  88. package/dist/provenance/frame-dependencies.js +550 -0
  89. package/dist/provenance/index.d.ts +26 -0
  90. package/dist/provenance/index.js +26 -0
  91. package/dist/provenance/local-events.d.ts +1 -0
  92. package/dist/provenance/local-events.js +1 -0
  93. package/dist/provenance/markers.d.ts +21 -0
  94. package/dist/provenance/markers.js +70 -0
  95. package/dist/provenance/multi-frame-anchor-map.d.ts +54 -0
  96. package/dist/provenance/multi-frame-anchor-map.js +109 -0
  97. package/dist/provenance/normalize.d.ts +2 -0
  98. package/dist/provenance/normalize.js +19 -0
  99. package/dist/provenance/overlay-focus-echo.d.ts +17 -0
  100. package/dist/provenance/overlay-focus-echo.js +16 -0
  101. package/dist/provenance/overlay-projection.d.ts +65 -0
  102. package/dist/provenance/overlay-projection.js +201 -0
  103. package/dist/provenance/owner-map.d.ts +55 -0
  104. package/dist/provenance/owner-map.js +239 -0
  105. package/dist/provenance/read-carrier.d.ts +22 -0
  106. package/dist/provenance/read-carrier.js +92 -0
  107. package/dist/provenance/recorder.d.ts +38 -0
  108. package/dist/provenance/recorder.js +82 -0
  109. package/dist/provenance/render-context.d.ts +106 -0
  110. package/dist/provenance/render-context.js +338 -0
  111. package/dist/provenance/reveal-contract.d.ts +44 -0
  112. package/dist/provenance/reveal-contract.js +1 -0
  113. package/dist/provenance/richtext-slice-embed-address.d.ts +20 -0
  114. package/dist/provenance/richtext-slice-embed-address.js +35 -0
  115. package/dist/provenance/runtime-script.d.ts +1 -0
  116. package/dist/provenance/runtime-script.js +2217 -0
  117. package/dist/provenance/sidecar-store.d.ts +63 -0
  118. package/dist/provenance/sidecar-store.js +151 -0
  119. package/dist/provenance/sidecar.d.ts +34 -0
  120. package/dist/provenance/sidecar.js +227 -0
  121. package/dist/provenance/slice-model.d.ts +28 -0
  122. package/dist/provenance/slice-model.js +86 -0
  123. package/dist/provenance/source-identity.d.ts +3 -0
  124. package/dist/provenance/source-identity.js +7 -0
  125. package/dist/provenance/source-map-materializer.d.ts +76 -0
  126. package/dist/provenance/source-map-materializer.js +286 -0
  127. package/dist/provenance/source-registry.d.ts +103 -0
  128. package/dist/provenance/source-registry.js +562 -0
  129. package/dist/provenance/transformed-output-gate.d.ts +161 -0
  130. package/dist/provenance/transformed-output-gate.js +171 -0
  131. package/dist/provenance/types.d.ts +102 -0
  132. package/dist/provenance/types.js +1 -0
  133. package/dist/refresh-policy.d.ts +11 -0
  134. package/dist/refresh-policy.js +14 -0
  135. package/dist/reveal-target.d.ts +8 -0
  136. package/dist/reveal-target.js +47 -0
  137. package/dist/route-paths.d.ts +11 -0
  138. package/dist/route-paths.js +14 -0
  139. package/dist/router-subscriber.d.ts +92 -0
  140. package/dist/router-subscriber.js +44 -0
  141. package/dist/runtime/anchor-types.d.ts +5 -0
  142. package/dist/runtime/anchor-types.js +1 -0
  143. package/dist/runtime/authority-store.d.ts +31 -0
  144. package/dist/runtime/authority-store.js +56 -0
  145. package/dist/runtime/boot.d.ts +52 -0
  146. package/dist/runtime/boot.js +379 -0
  147. package/dist/runtime/content-refresh.d.ts +30 -0
  148. package/dist/runtime/content-refresh.js +68 -0
  149. package/dist/runtime/emit-runtime-error.d.ts +51 -0
  150. package/dist/runtime/emit-runtime-error.js +64 -0
  151. package/dist/runtime/field-proxy.d.ts +112 -0
  152. package/dist/runtime/field-proxy.js +471 -0
  153. package/dist/runtime/form-contract-reporter.d.ts +99 -0
  154. package/dist/runtime/form-contract-reporter.js +391 -0
  155. package/dist/runtime/invalidation-adapter.d.ts +17 -0
  156. package/dist/runtime/invalidation-adapter.js +42 -0
  157. package/dist/runtime/live-query-overlay.d.ts +39 -0
  158. package/dist/runtime/live-query-overlay.js +345 -0
  159. package/dist/runtime/momentary-navigate.d.ts +16 -0
  160. package/dist/runtime/momentary-navigate.js +106 -0
  161. package/dist/runtime/observation-scheduler.d.ts +15 -0
  162. package/dist/runtime/observation-scheduler.js +42 -0
  163. package/dist/runtime/perf-metrics.d.ts +28 -0
  164. package/dist/runtime/perf-metrics.js +90 -0
  165. package/dist/runtime/preview-interaction-policy.d.ts +1 -0
  166. package/dist/runtime/preview-interaction-policy.js +103 -0
  167. package/dist/runtime/preview-overlay-painter.d.ts +27 -0
  168. package/dist/runtime/preview-overlay-painter.js +1732 -0
  169. package/dist/runtime/preview-overlay-sidecar.d.ts +26 -0
  170. package/dist/runtime/preview-overlay-sidecar.js +45 -0
  171. package/dist/runtime/preview-perspective-alignment.d.ts +15 -0
  172. package/dist/runtime/preview-perspective-alignment.js +40 -0
  173. package/dist/runtime/preview-perspective-intent-state.d.ts +23 -0
  174. package/dist/runtime/preview-perspective-intent-state.js +5 -0
  175. package/dist/runtime/preview-transition-state.d.ts +142 -0
  176. package/dist/runtime/preview-transition-state.js +257 -0
  177. package/dist/runtime/render-mode-state.d.ts +51 -0
  178. package/dist/runtime/render-mode-state.js +76 -0
  179. package/dist/runtime/request-id.d.ts +4 -0
  180. package/dist/runtime/request-id.js +8 -0
  181. package/dist/runtime/resolve-schema-field.d.ts +99 -0
  182. package/dist/runtime/resolve-schema-field.js +356 -0
  183. package/dist/runtime/runtime-error-dispatch.d.ts +23 -0
  184. package/dist/runtime/runtime-error-dispatch.js +75 -0
  185. package/dist/runtime/schema-store.d.ts +121 -0
  186. package/dist/runtime/schema-store.js +667 -0
  187. package/dist/runtime/snapshot-assembly.d.ts +104 -0
  188. package/dist/runtime/snapshot-assembly.js +553 -0
  189. package/dist/runtime/snapshot-reporter.d.ts +55 -0
  190. package/dist/runtime/snapshot-reporter.js +375 -0
  191. package/dist/runtime/standalone-overlay-controller.d.ts +33 -0
  192. package/dist/runtime/standalone-overlay-controller.js +281 -0
  193. package/dist/runtime/store.d.ts +307 -0
  194. package/dist/runtime/store.js +1026 -0
  195. package/dist/runtime/windowed-minimap-projection.d.ts +28 -0
  196. package/dist/runtime/windowed-minimap-projection.js +189 -0
  197. package/dist/runtime/workspace-shortcuts.d.ts +30 -0
  198. package/dist/runtime/workspace-shortcuts.js +188 -0
  199. package/dist/sdk-error-copy.d.ts +48 -0
  200. package/dist/sdk-error-copy.js +182 -0
  201. package/dist/select-cursor.d.ts +2 -0
  202. package/dist/select-cursor.js +23 -0
  203. package/dist/server/admission-cookie.d.ts +38 -0
  204. package/dist/server/admission-cookie.js +122 -0
  205. package/dist/server/control-client.d.ts +29 -0
  206. package/dist/server/control-client.js +22 -0
  207. package/dist/server/control-errors.d.ts +22 -0
  208. package/dist/server/control-errors.js +182 -0
  209. package/dist/server/draft-route.d.ts +12 -0
  210. package/dist/server/draft-route.js +45 -0
  211. package/dist/server/extract-token.d.ts +30 -0
  212. package/dist/server/extract-token.js +98 -0
  213. package/dist/server/grant.d.ts +128 -0
  214. package/dist/server/grant.js +395 -0
  215. package/dist/server/ordinary-read-provenance.d.ts +11 -0
  216. package/dist/server/ordinary-read-provenance.js +124 -0
  217. package/dist/server/preview-read-metadata.d.ts +10 -0
  218. package/dist/server/preview-read-metadata.js +27 -0
  219. package/dist/server/provenance-frame-builder.d.ts +53 -0
  220. package/dist/server/provenance-frame-builder.js +102 -0
  221. package/dist/server/provenance-runtime.d.ts +70 -0
  222. package/dist/server/provenance-runtime.js +46 -0
  223. package/dist/server/read-context-command.d.ts +11 -0
  224. package/dist/server/read-context-command.js +53 -0
  225. package/dist/server/read-identity.d.ts +3 -0
  226. package/dist/server/read-identity.js +63 -0
  227. package/dist/server/read-lifecycle.d.ts +17 -0
  228. package/dist/server/read-lifecycle.js +52 -0
  229. package/dist/server/render-frame-provenance.d.ts +133 -0
  230. package/dist/server/render-frame-provenance.js +676 -0
  231. package/dist/server/render-id.d.ts +15 -0
  232. package/dist/server/render-id.js +22 -0
  233. package/dist/server/route-document-provenance.d.ts +77 -0
  234. package/dist/server/route-document-provenance.js +1327 -0
  235. package/dist/server/route-session-cookie.d.ts +33 -0
  236. package/dist/server/route-session-cookie.js +54 -0
  237. package/dist/session-projections.d.ts +33 -0
  238. package/dist/session-projections.js +43 -0
  239. package/dist/session-subscriber.d.ts +124 -0
  240. package/dist/session-subscriber.js +79 -0
  241. package/dist/sha256-fallback.d.ts +30 -0
  242. package/dist/sha256-fallback.js +126 -0
  243. package/dist/types.d.ts +446 -0
  244. package/dist/types.js +85 -0
  245. package/dist/validators.d.ts +61 -0
  246. package/dist/validators.js +110 -0
  247. package/package.json +210 -3
@@ -0,0 +1,84 @@
1
+ /**
2
+ * ADR-116 D2 — bidirectional session sync over postMessage.
3
+ *
4
+ * The iframe SDK posts `session-request` to `window.parent` at boot AND
5
+ * on every `disconnected → resolving-identity` recovery transition. The
6
+ * Studio side responds with `session-broadcast` (and also proactively
7
+ * broadcasts on every iframe `load` once `activeSessionIdRef.current`
8
+ * is non-null). Both messages converge into the resolver's D10
9
+ * replacement table : duplicates are idempotent, conflicting projectIds
10
+ * are rejected.
11
+ *
12
+ * targetOrigin policy : the SDK does NOT know the Studio origin until
13
+ * a broadcast lands. Outbound session-request uses `targetOrigin = '*'`
14
+ * (the only sensible default pre-handshake). Inbound broadcasts are
15
+ * validated by origin allowlist + envelope shape ; after the first
16
+ * confirmed broadcast the iframe locks the parent origin to
17
+ * `event.origin` and refuses subsequent envelopes from any other.
18
+ */
19
+ import { type SessionRequestEnvelope } from '@storystick/transport-allowlist';
20
+ import type { ProvisionalIdentity } from '../identity-types.js';
21
+ /**
22
+ * Build a fresh `session-request` envelope. Pure — no side effects.
23
+ *
24
+ * The iframe does not yet know its sessionId, so the envelope carries
25
+ * an empty string for that field (legal exception in the validator
26
+ * for this type only).
27
+ */
28
+ export declare function buildSessionRequest(args: {
29
+ projectId: string;
30
+ reason: 'boot' | 'recovery';
31
+ now?: () => number;
32
+ randomHex?: () => string;
33
+ }): SessionRequestEnvelope;
34
+ /**
35
+ * Inbound broadcast validator. Wraps the shared `validatePreviewEnvelope`
36
+ * plus a projectId tie-breaker so the carrier returns the
37
+ * `ProvisionalIdentity` directly without the caller doing its own type
38
+ * narrowing.
39
+ *
40
+ * `eventOrigin` is the postMessage `MessageEvent.origin`. The resolver
41
+ * locks the iframe's parent origin to this value on the first confirmed
42
+ * broadcast (D2 targetOrigin policy).
43
+ */
44
+ export declare function parseInboundBroadcast(raw: unknown, eventOrigin: string, expectedProjectId: string): {
45
+ ok: true;
46
+ identity: ProvisionalIdentity;
47
+ } | {
48
+ ok: false;
49
+ reason: string;
50
+ };
51
+ /**
52
+ * Bind a `message` listener that forwards every well-formed
53
+ * `session-broadcast` envelope to the resolver callback. Returns the
54
+ * disposer. The caller MUST invoke the disposer on teardown (resolver
55
+ * stop, runtime dispose, framework HMR re-mount).
56
+ *
57
+ * The listener does NOT validate origin against an allowlist : that
58
+ * lookup lives Studio-side per ADR-087 §3.7 (projects.allowedOrigins).
59
+ * Iframe-side, the resolver locks `parentOrigin` to the first
60
+ * confirmed broadcast's origin and refuses subsequent broadcasts from
61
+ * any other origin. The lock is applied AFTER `parseInboundBroadcast`
62
+ * returns ok ; this function is a pure pipe.
63
+ */
64
+ export declare function attachBroadcastListener(args: {
65
+ expectedProjectId: string;
66
+ onBroadcast: (identity: ProvisionalIdentity, eventOrigin: string) => void;
67
+ /** Optional override for the global window — tests pass a stub. */
68
+ win?: Window;
69
+ }): () => void;
70
+ /**
71
+ * Post a `session-request` envelope to `window.parent`. Returns true
72
+ * if a parent exists ; false in SSR / detached contexts. The envelope
73
+ * is sent with `targetOrigin = '*'` because the iframe does not yet
74
+ * know the Studio origin (chicken-and-egg with the broadcast that
75
+ * would teach it).
76
+ */
77
+ export declare function postSessionRequest(args: {
78
+ projectId: string;
79
+ reason: 'boot' | 'recovery';
80
+ /** Optional override for `window.parent` — tests pass a stub. */
81
+ parent?: Window;
82
+ now?: () => number;
83
+ randomHex?: () => string;
84
+ }): boolean;
@@ -0,0 +1,177 @@
1
+ /**
2
+ * ADR-116 D2 — bidirectional session sync over postMessage.
3
+ *
4
+ * The iframe SDK posts `session-request` to `window.parent` at boot AND
5
+ * on every `disconnected → resolving-identity` recovery transition. The
6
+ * Studio side responds with `session-broadcast` (and also proactively
7
+ * broadcasts on every iframe `load` once `activeSessionIdRef.current`
8
+ * is non-null). Both messages converge into the resolver's D10
9
+ * replacement table : duplicates are idempotent, conflicting projectIds
10
+ * are rejected.
11
+ *
12
+ * targetOrigin policy : the SDK does NOT know the Studio origin until
13
+ * a broadcast lands. Outbound session-request uses `targetOrigin = '*'`
14
+ * (the only sensible default pre-handshake). Inbound broadcasts are
15
+ * validated by origin allowlist + envelope shape ; after the first
16
+ * confirmed broadcast the iframe locks the parent origin to
17
+ * `event.origin` and refuses subsequent envelopes from any other.
18
+ */
19
+ import { PREVIEW_NAMESPACE, PREVIEW_PROTOCOL_VERSION, validatePreviewEnvelope, } from '@storystick/transport-allowlist';
20
+ // Re-imported here so the file can stay agnostic to where the helper
21
+ // is exported from in the wider preview package.
22
+ function defaultRandomHex() {
23
+ if (typeof globalThis.crypto !== 'undefined' &&
24
+ typeof globalThis.crypto.getRandomValues === 'function') {
25
+ const buf = new Uint8Array(16);
26
+ globalThis.crypto.getRandomValues(buf);
27
+ return Array.from(buf, (b) => b.toString(16).padStart(2, '0')).join('');
28
+ }
29
+ // Test fallback — production browsers always have crypto.getRandomValues.
30
+ let s = '';
31
+ for (let i = 0; i < 32; i += 1)
32
+ s += Math.floor(Math.random() * 16).toString(16);
33
+ return s;
34
+ }
35
+ /**
36
+ * Build a fresh `session-request` envelope. Pure — no side effects.
37
+ *
38
+ * The iframe does not yet know its sessionId, so the envelope carries
39
+ * an empty string for that field (legal exception in the validator
40
+ * for this type only).
41
+ */
42
+ export function buildSessionRequest(args) {
43
+ const now = args.now ?? Date.now;
44
+ const rand = args.randomHex ?? defaultRandomHex;
45
+ return {
46
+ namespace: PREVIEW_NAMESPACE,
47
+ version: PREVIEW_PROTOCOL_VERSION,
48
+ type: 'storystick.preview.session-request',
49
+ // ADR-116 D2 — sessionId is empty pre-handshake. Validator allows
50
+ // this for session-request only.
51
+ sessionId: '',
52
+ projectId: args.projectId,
53
+ requestId: rand(),
54
+ nonce: rand(),
55
+ sentAt: now(),
56
+ payload: {
57
+ reason: args.reason,
58
+ },
59
+ };
60
+ }
61
+ /**
62
+ * Inbound broadcast validator. Wraps the shared `validatePreviewEnvelope`
63
+ * plus a projectId tie-breaker so the carrier returns the
64
+ * `ProvisionalIdentity` directly without the caller doing its own type
65
+ * narrowing.
66
+ *
67
+ * `eventOrigin` is the postMessage `MessageEvent.origin`. The resolver
68
+ * locks the iframe's parent origin to this value on the first confirmed
69
+ * broadcast (D2 targetOrigin policy).
70
+ */
71
+ export function parseInboundBroadcast(raw, eventOrigin, expectedProjectId) {
72
+ const validated = validatePreviewEnvelope(raw);
73
+ if (!validated.ok) {
74
+ return { ok: false, reason: validated.reason };
75
+ }
76
+ const env = validated.message;
77
+ if (env.type !== 'storystick.preview.session-broadcast') {
78
+ return { ok: false, reason: 'not a session-broadcast envelope' };
79
+ }
80
+ // Now that we have a typed envelope, narrow to the broadcast shape.
81
+ const broadcast = env;
82
+ if (broadcast.projectId !== expectedProjectId) {
83
+ return { ok: false, reason: 'projectId mismatch' };
84
+ }
85
+ if (typeof eventOrigin !== 'string' || eventOrigin.length === 0) {
86
+ return { ok: false, reason: 'event.origin is empty' };
87
+ }
88
+ return {
89
+ ok: true,
90
+ identity: {
91
+ sessionId: broadcast.sessionId,
92
+ projectId: broadcast.projectId,
93
+ parentOrigin: broadcast.payload.parentOrigin,
94
+ },
95
+ };
96
+ }
97
+ /**
98
+ * Bind a `message` listener that forwards every well-formed
99
+ * `session-broadcast` envelope to the resolver callback. Returns the
100
+ * disposer. The caller MUST invoke the disposer on teardown (resolver
101
+ * stop, runtime dispose, framework HMR re-mount).
102
+ *
103
+ * The listener does NOT validate origin against an allowlist : that
104
+ * lookup lives Studio-side per ADR-087 §3.7 (projects.allowedOrigins).
105
+ * Iframe-side, the resolver locks `parentOrigin` to the first
106
+ * confirmed broadcast's origin and refuses subsequent broadcasts from
107
+ * any other origin. The lock is applied AFTER `parseInboundBroadcast`
108
+ * returns ok ; this function is a pure pipe.
109
+ */
110
+ export function attachBroadcastListener(args) {
111
+ const target = args.win ?? (typeof window === 'undefined' ? undefined : window);
112
+ if (target === undefined)
113
+ return () => { };
114
+ const handler = (event) => {
115
+ const parsed = parseInboundBroadcast(event.data, event.origin, args.expectedProjectId);
116
+ if (!parsed.ok)
117
+ return;
118
+ args.onBroadcast(parsed.identity, event.origin);
119
+ // ADR-134 §9.T3 — bfcache liveness ack. Studio's bfcache handler
120
+ // posts a session-broadcast then waits 500ms for any iframe
121
+ // postMessage as a liveness proof. An authenticated + idle
122
+ // iframe doesn't naturally produce any reply postMessage when it
123
+ // receives a broadcast, so the alive path would false-negative
124
+ // (codex review R2 P2). Echo the broadcast back to Studio as a
125
+ // session-request envelope — handleSessionRequestEnvelope already
126
+ // routes session-requests for the queue-drain flow, and the
127
+ // existing 8/10s per-Window rate limiter caps the extra traffic.
128
+ // Origin policy : reply to `event.origin` (the broadcast's
129
+ // sender), not '*' — once the iframe has received a confirmed
130
+ // broadcast the parent origin is known.
131
+ try {
132
+ const reply = buildSessionRequest({
133
+ projectId: args.expectedProjectId,
134
+ reason: 'recovery',
135
+ });
136
+ // `event.source` is typed as MessageEventSource (Window | MessagePort | ServiceWorker)
137
+ // ; postMessage exists on Window. Defensive try/catch handles
138
+ // exotic source types without breaking the broadcast handler.
139
+ const source = event.source;
140
+ source?.postMessage(reply, event.origin);
141
+ }
142
+ catch {
143
+ /* defensive — telemetry path must not break broadcast handler */
144
+ }
145
+ };
146
+ target.addEventListener('message', handler);
147
+ return () => {
148
+ target.removeEventListener('message', handler);
149
+ };
150
+ }
151
+ /**
152
+ * Post a `session-request` envelope to `window.parent`. Returns true
153
+ * if a parent exists ; false in SSR / detached contexts. The envelope
154
+ * is sent with `targetOrigin = '*'` because the iframe does not yet
155
+ * know the Studio origin (chicken-and-egg with the broadcast that
156
+ * would teach it).
157
+ */
158
+ export function postSessionRequest(args) {
159
+ const target = args.parent ?? (typeof window === 'undefined' ? undefined : window.parent);
160
+ if (target === undefined)
161
+ return false;
162
+ if (typeof window !== 'undefined' && target === window)
163
+ return false;
164
+ const envelope = buildSessionRequest({
165
+ projectId: args.projectId,
166
+ reason: args.reason,
167
+ now: args.now,
168
+ randomHex: args.randomHex,
169
+ });
170
+ try {
171
+ target.postMessage(envelope, '*');
172
+ }
173
+ catch {
174
+ return false;
175
+ }
176
+ return true;
177
+ }
@@ -0,0 +1,41 @@
1
+ /** Shared storage state and validated reads. Writers are loaded by Preview activation. */
2
+ export declare const STORAGE_KEY = "__storystick_preview_identity__v1";
3
+ export declare const STANDALONE_STORAGE_KEY = "__storystick_standalone_identity__v1";
4
+ export declare const standaloneMemory: {
5
+ value: StoredIdentityV1 | null;
6
+ };
7
+ /**
8
+ * Embedded identity TTL — 5 minutes from last broadcast confirmation.
9
+ * Standalone hints survive for the tab lifetime; only the server grant
10
+ * authorizes their use, including after reload or suspension.
11
+ *
12
+ * Post-ADR-131 C7 Gap A fix (2026-05-26): reduced from 30min to 5min
13
+ * to align with the preview session row refresh rate. The longer
14
+ * window allowed sessionStorage to outlive `previewSessions` rows that
15
+ * had already been GC'd / replaced, surfacing as
16
+ * `INVALID_PREVIEW_SESSION` from preview-scoped follow-up calls when
17
+ * the iframe submitted with a stale cached sessionId.
18
+ */
19
+ export declare const STORAGE_TTL_MS: number;
20
+ /**
21
+ * Wire shape persisted in sessionStorage. Distinct from `PreviewIdentity`
22
+ * because the wire payload carries an explicit `expiresAt` for embedded
23
+ * freshness and a standalone marker for the tab-scoped carrier.
24
+ */
25
+ export interface StoredIdentityV1 {
26
+ v: 1;
27
+ /** A server-validated top-level view persists for this tab's lifetime. */
28
+ standalone?: true;
29
+ studioUrl?: string;
30
+ /** Tab-local preference, never an authorization or a momentary Alt override. */
31
+ interactionMode?: 'edit' | 'browse';
32
+ sessionId: string;
33
+ projectId: string;
34
+ parentOrigin: string | null;
35
+ expiresAt: number;
36
+ }
37
+ export declare function getStorage(): Storage | null;
38
+ /** Read raw stored entry (exposed for tests). */
39
+ export declare function readStored(): StoredIdentityV1 | null;
40
+ export declare function readStandaloneStored(): StoredIdentityV1 | null;
41
+ export declare function safeStudioUrl(value: unknown): value is string;
@@ -0,0 +1,99 @@
1
+ /** Shared storage state and validated reads. Writers are loaded by Preview activation. */
2
+ export const STORAGE_KEY = '__storystick_preview_identity__v1';
3
+ // A Studio iframe and the previous autonomous page can share origin + tab
4
+ // storage. Their view identities must never overwrite or resume one another.
5
+ export const STANDALONE_STORAGE_KEY = '__storystick_standalone_identity__v1';
6
+ export const standaloneMemory = { value: null };
7
+ /**
8
+ * Embedded identity TTL — 5 minutes from last broadcast confirmation.
9
+ * Standalone hints survive for the tab lifetime; only the server grant
10
+ * authorizes their use, including after reload or suspension.
11
+ *
12
+ * Post-ADR-131 C7 Gap A fix (2026-05-26): reduced from 30min to 5min
13
+ * to align with the preview session row refresh rate. The longer
14
+ * window allowed sessionStorage to outlive `previewSessions` rows that
15
+ * had already been GC'd / replaced, surfacing as
16
+ * `INVALID_PREVIEW_SESSION` from preview-scoped follow-up calls when
17
+ * the iframe submitted with a stale cached sessionId.
18
+ */
19
+ export const STORAGE_TTL_MS = 5 * 60 * 1000;
20
+ export function getStorage() {
21
+ if (typeof window === 'undefined')
22
+ return null;
23
+ try {
24
+ return window.sessionStorage;
25
+ }
26
+ catch {
27
+ return null;
28
+ }
29
+ }
30
+ function safeGetItem(storage, key) {
31
+ try {
32
+ return storage.getItem(key);
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ }
38
+ function parseStored(raw) {
39
+ let json;
40
+ try {
41
+ json = JSON.parse(raw);
42
+ }
43
+ catch {
44
+ return null;
45
+ }
46
+ if (typeof json !== 'object' || json === null)
47
+ return null;
48
+ const obj = json;
49
+ if (obj.v !== 1)
50
+ return null;
51
+ if (typeof obj.sessionId !== 'string' || obj.sessionId.length === 0) {
52
+ return null;
53
+ }
54
+ if (typeof obj.projectId !== 'string' || obj.projectId.length === 0) {
55
+ return null;
56
+ }
57
+ if (typeof obj.parentOrigin !== 'string' && obj.parentOrigin !== null) {
58
+ return null;
59
+ }
60
+ if (typeof obj.expiresAt !== 'number' || !Number.isFinite(obj.expiresAt)) {
61
+ return null;
62
+ }
63
+ return {
64
+ v: 1,
65
+ ...(obj.standalone === true ? { standalone: true, ...(safeStudioUrl(obj.studioUrl) ? { studioUrl: obj.studioUrl } : {}) } : {}),
66
+ ...(obj.standalone === true && (obj.interactionMode === 'edit' || obj.interactionMode === 'browse') ? { interactionMode: obj.interactionMode } : {}),
67
+ sessionId: obj.sessionId,
68
+ projectId: obj.projectId,
69
+ parentOrigin: obj.parentOrigin ?? null,
70
+ expiresAt: obj.expiresAt,
71
+ };
72
+ }
73
+ /** Read raw stored entry (exposed for tests). */
74
+ export function readStored() {
75
+ return readEntry(STORAGE_KEY);
76
+ }
77
+ export function readStandaloneStored() {
78
+ return standaloneMemory.value ?? readEntry(STANDALONE_STORAGE_KEY);
79
+ }
80
+ function readEntry(key) {
81
+ const storage = getStorage();
82
+ if (storage === null)
83
+ return null;
84
+ const raw = safeGetItem(storage, key);
85
+ if (raw === null)
86
+ return null;
87
+ return parseStored(raw);
88
+ }
89
+ export function safeStudioUrl(value) {
90
+ if (typeof value !== 'string' || value.length > 8192)
91
+ return false;
92
+ try {
93
+ const url = new URL(value);
94
+ return (url.protocol === 'https:' || url.protocol === 'http:') && !url.username && !url.password;
95
+ }
96
+ catch {
97
+ return false;
98
+ }
99
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * ADR-116 D7 — sessionStorage carrier (tier-2 persistence).
3
+ *
4
+ * Stores the resolved public identity inside the tab's sessionStorage so a
5
+ * hard navigation (raw `<a>` click, full reload, `window.location`
6
+ * assignment) re-bootstraps identity even before a live Studio
7
+ * broadcast lands.
8
+ *
9
+ * PR1 scope : full read/write/validate surface ; PR2 extends the
10
+ * tests + dogfood entries. The behaviour shipped here is sufficient
11
+ * for the resolver wiring to consume it.
12
+ *
13
+ * Key versioning : `__storystick_preview_identity__v1`. Future shape
14
+ * changes bump to `_v2` so stale-format reads are dropped instead of
15
+ * deserialised into an invalid runtime state.
16
+ */
17
+ import type { Carrier, PreviewIdentity } from '../identity-types.js';
18
+ export { STORAGE_KEY, STANDALONE_STORAGE_KEY, STORAGE_TTL_MS, readStored, readStandaloneStored } from './storage-state.js';
19
+ export type { StoredIdentityV1 } from './storage-state.js';
20
+ /**
21
+ * Write a fresh identity to sessionStorage (D7 A3 — fires on every
22
+ * confirmed `session-broadcast` envelope receipt). Wraps every storage
23
+ * call in try/catch so hardened/private modes never throw out of the
24
+ * resolver's hot path.
25
+ */
26
+ export declare function writeStored(identity: PreviewIdentity, now?: () => number): boolean;
27
+ export declare function writeStandaloneStored(identity: Pick<PreviewIdentity, 'sessionId' | 'projectId' | 'parentOrigin'>, now?: () => number): boolean;
28
+ /** Called only after a server-authorized view response, never from URL input. */
29
+ export declare function rememberStandaloneStudioUrl(identity: Pick<PreviewIdentity, 'sessionId' | 'projectId'>, studioUrl: string): void;
30
+ /** A UI preference bound to the existing tab view; a new view starts in Select. */
31
+ export declare function rememberStandaloneInteractionMode(identity: Pick<PreviewIdentity, 'sessionId' | 'projectId'>, interactionMode: 'edit' | 'browse'): void;
32
+ /** Keep an autonomous recovery surface available after a refused cold boot.
33
+ * This hint carries no authority; Exit Preview explicitly removes it.
34
+ */
35
+ export declare function clearStoredAfterAuthFailure(): void;
36
+ /** Remove the stored identity (used on expired-storage clear per D10). */
37
+ export declare function clearStored(): void;
38
+ export declare function clearStandaloneStored(): void;
39
+ export declare function createStorageCarrier(): Carrier;
@@ -0,0 +1,182 @@
1
+ /**
2
+ * ADR-116 D7 — sessionStorage carrier (tier-2 persistence).
3
+ *
4
+ * Stores the resolved public identity inside the tab's sessionStorage so a
5
+ * hard navigation (raw `<a>` click, full reload, `window.location`
6
+ * assignment) re-bootstraps identity even before a live Studio
7
+ * broadcast lands.
8
+ *
9
+ * PR1 scope : full read/write/validate surface ; PR2 extends the
10
+ * tests + dogfood entries. The behaviour shipped here is sufficient
11
+ * for the resolver wiring to consume it.
12
+ *
13
+ * Key versioning : `__storystick_preview_identity__v1`. Future shape
14
+ * changes bump to `_v2` so stale-format reads are dropped instead of
15
+ * deserialised into an invalid runtime state.
16
+ */
17
+ import { detectPreviewSessionFromWindow } from '../preview-identity.js';
18
+ import { standaloneMemory, getStorage, safeStudioUrl, STORAGE_KEY, STANDALONE_STORAGE_KEY, STORAGE_TTL_MS, readStored, readStandaloneStored, } from './storage-state.js';
19
+ export { STORAGE_KEY, STANDALONE_STORAGE_KEY, STORAGE_TTL_MS, readStored, readStandaloneStored } from './storage-state.js';
20
+ function safeSetItem(storage, key, value) {
21
+ try {
22
+ storage.setItem(key, value);
23
+ return true;
24
+ }
25
+ catch {
26
+ return false;
27
+ }
28
+ }
29
+ function safeRemoveItem(storage, key) {
30
+ try {
31
+ storage.removeItem(key);
32
+ }
33
+ catch {
34
+ /* hardened mode — silent */
35
+ }
36
+ }
37
+ /**
38
+ * Write a fresh identity to sessionStorage (D7 A3 — fires on every
39
+ * confirmed `session-broadcast` envelope receipt). Wraps every storage
40
+ * call in try/catch so hardened/private modes never throw out of the
41
+ * resolver's hot path.
42
+ */
43
+ export function writeStored(identity, now = Date.now) {
44
+ return writeEntry(identity, now, false);
45
+ }
46
+ export function writeStandaloneStored(identity, now = Date.now) {
47
+ return writeEntry(identity, now, true);
48
+ }
49
+ function writeEntry(identity, now, standalone) {
50
+ const storage = getStorage();
51
+ const previous = standalone ? readStandaloneStored() : null;
52
+ const studioUrl = previous?.standalone && previous.sessionId === identity.sessionId && previous.projectId === identity.projectId ? previous.studioUrl : undefined;
53
+ const interactionMode = previous?.standalone && previous.sessionId === identity.sessionId && previous.projectId === identity.projectId ? previous.interactionMode : undefined;
54
+ const wire = {
55
+ v: 1,
56
+ ...(standalone ? { standalone: true, ...(studioUrl ? { studioUrl } : {}) } : {}),
57
+ ...(interactionMode ? { interactionMode } : {}),
58
+ sessionId: identity.sessionId,
59
+ projectId: identity.projectId,
60
+ parentOrigin: identity.parentOrigin,
61
+ expiresAt: now() + STORAGE_TTL_MS,
62
+ };
63
+ if (standalone)
64
+ standaloneMemory.value = wire;
65
+ return storage !== null && safeSetItem(storage, standalone ? STANDALONE_STORAGE_KEY : STORAGE_KEY, JSON.stringify(wire));
66
+ }
67
+ /** Called only after a server-authorized view response, never from URL input. */
68
+ export function rememberStandaloneStudioUrl(identity, studioUrl) {
69
+ if (!safeStudioUrl(studioUrl) || typeof window === 'undefined' || window.parent !== window)
70
+ return;
71
+ const stored = readStandaloneStored();
72
+ const storage = getStorage();
73
+ if (!stored?.standalone || stored.sessionId !== identity.sessionId || stored.projectId !== identity.projectId)
74
+ return;
75
+ standaloneMemory.value = { ...stored, studioUrl };
76
+ if (storage)
77
+ safeSetItem(storage, STANDALONE_STORAGE_KEY, JSON.stringify(standaloneMemory.value));
78
+ }
79
+ /** A UI preference bound to the existing tab view; a new view starts in Select. */
80
+ export function rememberStandaloneInteractionMode(identity, interactionMode) {
81
+ if (typeof window === 'undefined' || window.parent !== window)
82
+ return;
83
+ const stored = readStandaloneStored();
84
+ if (!stored?.standalone || stored.sessionId !== identity.sessionId || stored.projectId !== identity.projectId)
85
+ return;
86
+ standaloneMemory.value = { ...stored, interactionMode };
87
+ const storage = getStorage();
88
+ if (storage)
89
+ safeSetItem(storage, STANDALONE_STORAGE_KEY, JSON.stringify(standaloneMemory.value));
90
+ }
91
+ /** Keep an autonomous recovery surface available after a refused cold boot.
92
+ * This hint carries no authority; Exit Preview explicitly removes it.
93
+ */
94
+ export function clearStoredAfterAuthFailure() {
95
+ if (typeof window !== 'undefined' && window.parent === window)
96
+ return;
97
+ clearStored();
98
+ }
99
+ /** Remove the stored identity (used on expired-storage clear per D10). */
100
+ export function clearStored() {
101
+ clearEntry(STORAGE_KEY);
102
+ }
103
+ export function clearStandaloneStored() {
104
+ standaloneMemory.value = null;
105
+ clearEntry(STANDALONE_STORAGE_KEY);
106
+ }
107
+ function clearEntry(key) {
108
+ const storage = getStorage();
109
+ if (storage === null)
110
+ return;
111
+ safeRemoveItem(storage, key);
112
+ }
113
+ /**
114
+ * Read the URL's `_ss_session` query param (the bootstrap signal
115
+ * Studio stamps when mounting the iframe at a fresh session). Returns
116
+ * `null` when no window is available, when the iframe is not nested,
117
+ * or when the param is absent / empty.
118
+ *
119
+ * Used by `createStorageCarrier` to invalidate stored identity on
120
+ * cross-session navigation (ADR-131 C7 Gap A — 2026-05-26).
121
+ */
122
+ function readUrlSessionId() {
123
+ return detectPreviewSessionFromWindow().sessionId;
124
+ }
125
+ export function createStorageCarrier() {
126
+ return {
127
+ name: 'storage',
128
+ read(ctx) {
129
+ const autonomousTab = typeof window !== 'undefined' && window.parent === window;
130
+ const stored = autonomousTab ? readStandaloneStored() : readStored();
131
+ const clear = autonomousTab ? clearStandaloneStored : clearStored;
132
+ if (stored === null)
133
+ return { kind: 'miss' };
134
+ // Expired — clear and miss.
135
+ // An autonomous view is reauthorized on every boot by /token. A short
136
+ // iframe TTL must not silently turn off Preview after tab suspension.
137
+ if ((stored.standalone === true) !== autonomousTab)
138
+ return { kind: 'miss' };
139
+ if (!autonomousTab && stored.expiresAt <= ctx.now()) {
140
+ clear();
141
+ return { kind: 'miss' };
142
+ }
143
+ // ADR-131 C7 Gap A fix (2026-05-26): URL-sessionId vs stored-
144
+ // sessionId mismatch invalidates the stored identity.
145
+ //
146
+ // Symptom (Vercel-spike debug log §"Gap A"): the iframe boot
147
+ // sequence reads sessionStorage BEFORE the live broadcast
148
+ // arrives. Pre-fix the stored sessionId from a prior session
149
+ // (TTL 30min) could be returned even when the URL carried a
150
+ // FRESH `_ss_session` — the iframe then submitted preview-scoped
151
+ // requests against a session row Convex had already GC'd /
152
+ // replaced, surfacing as `INVALID_PREVIEW_SESSION`.
153
+ //
154
+ // The fix: if the URL carries a sessionId AND it differs from
155
+ // the stored sessionId, clear storage and return miss. The
156
+ // carrier chain then falls through to the URL tier (the
157
+ // authoritative bootstrap signal). When the URL carries no
158
+ // sessionId we leave storage alone (the stored entry is the
159
+ // best available bootstrap signal until the broadcast lands).
160
+ const urlSessionId = readUrlSessionId();
161
+ if (urlSessionId !== null && urlSessionId !== stored.sessionId) {
162
+ clear();
163
+ return { kind: 'miss' };
164
+ }
165
+ // Different project — drop and treat as miss. The expectedProjectId
166
+ // (from <Storystick projectId> prop) is authoritative ; a different
167
+ // value indicates a stale read from another preview session and
168
+ // must not poison the current iframe.
169
+ if (stored.projectId !== ctx.expectedProjectId) {
170
+ return { kind: 'miss' };
171
+ }
172
+ return {
173
+ kind: 'hit',
174
+ identity: {
175
+ sessionId: stored.sessionId,
176
+ projectId: stored.projectId,
177
+ parentOrigin: stored.parentOrigin,
178
+ },
179
+ };
180
+ },
181
+ };
182
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * ADR-116 D3 — URL carrier.
3
+ *
4
+ * Reads `_ss_session` from `window.location.search` at boot. This is
5
+ * the tier-3 carrier (lowest priority) ; the URL marker is a bootstrap
6
+ * signal only, never load-bearing post-init (D5 + D10).
7
+ *
8
+ * The URL does NOT carry a projectId — only sessionId. We trust the
9
+ * iframe's `<Storystick projectId>` prop as the projectId at boot ;
10
+ * a subsequent live broadcast confirms or replaces the identity (D10
11
+ * Confirm / Replace transitions).
12
+ *
13
+ * SSR / no-window contexts return `miss`. Hardened browser modes do
14
+ * not affect URL reads (window.location is always available where a
15
+ * window exists).
16
+ */
17
+ import type { Carrier } from '../identity-types.js';
18
+ export declare const URL_SESSION_PARAM = "_ss_session";
19
+ export declare function createUrlCarrier(): Carrier;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * ADR-116 D3 — URL carrier.
3
+ *
4
+ * Reads `_ss_session` from `window.location.search` at boot. This is
5
+ * the tier-3 carrier (lowest priority) ; the URL marker is a bootstrap
6
+ * signal only, never load-bearing post-init (D5 + D10).
7
+ *
8
+ * The URL does NOT carry a projectId — only sessionId. We trust the
9
+ * iframe's `<Storystick projectId>` prop as the projectId at boot ;
10
+ * a subsequent live broadcast confirms or replaces the identity (D10
11
+ * Confirm / Replace transitions).
12
+ *
13
+ * SSR / no-window contexts return `miss`. Hardened browser modes do
14
+ * not affect URL reads (window.location is always available where a
15
+ * window exists).
16
+ */
17
+ import { PREVIEW_SESSION_QUERY_PARAM, readPreviewSessionIdFromSearch, } from '../preview-identity.js';
18
+ export const URL_SESSION_PARAM = PREVIEW_SESSION_QUERY_PARAM;
19
+ export function createUrlCarrier() {
20
+ return {
21
+ name: 'url',
22
+ read(ctx) {
23
+ if (typeof window === 'undefined')
24
+ return { kind: 'miss' };
25
+ const sessionId = readPreviewSessionIdFromSearch(window.location.search, URL_SESSION_PARAM);
26
+ if (sessionId === null)
27
+ return { kind: 'miss' };
28
+ return {
29
+ kind: 'hit',
30
+ identity: {
31
+ sessionId,
32
+ projectId: ctx.expectedProjectId,
33
+ parentOrigin: null,
34
+ },
35
+ };
36
+ },
37
+ };
38
+ }