@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,329 @@
1
+ import { transitionPreviewGrantLifecycle, } from "./grant-lifecycle.js";
2
+ import { previewPerfNow, recordPreviewPerfMeasure, recordPreviewPerfMetric } from './runtime/perf-metrics.js';
3
+ export const PREVIEW_TOKEN_ROUTE = "/api/storystick/preview/token";
4
+ export const PREVIEW_GRANT_RETRY_BACKOFF_MS = [
5
+ 1_000, 2_000, 5_000, 10_000, 30_000,
6
+ ];
7
+ const MIN_REFRESH_DELAY_MS = 1_000;
8
+ export class PreviewGrantRequestError extends Error {
9
+ code;
10
+ retryable;
11
+ retryAfterMs;
12
+ constructor(args) {
13
+ super(args.message);
14
+ this.name = "PreviewGrantRequestError";
15
+ this.code = args.code;
16
+ this.retryable = args.retryable;
17
+ this.retryAfterMs = args.retryAfterMs ?? null;
18
+ }
19
+ }
20
+ export async function acquirePreviewBrowserGrant(args) {
21
+ const fetchFn = args.fetchFn ?? fetch;
22
+ const url = new URL(args.tokenRoute ?? PREVIEW_TOKEN_ROUTE, window.location.origin);
23
+ url.searchParams.set("_ss_session", args.sessionId);
24
+ let response;
25
+ try {
26
+ response = await fetchFn(url.toString(), {
27
+ method: "POST",
28
+ credentials: "same-origin",
29
+ cache: "no-store",
30
+ headers: { Accept: "application/json" },
31
+ signal: args.signal,
32
+ });
33
+ }
34
+ catch (error) {
35
+ if (args.signal.aborted)
36
+ throw error;
37
+ throw new PreviewGrantRequestError({
38
+ code: "PREVIEW_TOKEN_UNAVAILABLE",
39
+ message: error instanceof Error
40
+ ? error.message
41
+ : "Preview token route unavailable.",
42
+ retryable: true,
43
+ });
44
+ }
45
+ let body = null;
46
+ try {
47
+ body = await response.json();
48
+ }
49
+ catch {
50
+ body = null;
51
+ }
52
+ const record = typeof body === "object" && body !== null && !Array.isArray(body)
53
+ ? body
54
+ : null;
55
+ if (!response.ok) {
56
+ const errorRecord = typeof record?.error === "object" &&
57
+ record.error !== null &&
58
+ !Array.isArray(record.error)
59
+ ? record.error
60
+ : null;
61
+ const retryAfterHeader = response.headers.get("Retry-After");
62
+ const parsedRetryAfter = retryAfterHeader === null ? NaN : Number(retryAfterHeader);
63
+ const retryable = typeof errorRecord?.retryable === "boolean"
64
+ ? errorRecord.retryable
65
+ : typeof errorRecord?.recoverable === "boolean"
66
+ ? errorRecord.recoverable
67
+ : response.status === 408 ||
68
+ response.status === 429 ||
69
+ response.status >= 500;
70
+ throw new PreviewGrantRequestError({
71
+ code: typeof errorRecord?.code === "string"
72
+ ? errorRecord.code
73
+ : retryable
74
+ ? "PREVIEW_TOKEN_UNAVAILABLE"
75
+ : "PREVIEW_TOKEN_DENIED",
76
+ message: typeof errorRecord?.message === "string"
77
+ ? errorRecord.message
78
+ : retryable
79
+ ? "Preview access is temporarily unavailable."
80
+ : "Preview access was denied.",
81
+ retryable,
82
+ retryAfterMs: Number.isFinite(parsedRetryAfter)
83
+ ? Math.max(0, parsedRetryAfter * 1_000)
84
+ : null,
85
+ });
86
+ }
87
+ const responseKeys = record === null ? [] : Object.keys(record).sort();
88
+ if (record === null ||
89
+ responseKeys.join("\0") !==
90
+ ["expiresAt", "projectId", "refreshAfter", "sessionId", "token"]
91
+ .sort()
92
+ .join("\0") ||
93
+ typeof record.token !== "string" ||
94
+ record.token.length === 0 ||
95
+ typeof record.expiresAt !== "number" ||
96
+ !Number.isFinite(record.expiresAt) ||
97
+ typeof record.refreshAfter !== "number" ||
98
+ !Number.isFinite(record.refreshAfter) ||
99
+ record.sessionId !== args.sessionId ||
100
+ record.projectId !== args.projectId) {
101
+ throw new PreviewGrantRequestError({
102
+ code: "PREVIEW_TOKEN_PROTOCOL_INVALID",
103
+ message: "Preview token route returned an invalid response.",
104
+ retryable: false,
105
+ });
106
+ }
107
+ return {
108
+ token: record.token,
109
+ expiresAt: record.expiresAt,
110
+ refreshAfter: record.refreshAfter,
111
+ sessionId: record.sessionId,
112
+ projectId: record.projectId,
113
+ };
114
+ }
115
+ export function createPreviewGrantCoordinator(args) {
116
+ const now = args.now ?? Date.now;
117
+ const random = args.random ?? Math.random;
118
+ const setT = args.setTimeoutFn ?? setTimeout;
119
+ const clearT = args.clearTimeoutFn ?? clearTimeout;
120
+ const win = args.windowRef ?? (typeof window === "undefined" ? undefined : window);
121
+ const acquire = args.acquire ?? acquirePreviewBrowserGrant;
122
+ let lifecycle = transitionPreviewGrantLifecycle(null, {
123
+ type: "start",
124
+ sessionId: args.sessionId,
125
+ at: now(),
126
+ });
127
+ let stopped = false;
128
+ let timer = null;
129
+ let controller = null;
130
+ let flight = 0;
131
+ let hasGrant = false;
132
+ let announced = false;
133
+ let admissionInvalidated = false;
134
+ let supersededAttempts = 0;
135
+ let channel = null;
136
+ const publish = () => args.onLifecycle(lifecycle);
137
+ const clearTimer = () => {
138
+ if (timer === null)
139
+ return;
140
+ clearT(timer);
141
+ timer = null;
142
+ };
143
+ const schedule = (delayMs, callback) => {
144
+ clearTimer();
145
+ timer = setT(callback, Math.max(MIN_REFRESH_DELAY_MS, delayMs));
146
+ };
147
+ const retryDelay = (attempt, override) => {
148
+ if (override !== null)
149
+ return override;
150
+ const index = Math.min(Math.max(0, attempt - 1), PREVIEW_GRANT_RETRY_BACKOFF_MS.length - 1);
151
+ const base = PREVIEW_GRANT_RETRY_BACKOFF_MS[index];
152
+ return Math.round(base * (0.8 + random() * 0.4));
153
+ };
154
+ const attemptAcquire = () => {
155
+ if (stopped || controller !== null)
156
+ return;
157
+ const currentFlight = ++flight;
158
+ controller = new AbortController();
159
+ const signal = controller.signal;
160
+ const startedAt = win ? previewPerfNow(win) : 0;
161
+ let responseObserved = false;
162
+ if (win)
163
+ recordPreviewPerfMetric(win, { name: 'preview.grant.request' });
164
+ void acquire({
165
+ sessionId: args.sessionId,
166
+ projectId: args.projectId,
167
+ ...(args.tokenRoute !== undefined ? { tokenRoute: args.tokenRoute } : {}),
168
+ signal,
169
+ })
170
+ .then((grant) => {
171
+ responseObserved = true;
172
+ if (win)
173
+ recordPreviewPerfMeasure(win, 'preview.grant.response', startedAt, { ok: true, aborted: signal.aborted });
174
+ if (stopped || signal.aborted || currentFlight !== flight)
175
+ return;
176
+ controller = null;
177
+ if (admissionInvalidated) {
178
+ admissionInvalidated = false;
179
+ attemptAcquire();
180
+ return;
181
+ }
182
+ hasGrant = true;
183
+ supersededAttempts = 0;
184
+ args.onGrant(grant);
185
+ if (!announced) {
186
+ announced = true;
187
+ channel?.postMessage('admission-changed');
188
+ }
189
+ if (lifecycle.phase === "reconnecting" && lifecycle.cause === "grant") {
190
+ lifecycle = transitionPreviewGrantLifecycle(lifecycle, {
191
+ type: "ready",
192
+ at: now(),
193
+ generation: args.generation,
194
+ });
195
+ publish();
196
+ }
197
+ schedule(grant.refreshAfter - now(), attemptAcquire);
198
+ })
199
+ .catch((error) => {
200
+ if (win && !responseObserved)
201
+ recordPreviewPerfMeasure(win, 'preview.grant.response', startedAt, { ok: false, aborted: signal.aborted });
202
+ if (stopped || signal.aborted || currentFlight !== flight)
203
+ return;
204
+ controller = null;
205
+ if (admissionInvalidated) {
206
+ admissionInvalidated = false;
207
+ attemptAcquire();
208
+ return;
209
+ }
210
+ const normalized = error instanceof PreviewGrantRequestError
211
+ ? error
212
+ : new PreviewGrantRequestError({
213
+ code: "PREVIEW_TOKEN_UNAVAILABLE",
214
+ message: error instanceof Error
215
+ ? error.message
216
+ : "Preview token unavailable.",
217
+ retryable: true,
218
+ });
219
+ if (normalized.code === 'PREVIEW_ADMISSION_SUPERSEDED')
220
+ supersededAttempts++;
221
+ if (!normalized.retryable || supersededAttempts >= 3) {
222
+ lifecycle = transitionPreviewGrantLifecycle(lifecycle, {
223
+ type: "blocked",
224
+ at: now(),
225
+ code: normalized.code,
226
+ message: normalized.message,
227
+ });
228
+ publish();
229
+ return;
230
+ }
231
+ lifecycle = transitionPreviewGrantLifecycle(lifecycle, {
232
+ type: "retry",
233
+ at: now(),
234
+ cause: "grant",
235
+ });
236
+ publish();
237
+ schedule(retryDelay(lifecycle.attempt, normalized.retryAfterMs), attemptAcquire);
238
+ });
239
+ };
240
+ const retryNow = () => {
241
+ if (stopped || lifecycle.phase === "blocked")
242
+ return;
243
+ clearTimer();
244
+ if (controller === null)
245
+ attemptAcquire();
246
+ };
247
+ const onWake = () => retryNow();
248
+ return {
249
+ start() {
250
+ if (stopped)
251
+ return;
252
+ publish();
253
+ const Channel = win?.BroadcastChannel;
254
+ if (Channel) {
255
+ try {
256
+ channel = new Channel(`storystick:preview:${args.projectId}`);
257
+ channel.onmessage = () => {
258
+ // Hints never grant access. Do not lose one while an older request is in flight.
259
+ if (controller) {
260
+ admissionInvalidated = true;
261
+ return;
262
+ }
263
+ if (lifecycle.phase === 'blocked') {
264
+ lifecycle = transitionPreviewGrantLifecycle(null, { type: 'start', sessionId: args.sessionId, at: now() });
265
+ supersededAttempts = 0;
266
+ publish();
267
+ }
268
+ retryNow();
269
+ };
270
+ }
271
+ catch { /* Optional invalidation hint; waking/renewal still reacquires authority. */ }
272
+ }
273
+ win?.addEventListener("online", onWake);
274
+ win?.addEventListener("focus", onWake);
275
+ win?.addEventListener("pageshow", onWake);
276
+ attemptAcquire();
277
+ },
278
+ stop() {
279
+ if (stopped)
280
+ return;
281
+ stopped = true;
282
+ clearTimer();
283
+ controller?.abort();
284
+ controller = null;
285
+ channel?.close();
286
+ channel = null;
287
+ win?.removeEventListener("online", onWake);
288
+ win?.removeEventListener("focus", onWake);
289
+ win?.removeEventListener("pageshow", onWake);
290
+ },
291
+ markReady() {
292
+ if (stopped || !hasGrant || lifecycle.phase === "ready")
293
+ return;
294
+ lifecycle = transitionPreviewGrantLifecycle(lifecycle, {
295
+ type: "ready",
296
+ at: now(),
297
+ generation: args.generation,
298
+ });
299
+ publish();
300
+ },
301
+ reportTransientFailure(cause) {
302
+ if (stopped || lifecycle.phase === "blocked")
303
+ return;
304
+ lifecycle = transitionPreviewGrantLifecycle(lifecycle, {
305
+ type: "retry",
306
+ at: now(),
307
+ cause,
308
+ });
309
+ publish();
310
+ retryNow();
311
+ },
312
+ reportBlocked(code, message) {
313
+ if (stopped || lifecycle.phase === "blocked")
314
+ return;
315
+ lifecycle = transitionPreviewGrantLifecycle(lifecycle, {
316
+ type: "blocked",
317
+ at: now(),
318
+ code,
319
+ message,
320
+ });
321
+ publish();
322
+ clearTimer();
323
+ controller?.abort();
324
+ controller = null;
325
+ },
326
+ retryNow,
327
+ getLifecycle: () => lifecycle,
328
+ };
329
+ }
@@ -0,0 +1,52 @@
1
+ import type { PreviewLifecyclePhase } from '@storystick/transport-allowlist';
2
+ export type PreviewGrantFailureCause = 'grant' | 'transport' | 'subscription';
3
+ /** Compatibility projection consumed by existing framework adapters. */
4
+ export type LegacyPreviewAuthState = 'idle' | 'authenticating' | 'authenticated' | 'refreshing' | 'error';
5
+ export type PreviewGrantLifecycle = {
6
+ phase: 'connecting';
7
+ sessionId: string;
8
+ attempt: number;
9
+ since: number;
10
+ } | {
11
+ phase: 'ready';
12
+ sessionId: string;
13
+ attempt: 0;
14
+ since: number;
15
+ readyAt: number;
16
+ generation: number;
17
+ } | {
18
+ phase: 'reconnecting';
19
+ sessionId: string;
20
+ attempt: number;
21
+ since: number;
22
+ lastReadyAt: number;
23
+ cause: PreviewGrantFailureCause;
24
+ } | {
25
+ phase: 'blocked';
26
+ sessionId: string;
27
+ attempt: number;
28
+ since: number;
29
+ code: string;
30
+ message: string;
31
+ };
32
+ export type PreviewGrantLifecycleEvent = {
33
+ type: 'start';
34
+ sessionId: string;
35
+ at: number;
36
+ } | {
37
+ type: 'retry';
38
+ at: number;
39
+ cause: PreviewGrantFailureCause;
40
+ } | {
41
+ type: 'ready';
42
+ at: number;
43
+ generation: number;
44
+ } | {
45
+ type: 'blocked';
46
+ at: number;
47
+ code: string;
48
+ message: string;
49
+ };
50
+ export declare function transitionPreviewGrantLifecycle(current: PreviewGrantLifecycle | null, event: PreviewGrantLifecycleEvent): PreviewGrantLifecycle;
51
+ export declare function legacyPreviewAuthState(lifecycle: PreviewGrantLifecycle | null): LegacyPreviewAuthState;
52
+ export declare function isPreviewLifecyclePhase(value: unknown): value is PreviewLifecyclePhase;
@@ -0,0 +1,74 @@
1
+ export function transitionPreviewGrantLifecycle(current, event) {
2
+ if (event.type === 'start') {
3
+ return {
4
+ phase: 'connecting',
5
+ sessionId: event.sessionId,
6
+ attempt: 0,
7
+ since: event.at,
8
+ };
9
+ }
10
+ if (current === null) {
11
+ throw new Error(`Preview grant lifecycle received ${event.type} before start`);
12
+ }
13
+ if (event.type === 'blocked') {
14
+ return {
15
+ phase: 'blocked',
16
+ sessionId: current.sessionId,
17
+ attempt: current.attempt,
18
+ since: event.at,
19
+ code: event.code,
20
+ message: event.message,
21
+ };
22
+ }
23
+ if (event.type === 'ready') {
24
+ return {
25
+ phase: 'ready',
26
+ sessionId: current.sessionId,
27
+ attempt: 0,
28
+ since: event.at,
29
+ readyAt: event.at,
30
+ generation: event.generation,
31
+ };
32
+ }
33
+ const lastReadyAt = current.phase === 'ready'
34
+ ? current.readyAt
35
+ : current.phase === 'reconnecting'
36
+ ? current.lastReadyAt
37
+ : null;
38
+ if (lastReadyAt === null) {
39
+ return {
40
+ phase: 'connecting',
41
+ sessionId: current.sessionId,
42
+ attempt: current.attempt + 1,
43
+ since: current.since,
44
+ };
45
+ }
46
+ return {
47
+ phase: 'reconnecting',
48
+ sessionId: current.sessionId,
49
+ attempt: current.attempt + 1,
50
+ since: current.phase === 'reconnecting' ? current.since : event.at,
51
+ lastReadyAt,
52
+ cause: event.cause,
53
+ };
54
+ }
55
+ export function legacyPreviewAuthState(lifecycle) {
56
+ if (lifecycle === null)
57
+ return 'idle';
58
+ switch (lifecycle.phase) {
59
+ case 'connecting':
60
+ return 'authenticating';
61
+ case 'ready':
62
+ return 'authenticated';
63
+ case 'reconnecting':
64
+ return 'refreshing';
65
+ case 'blocked':
66
+ return 'error';
67
+ }
68
+ }
69
+ export function isPreviewLifecyclePhase(value) {
70
+ return (value === 'connecting' ||
71
+ value === 'ready' ||
72
+ value === 'reconnecting' ||
73
+ value === 'blocked');
74
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * ADR-116 D6 — fake framework adapter for core unit tests.
3
+ *
4
+ * Validates the contract (`navigateTo`, `subscribeToLocation`,
5
+ * `getCurrentLocation`) without pulling React / Next / TanStack. Tests
6
+ * use it to drive identity-resolver scenarios end-to-end.
7
+ *
8
+ * Public surface :
9
+ * - `createTestAdapter()` returns a `{ adapter, harness }` pair.
10
+ * - `harness.setLocation({pathname, search, hash})` rewrites the
11
+ * current location and notifies subscribers (the framework adapter
12
+ * contract's `subscribeToLocation` callback).
13
+ * - `harness.replaceCalls` is the log of `navigateTo` invocations.
14
+ */
15
+ import type { FrameworkAdapter, FrameworkLocation } from './identity-types.js';
16
+ export interface TestAdapterHarness {
17
+ setLocation(loc: Partial<FrameworkLocation>): void;
18
+ replaceCalls: ReadonlyArray<{
19
+ path: string;
20
+ opts?: {
21
+ refresh?: boolean;
22
+ };
23
+ }>;
24
+ /**
25
+ * Number of currently-attached subscribers. Tests assert this returns
26
+ * to 0 after the resolver unsubscribes on teardown.
27
+ */
28
+ subscriberCount(): number;
29
+ }
30
+ export interface TestAdapterPair {
31
+ adapter: FrameworkAdapter;
32
+ harness: TestAdapterHarness;
33
+ }
34
+ export declare function createTestAdapter(initial?: Partial<FrameworkLocation>): TestAdapterPair;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * ADR-116 D6 — fake framework adapter for core unit tests.
3
+ *
4
+ * Validates the contract (`navigateTo`, `subscribeToLocation`,
5
+ * `getCurrentLocation`) without pulling React / Next / TanStack. Tests
6
+ * use it to drive identity-resolver scenarios end-to-end.
7
+ *
8
+ * Public surface :
9
+ * - `createTestAdapter()` returns a `{ adapter, harness }` pair.
10
+ * - `harness.setLocation({pathname, search, hash})` rewrites the
11
+ * current location and notifies subscribers (the framework adapter
12
+ * contract's `subscribeToLocation` callback).
13
+ * - `harness.replaceCalls` is the log of `navigateTo` invocations.
14
+ */
15
+ export function createTestAdapter(initial = {}) {
16
+ let location = {
17
+ pathname: initial.pathname ?? '/',
18
+ search: initial.search ?? '',
19
+ hash: initial.hash ?? '',
20
+ };
21
+ const subscribers = new Set();
22
+ const replaceCalls = [];
23
+ const adapter = {
24
+ navigateTo(path, opts) {
25
+ replaceCalls.push({ path, opts });
26
+ },
27
+ subscribeToLocation(cb) {
28
+ subscribers.add(cb);
29
+ return () => {
30
+ subscribers.delete(cb);
31
+ };
32
+ },
33
+ getCurrentLocation() {
34
+ return location;
35
+ },
36
+ };
37
+ const harness = {
38
+ setLocation(loc) {
39
+ location = {
40
+ pathname: loc.pathname ?? location.pathname,
41
+ search: loc.search ?? location.search,
42
+ hash: loc.hash ?? location.hash,
43
+ };
44
+ for (const cb of subscribers) {
45
+ try {
46
+ cb(location);
47
+ }
48
+ catch {
49
+ /* defensive */
50
+ }
51
+ }
52
+ },
53
+ replaceCalls,
54
+ subscriberCount() {
55
+ return subscribers.size;
56
+ },
57
+ };
58
+ return { adapter, harness };
59
+ }
@@ -0,0 +1,4 @@
1
+ /** Public bootstrap hint only. The same-origin token route still authorizes it. */
2
+ export declare function readStandalonePreviewSessionId(expectedProjectId?: string): string | null;
3
+ /** Small, framework-independent gate; never loads the authenticated runtime. */
4
+ export declare function isPreviewBrowserContext(expectedProjectId?: string): boolean;
@@ -0,0 +1,22 @@
1
+ import { readStandaloneStored } from './carriers/storage-state.js';
2
+ import { PREVIEW_SESSION_QUERY_PARAM, readPreviewSessionIdFromSearch } from './session-query.js';
3
+ /** Public bootstrap hint only. The same-origin token route still authorizes it. */
4
+ export function readStandalonePreviewSessionId(expectedProjectId) {
5
+ if (typeof window === 'undefined' || window.parent !== window)
6
+ return null;
7
+ const search = window.location.search;
8
+ if (new URLSearchParams(search).has(PREVIEW_SESSION_QUERY_PARAM)) {
9
+ const sessionId = readPreviewSessionIdFromSearch(search);
10
+ return sessionId && /^[A-Za-z0-9_-]{1,180}$/.test(sessionId) ? sessionId : null;
11
+ }
12
+ const stored = readStandaloneStored();
13
+ if (!stored?.standalone || (expectedProjectId && stored.projectId !== expectedProjectId))
14
+ return null;
15
+ return /^[A-Za-z0-9_-]{1,180}$/.test(stored.sessionId) ? stored.sessionId : null;
16
+ }
17
+ /** Small, framework-independent gate; never loads the authenticated runtime. */
18
+ export function isPreviewBrowserContext(expectedProjectId) {
19
+ if (typeof window === 'undefined')
20
+ return false;
21
+ return window.parent !== window || readStandalonePreviewSessionId(expectedProjectId) !== null;
22
+ }
@@ -0,0 +1,13 @@
1
+ /** Public return target only, retained for a refused cold boot of this view. */
2
+ export declare function readStandaloneStudioUrl(projectId: string | null, sessionId: string | null): string | null;
3
+ /** Called after an authenticated bootstrap response, never for a URL payload. */
4
+ export declare function rememberStandaloneView(view: {
5
+ projectId: string;
6
+ sessionId: string;
7
+ parentOrigin: string;
8
+ studioUrl: string;
9
+ }): void;
10
+ /** Persistent interaction choice for this view only; access is authorized separately. */
11
+ export declare function readStandaloneInteractionMode(projectId: string | null, sessionId: string | null): 'edit' | 'browse';
12
+ export { rememberStandaloneInteractionMode } from './carriers/storage.js';
13
+ export { readStandalonePreviewSessionId, isPreviewBrowserContext } from './browser-context-gate.js';
@@ -0,0 +1,22 @@
1
+ import { writeStandaloneStored, rememberStandaloneStudioUrl, readStandaloneStored, } from './carriers/storage.js';
2
+ /** Public return target only, retained for a refused cold boot of this view. */
3
+ export function readStandaloneStudioUrl(projectId, sessionId) {
4
+ if (!projectId || !sessionId || typeof window === 'undefined' || window.parent !== window)
5
+ return null;
6
+ const stored = readStandaloneStored();
7
+ return stored?.standalone && stored.projectId === projectId && stored.sessionId === sessionId ? stored.studioUrl ?? null : null;
8
+ }
9
+ /** Called after an authenticated bootstrap response, never for a URL payload. */
10
+ export function rememberStandaloneView(view) {
11
+ writeStandaloneStored(view);
12
+ rememberStandaloneStudioUrl(view, view.studioUrl);
13
+ }
14
+ /** Persistent interaction choice for this view only; access is authorized separately. */
15
+ export function readStandaloneInteractionMode(projectId, sessionId) {
16
+ const stored = readStandaloneStored();
17
+ return projectId && sessionId && stored?.standalone && stored.projectId === projectId && stored.sessionId === sessionId
18
+ ? stored.interactionMode ?? 'edit'
19
+ : 'edit';
20
+ }
21
+ export { rememberStandaloneInteractionMode } from './carriers/storage.js';
22
+ export { readStandalonePreviewSessionId, isPreviewBrowserContext } from './browser-context-gate.js';