@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.
- package/LICENSE +112 -0
- package/PUBLIC-API.md +56 -0
- package/README.md +42 -2
- package/dist/.source-hash +1 -0
- package/dist/authoring/runtime-contract.d.ts +92 -0
- package/dist/authoring/runtime-contract.js +28 -0
- package/dist/channel-taxonomy.d.ts +17 -0
- package/dist/channel-taxonomy.js +46 -0
- package/dist/context-command.d.ts +18 -0
- package/dist/context-command.js +38 -0
- package/dist/dev/suspicious-codeword-detector.d.ts +15 -0
- package/dist/dev/suspicious-codeword-detector.js +220 -0
- package/dist/dev/suspicious-codeword-detector.noop.d.ts +3 -0
- package/dist/dev/suspicious-codeword-detector.noop.js +4 -0
- package/dist/framework/browser-context.d.ts +2 -0
- package/dist/framework/browser-context.js +2 -0
- package/dist/framework/generation-refresh.d.ts +23 -0
- package/dist/framework/generation-refresh.js +82 -0
- package/dist/framework/generation-signal.d.ts +8 -0
- package/dist/framework/generation-signal.js +17 -0
- package/dist/framework/projection-generation-watch.d.ts +5 -0
- package/dist/framework/projection-generation-watch.js +53 -0
- package/dist/framework/render-receipt.d.ts +8 -0
- package/dist/framework/render-receipt.js +10 -0
- package/dist/framework/runtime-adapter-store.d.ts +30 -0
- package/dist/framework/runtime-adapter-store.js +81 -0
- package/dist/grant-coordinator.d.ts +53 -0
- package/dist/grant-coordinator.js +329 -0
- package/dist/grant-lifecycle.d.ts +52 -0
- package/dist/grant-lifecycle.js +74 -0
- package/dist/identity/__test-adapter.d.ts +34 -0
- package/dist/identity/__test-adapter.js +59 -0
- package/dist/identity/browser-context-gate.d.ts +4 -0
- package/dist/identity/browser-context-gate.js +22 -0
- package/dist/identity/browser-context.d.ts +13 -0
- package/dist/identity/browser-context.js +22 -0
- package/dist/identity/carriers/postmessage.d.ts +84 -0
- package/dist/identity/carriers/postmessage.js +177 -0
- package/dist/identity/carriers/storage-state.d.ts +41 -0
- package/dist/identity/carriers/storage-state.js +99 -0
- package/dist/identity/carriers/storage.d.ts +39 -0
- package/dist/identity/carriers/storage.js +182 -0
- package/dist/identity/carriers/url.d.ts +19 -0
- package/dist/identity/carriers/url.js +38 -0
- package/dist/identity/identity-state-machine.d.ts +70 -0
- package/dist/identity/identity-state-machine.js +244 -0
- package/dist/identity/identity-types.d.ts +192 -0
- package/dist/identity/identity-types.js +22 -0
- package/dist/identity/index.d.ts +18 -0
- package/dist/identity/index.js +16 -0
- package/dist/identity/preview-cookie.d.ts +9 -0
- package/dist/identity/preview-cookie.js +12 -0
- package/dist/identity/preview-draft.d.ts +6 -0
- package/dist/identity/preview-draft.js +25 -0
- package/dist/identity/preview-identity.d.ts +28 -0
- package/dist/identity/preview-identity.js +84 -0
- package/dist/identity/preview-scope.d.ts +28 -0
- package/dist/identity/preview-scope.js +47 -0
- package/dist/identity/resolver.d.ts +110 -0
- package/dist/identity/resolver.js +334 -0
- package/dist/identity/session-query.d.ts +4 -0
- package/dist/identity/session-query.js +19 -0
- package/dist/index.d.ts +90 -0
- package/dist/index.js +788 -0
- package/dist/interaction-mode.d.ts +58 -0
- package/dist/interaction-mode.js +93 -0
- package/dist/nav-handler.d.ts +95 -0
- package/dist/nav-handler.js +84 -0
- package/dist/overlay-label.d.ts +79 -0
- package/dist/overlay-label.js +73 -0
- package/dist/page-presentation.d.ts +24 -0
- package/dist/page-presentation.js +32 -0
- package/dist/provenance/anchor-map.d.ts +95 -0
- package/dist/provenance/anchor-map.js +177 -0
- package/dist/provenance/carrier-plan.d.ts +51 -0
- package/dist/provenance/carrier-plan.js +90 -0
- package/dist/provenance/coded-text.d.ts +20 -0
- package/dist/provenance/coded-text.js +302 -0
- package/dist/provenance/direct-edit-locator.d.ts +29 -0
- package/dist/provenance/direct-edit-locator.js +218 -0
- package/dist/provenance/editable-leaf.d.ts +134 -0
- package/dist/provenance/editable-leaf.js +483 -0
- package/dist/provenance/editor-target.d.ts +29 -0
- package/dist/provenance/editor-target.js +46 -0
- package/dist/provenance/field-leaf-walker.d.ts +21 -0
- package/dist/provenance/field-leaf-walker.js +98 -0
- package/dist/provenance/frame-dependencies.d.ts +212 -0
- package/dist/provenance/frame-dependencies.js +550 -0
- package/dist/provenance/index.d.ts +26 -0
- package/dist/provenance/index.js +26 -0
- package/dist/provenance/local-events.d.ts +1 -0
- package/dist/provenance/local-events.js +1 -0
- package/dist/provenance/markers.d.ts +21 -0
- package/dist/provenance/markers.js +70 -0
- package/dist/provenance/multi-frame-anchor-map.d.ts +54 -0
- package/dist/provenance/multi-frame-anchor-map.js +109 -0
- package/dist/provenance/normalize.d.ts +2 -0
- package/dist/provenance/normalize.js +19 -0
- package/dist/provenance/overlay-focus-echo.d.ts +17 -0
- package/dist/provenance/overlay-focus-echo.js +16 -0
- package/dist/provenance/overlay-projection.d.ts +65 -0
- package/dist/provenance/overlay-projection.js +201 -0
- package/dist/provenance/owner-map.d.ts +55 -0
- package/dist/provenance/owner-map.js +239 -0
- package/dist/provenance/read-carrier.d.ts +22 -0
- package/dist/provenance/read-carrier.js +92 -0
- package/dist/provenance/recorder.d.ts +38 -0
- package/dist/provenance/recorder.js +82 -0
- package/dist/provenance/render-context.d.ts +106 -0
- package/dist/provenance/render-context.js +338 -0
- package/dist/provenance/reveal-contract.d.ts +44 -0
- package/dist/provenance/reveal-contract.js +1 -0
- package/dist/provenance/richtext-slice-embed-address.d.ts +20 -0
- package/dist/provenance/richtext-slice-embed-address.js +35 -0
- package/dist/provenance/runtime-script.d.ts +1 -0
- package/dist/provenance/runtime-script.js +2217 -0
- package/dist/provenance/sidecar-store.d.ts +63 -0
- package/dist/provenance/sidecar-store.js +151 -0
- package/dist/provenance/sidecar.d.ts +34 -0
- package/dist/provenance/sidecar.js +227 -0
- package/dist/provenance/slice-model.d.ts +28 -0
- package/dist/provenance/slice-model.js +86 -0
- package/dist/provenance/source-identity.d.ts +3 -0
- package/dist/provenance/source-identity.js +7 -0
- package/dist/provenance/source-map-materializer.d.ts +76 -0
- package/dist/provenance/source-map-materializer.js +286 -0
- package/dist/provenance/source-registry.d.ts +103 -0
- package/dist/provenance/source-registry.js +562 -0
- package/dist/provenance/transformed-output-gate.d.ts +161 -0
- package/dist/provenance/transformed-output-gate.js +171 -0
- package/dist/provenance/types.d.ts +102 -0
- package/dist/provenance/types.js +1 -0
- package/dist/refresh-policy.d.ts +11 -0
- package/dist/refresh-policy.js +14 -0
- package/dist/reveal-target.d.ts +8 -0
- package/dist/reveal-target.js +47 -0
- package/dist/route-paths.d.ts +11 -0
- package/dist/route-paths.js +14 -0
- package/dist/router-subscriber.d.ts +92 -0
- package/dist/router-subscriber.js +44 -0
- package/dist/runtime/anchor-types.d.ts +5 -0
- package/dist/runtime/anchor-types.js +1 -0
- package/dist/runtime/authority-store.d.ts +31 -0
- package/dist/runtime/authority-store.js +56 -0
- package/dist/runtime/boot.d.ts +52 -0
- package/dist/runtime/boot.js +379 -0
- package/dist/runtime/content-refresh.d.ts +30 -0
- package/dist/runtime/content-refresh.js +68 -0
- package/dist/runtime/emit-runtime-error.d.ts +51 -0
- package/dist/runtime/emit-runtime-error.js +64 -0
- package/dist/runtime/field-proxy.d.ts +112 -0
- package/dist/runtime/field-proxy.js +471 -0
- package/dist/runtime/form-contract-reporter.d.ts +99 -0
- package/dist/runtime/form-contract-reporter.js +391 -0
- package/dist/runtime/invalidation-adapter.d.ts +17 -0
- package/dist/runtime/invalidation-adapter.js +42 -0
- package/dist/runtime/live-query-overlay.d.ts +39 -0
- package/dist/runtime/live-query-overlay.js +345 -0
- package/dist/runtime/momentary-navigate.d.ts +16 -0
- package/dist/runtime/momentary-navigate.js +106 -0
- package/dist/runtime/observation-scheduler.d.ts +15 -0
- package/dist/runtime/observation-scheduler.js +42 -0
- package/dist/runtime/perf-metrics.d.ts +28 -0
- package/dist/runtime/perf-metrics.js +90 -0
- package/dist/runtime/preview-interaction-policy.d.ts +1 -0
- package/dist/runtime/preview-interaction-policy.js +103 -0
- package/dist/runtime/preview-overlay-painter.d.ts +27 -0
- package/dist/runtime/preview-overlay-painter.js +1732 -0
- package/dist/runtime/preview-overlay-sidecar.d.ts +26 -0
- package/dist/runtime/preview-overlay-sidecar.js +45 -0
- package/dist/runtime/preview-perspective-alignment.d.ts +15 -0
- package/dist/runtime/preview-perspective-alignment.js +40 -0
- package/dist/runtime/preview-perspective-intent-state.d.ts +23 -0
- package/dist/runtime/preview-perspective-intent-state.js +5 -0
- package/dist/runtime/preview-transition-state.d.ts +142 -0
- package/dist/runtime/preview-transition-state.js +257 -0
- package/dist/runtime/render-mode-state.d.ts +51 -0
- package/dist/runtime/render-mode-state.js +76 -0
- package/dist/runtime/request-id.d.ts +4 -0
- package/dist/runtime/request-id.js +8 -0
- package/dist/runtime/resolve-schema-field.d.ts +99 -0
- package/dist/runtime/resolve-schema-field.js +356 -0
- package/dist/runtime/runtime-error-dispatch.d.ts +23 -0
- package/dist/runtime/runtime-error-dispatch.js +75 -0
- package/dist/runtime/schema-store.d.ts +121 -0
- package/dist/runtime/schema-store.js +667 -0
- package/dist/runtime/snapshot-assembly.d.ts +104 -0
- package/dist/runtime/snapshot-assembly.js +553 -0
- package/dist/runtime/snapshot-reporter.d.ts +55 -0
- package/dist/runtime/snapshot-reporter.js +375 -0
- package/dist/runtime/standalone-overlay-controller.d.ts +33 -0
- package/dist/runtime/standalone-overlay-controller.js +281 -0
- package/dist/runtime/store.d.ts +307 -0
- package/dist/runtime/store.js +1026 -0
- package/dist/runtime/windowed-minimap-projection.d.ts +28 -0
- package/dist/runtime/windowed-minimap-projection.js +189 -0
- package/dist/runtime/workspace-shortcuts.d.ts +30 -0
- package/dist/runtime/workspace-shortcuts.js +188 -0
- package/dist/sdk-error-copy.d.ts +48 -0
- package/dist/sdk-error-copy.js +182 -0
- package/dist/select-cursor.d.ts +2 -0
- package/dist/select-cursor.js +23 -0
- package/dist/server/admission-cookie.d.ts +38 -0
- package/dist/server/admission-cookie.js +122 -0
- package/dist/server/control-client.d.ts +29 -0
- package/dist/server/control-client.js +22 -0
- package/dist/server/control-errors.d.ts +22 -0
- package/dist/server/control-errors.js +182 -0
- package/dist/server/draft-route.d.ts +12 -0
- package/dist/server/draft-route.js +45 -0
- package/dist/server/extract-token.d.ts +30 -0
- package/dist/server/extract-token.js +98 -0
- package/dist/server/grant.d.ts +128 -0
- package/dist/server/grant.js +395 -0
- package/dist/server/ordinary-read-provenance.d.ts +11 -0
- package/dist/server/ordinary-read-provenance.js +124 -0
- package/dist/server/preview-read-metadata.d.ts +10 -0
- package/dist/server/preview-read-metadata.js +27 -0
- package/dist/server/provenance-frame-builder.d.ts +53 -0
- package/dist/server/provenance-frame-builder.js +102 -0
- package/dist/server/provenance-runtime.d.ts +70 -0
- package/dist/server/provenance-runtime.js +46 -0
- package/dist/server/read-context-command.d.ts +11 -0
- package/dist/server/read-context-command.js +53 -0
- package/dist/server/read-identity.d.ts +3 -0
- package/dist/server/read-identity.js +63 -0
- package/dist/server/read-lifecycle.d.ts +17 -0
- package/dist/server/read-lifecycle.js +52 -0
- package/dist/server/render-frame-provenance.d.ts +133 -0
- package/dist/server/render-frame-provenance.js +676 -0
- package/dist/server/render-id.d.ts +15 -0
- package/dist/server/render-id.js +22 -0
- package/dist/server/route-document-provenance.d.ts +77 -0
- package/dist/server/route-document-provenance.js +1327 -0
- package/dist/server/route-session-cookie.d.ts +33 -0
- package/dist/server/route-session-cookie.js +54 -0
- package/dist/session-projections.d.ts +33 -0
- package/dist/session-projections.js +43 -0
- package/dist/session-subscriber.d.ts +124 -0
- package/dist/session-subscriber.js +79 -0
- package/dist/sha256-fallback.d.ts +30 -0
- package/dist/sha256-fallback.js +126 -0
- package/dist/types.d.ts +446 -0
- package/dist/types.js +85 -0
- package/dist/validators.d.ts +61 -0
- package/dist/validators.js +110 -0
- 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
|
+
}
|