@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
|
+
import { PREVIEW_SESSION_QUERY_PARAM, readPreviewSessionIdFromSearch, hasPreviewSessionParam } from './session-query.js';
|
|
2
|
+
export { PREVIEW_SESSION_QUERY_PARAM, readPreviewSessionIdFromSearch, hasPreviewSessionParam } from './session-query.js';
|
|
3
|
+
export const PREVIEW_VARIANT_QUERY_PARAM = '_ss_variant';
|
|
4
|
+
export const PREVIEW_RELEASE_QUERY_PARAM = '_ss_release';
|
|
5
|
+
export function isPreviewIdentity(value) {
|
|
6
|
+
if (value === null || typeof value !== 'object')
|
|
7
|
+
return false;
|
|
8
|
+
const identity = value;
|
|
9
|
+
return (typeof identity.sessionId === 'string' &&
|
|
10
|
+
identity.sessionId.length > 0 &&
|
|
11
|
+
typeof identity.projectId === 'string' &&
|
|
12
|
+
identity.projectId.length > 0 &&
|
|
13
|
+
(identity.parentOrigin === null ||
|
|
14
|
+
typeof identity.parentOrigin === 'string') &&
|
|
15
|
+
(identity.source === 'broadcast' ||
|
|
16
|
+
identity.source === 'request' ||
|
|
17
|
+
identity.source === 'storage' ||
|
|
18
|
+
identity.source === 'url') &&
|
|
19
|
+
typeof identity.epoch === 'number' &&
|
|
20
|
+
Number.isFinite(identity.epoch) &&
|
|
21
|
+
typeof identity.resolvedAt === 'number' &&
|
|
22
|
+
Number.isFinite(identity.resolvedAt));
|
|
23
|
+
}
|
|
24
|
+
export function readPreviewIdentitySessionId(identity) {
|
|
25
|
+
return isPreviewIdentity(identity) ? identity.sessionId : null;
|
|
26
|
+
}
|
|
27
|
+
export function readPreviewIdentity(source) {
|
|
28
|
+
if (source === null || source === undefined)
|
|
29
|
+
return null;
|
|
30
|
+
if (typeof source.getIdentity !== 'function')
|
|
31
|
+
return null;
|
|
32
|
+
try {
|
|
33
|
+
const identity = source.getIdentity();
|
|
34
|
+
return isPreviewIdentity(identity) ? identity : null;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function readPreviewIdentitySessionIdFromSource(source) {
|
|
41
|
+
return readPreviewIdentitySessionId(readPreviewIdentity(source));
|
|
42
|
+
}
|
|
43
|
+
export function readPreviewVariantIdFromSearch(search, paramName = PREVIEW_VARIANT_QUERY_PARAM) {
|
|
44
|
+
try {
|
|
45
|
+
const variantId = new URLSearchParams(search).get(paramName);
|
|
46
|
+
return normalizePreviewVariantId(variantId);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function normalizePreviewVariantId(value) {
|
|
53
|
+
if (typeof value !== 'string')
|
|
54
|
+
return null;
|
|
55
|
+
const trimmed = value.trim();
|
|
56
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
57
|
+
}
|
|
58
|
+
export function readPreviewReleaseIdFromSearch(search, paramName = PREVIEW_RELEASE_QUERY_PARAM) {
|
|
59
|
+
try {
|
|
60
|
+
const releaseId = new URLSearchParams(search).get(paramName);
|
|
61
|
+
return normalizePreviewReleaseId(releaseId);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export function normalizePreviewReleaseId(value) {
|
|
68
|
+
if (typeof value !== 'string')
|
|
69
|
+
return null;
|
|
70
|
+
const trimmed = value.trim();
|
|
71
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
72
|
+
}
|
|
73
|
+
export function detectPreviewSessionFromWindow(win = typeof window === 'undefined' ? null : window, paramName = PREVIEW_SESSION_QUERY_PARAM) {
|
|
74
|
+
if (win === null || win === undefined) {
|
|
75
|
+
return { active: false, sessionId: null, variantId: null, releaseId: null };
|
|
76
|
+
}
|
|
77
|
+
const search = typeof win.location?.search === 'string' ? win.location.search : '';
|
|
78
|
+
return {
|
|
79
|
+
active: hasPreviewSessionParam(search, paramName),
|
|
80
|
+
sessionId: readPreviewSessionIdFromSearch(search, paramName),
|
|
81
|
+
variantId: readPreviewVariantIdFromSearch(search),
|
|
82
|
+
releaseId: readPreviewReleaseIdFromSearch(search),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The content perspective selected by one preview view.
|
|
3
|
+
*
|
|
4
|
+
* Authorization deliberately does not live here: a durable preview
|
|
5
|
+
* capability answers "may this browser read preview content?", while this
|
|
6
|
+
* value answers "which preview content should this tab render?".
|
|
7
|
+
*/
|
|
8
|
+
export type PreviewScope = {
|
|
9
|
+
kind: 'living';
|
|
10
|
+
} | {
|
|
11
|
+
kind: 'published';
|
|
12
|
+
} | {
|
|
13
|
+
kind: 'variant';
|
|
14
|
+
variantId: string;
|
|
15
|
+
} | {
|
|
16
|
+
kind: 'release';
|
|
17
|
+
releaseId: string;
|
|
18
|
+
};
|
|
19
|
+
export declare class PreviewScopeConflictError extends Error {
|
|
20
|
+
constructor();
|
|
21
|
+
}
|
|
22
|
+
export declare function createPreviewScope(input: {
|
|
23
|
+
variantId?: string | null;
|
|
24
|
+
releaseId?: string | null;
|
|
25
|
+
}): PreviewScope;
|
|
26
|
+
export declare function readPreviewScopeFromSearch(search: string): PreviewScope;
|
|
27
|
+
export declare function writePreviewScopeToSearchParams(params: URLSearchParams, scope: PreviewScope): void;
|
|
28
|
+
export declare function previewScopeKey(scope: PreviewScope): string;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { PREVIEW_RELEASE_QUERY_PARAM, PREVIEW_VARIANT_QUERY_PARAM, normalizePreviewReleaseId, normalizePreviewVariantId, } from './preview-identity.js';
|
|
2
|
+
export class PreviewScopeConflictError extends Error {
|
|
3
|
+
constructor() {
|
|
4
|
+
super('A preview view cannot select a variant and a release together.');
|
|
5
|
+
this.name = 'PreviewScopeConflictError';
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export function createPreviewScope(input) {
|
|
9
|
+
const variantId = normalizePreviewVariantId(input.variantId);
|
|
10
|
+
const releaseId = normalizePreviewReleaseId(input.releaseId);
|
|
11
|
+
if (variantId && releaseId)
|
|
12
|
+
throw new PreviewScopeConflictError();
|
|
13
|
+
if (releaseId)
|
|
14
|
+
return { kind: 'release', releaseId };
|
|
15
|
+
if (variantId)
|
|
16
|
+
return { kind: 'variant', variantId };
|
|
17
|
+
return { kind: 'living' };
|
|
18
|
+
}
|
|
19
|
+
export function readPreviewScopeFromSearch(search) {
|
|
20
|
+
const params = new URLSearchParams(search);
|
|
21
|
+
return createPreviewScope({
|
|
22
|
+
variantId: params.get(PREVIEW_VARIANT_QUERY_PARAM),
|
|
23
|
+
releaseId: params.get(PREVIEW_RELEASE_QUERY_PARAM),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
export function writePreviewScopeToSearchParams(params, scope) {
|
|
27
|
+
params.delete(PREVIEW_VARIANT_QUERY_PARAM);
|
|
28
|
+
params.delete(PREVIEW_RELEASE_QUERY_PARAM);
|
|
29
|
+
if (scope.kind === 'variant') {
|
|
30
|
+
params.set(PREVIEW_VARIANT_QUERY_PARAM, scope.variantId);
|
|
31
|
+
}
|
|
32
|
+
else if (scope.kind === 'release') {
|
|
33
|
+
params.set(PREVIEW_RELEASE_QUERY_PARAM, scope.releaseId);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function previewScopeKey(scope) {
|
|
37
|
+
switch (scope.kind) {
|
|
38
|
+
case 'published':
|
|
39
|
+
return 'published';
|
|
40
|
+
case 'living':
|
|
41
|
+
return 'living';
|
|
42
|
+
case 'variant':
|
|
43
|
+
return `variant:${scope.variantId}`;
|
|
44
|
+
case 'release':
|
|
45
|
+
return `release:${scope.releaseId}`;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-116 D3 + D5 + D10 — preview identity resolver.
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates the carrier chain (broadcast > storage > URL), the
|
|
5
|
+
* state machine (`identity-state-machine.ts`), and the side effects
|
|
6
|
+
* (storage writes on broadcast confirmation, postMessage listener
|
|
7
|
+
* attach/detach, carrier-miss timeout, recovery backoff).
|
|
8
|
+
*
|
|
9
|
+
* The resolver itself is framework-agnostic : it knows nothing about
|
|
10
|
+
* React, Next, Convex. The runtime store (PR4) is the framework-aware
|
|
11
|
+
* consumer that ties the resolver to the PreviewGrant + Convex client
|
|
12
|
+
* lifecycle.
|
|
13
|
+
*
|
|
14
|
+
* D5 invariants (codex R6) :
|
|
15
|
+
* - `init()` is async + retriable, returns Promise<void>, never
|
|
16
|
+
* rejects ; failure modes surface via `state === 'error'`.
|
|
17
|
+
* - Compare-and-set on state transitions ; concurrent `start()` calls
|
|
18
|
+
* resolve to "first wins, subsequent observe new state and bail".
|
|
19
|
+
* - HMR-durability via the runtime cell (PR4 wires globalThis ; the
|
|
20
|
+
* resolver itself is a single per-iframe instance owned by store.ts).
|
|
21
|
+
*/
|
|
22
|
+
import type { Carrier, IdentityListener, IdentitySnapshot, PreviewIdentity, ProvisionalIdentity } from './identity-types.js';
|
|
23
|
+
import { STORAGE_KEY } from './carriers/storage.js';
|
|
24
|
+
/** Default carrier-miss timeout (D5). 3 seconds matches the ADR. */
|
|
25
|
+
export declare const DEFAULT_CARRIER_TIMEOUT_MS = 3000;
|
|
26
|
+
/** Recovery backoff (ms) — exponential, capped. D4. */
|
|
27
|
+
export declare const RECOVERY_BACKOFF_MS: readonly [200, 800, 2000];
|
|
28
|
+
export interface CreateResolverOpts {
|
|
29
|
+
/** Authoritative projectId from the `<Storystick projectId>` prop. */
|
|
30
|
+
expectedProjectId: string;
|
|
31
|
+
/** Strict-precedence carriers. The broadcast carrier is reactive
|
|
32
|
+
* (postMessage listener) and not part of the read-pass list. */
|
|
33
|
+
carriers: {
|
|
34
|
+
storage?: Carrier;
|
|
35
|
+
url?: Carrier;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Carrier-miss timeout (D5). When all carriers miss AND no broadcast
|
|
39
|
+
* arrives within this window, the resolver transitions to `error`.
|
|
40
|
+
* Test harnesses inject a small value (10ms) for deterministic
|
|
41
|
+
* timeout tests.
|
|
42
|
+
*/
|
|
43
|
+
carrierTimeoutMs?: number;
|
|
44
|
+
/** Wall clock — injectable. */
|
|
45
|
+
now?: () => number;
|
|
46
|
+
/**
|
|
47
|
+
* Optional override for the `window` that receives broadcasts.
|
|
48
|
+
* Tests pass a jsdom stub. Production uses the global `window`.
|
|
49
|
+
*/
|
|
50
|
+
win?: Window;
|
|
51
|
+
/**
|
|
52
|
+
* Optional override for `window.parent` that the resolver targets
|
|
53
|
+
* with session-request envelopes. Tests pass a stub.
|
|
54
|
+
*/
|
|
55
|
+
parent?: Window;
|
|
56
|
+
/**
|
|
57
|
+
* Storage writer — fires on every confirmed broadcast (D7 A3 write
|
|
58
|
+
* timing). Defaults to the sessionStorage writer in `carriers/storage`.
|
|
59
|
+
* Test harnesses override with an in-memory spy.
|
|
60
|
+
*/
|
|
61
|
+
storageWriter?: (identity: PreviewIdentity) => void;
|
|
62
|
+
/**
|
|
63
|
+
* Storage clearer — fires on identity invalidation (e.g. recovery
|
|
64
|
+
* exhausted, project mismatch). The default clears embedded storage and
|
|
65
|
+
* retains a standalone public hint for retry/exit, never an authority.
|
|
66
|
+
*/
|
|
67
|
+
storageClearer?: () => void;
|
|
68
|
+
}
|
|
69
|
+
export interface IdentityResolver {
|
|
70
|
+
/**
|
|
71
|
+
* Begin carrier resolution. Idempotent — concurrent calls collapse
|
|
72
|
+
* to the in-flight promise. Returns `Promise<void>` that resolves
|
|
73
|
+
* once the state machine leaves `resolving-identity` (transition
|
|
74
|
+
* to `handshaking` or `error`). NEVER rejects ; failure surfaces
|
|
75
|
+
* via `state === 'error'`.
|
|
76
|
+
*/
|
|
77
|
+
start(): Promise<void>;
|
|
78
|
+
/** Tear down listeners + clear pending timeouts. */
|
|
79
|
+
stop(): void;
|
|
80
|
+
/** Test-only reset to initial snapshot ; clears listeners. */
|
|
81
|
+
reset(): void;
|
|
82
|
+
getSnapshot(): IdentitySnapshot;
|
|
83
|
+
subscribe(listener: IdentityListener): () => void;
|
|
84
|
+
/**
|
|
85
|
+
* External notifications. The runtime store projects PreviewGrant and
|
|
86
|
+
* subscription readiness through the legacy identity state machine.
|
|
87
|
+
*/
|
|
88
|
+
notifyAuthSucceeded(): void;
|
|
89
|
+
notifyAuthFailed(err: {
|
|
90
|
+
code: string;
|
|
91
|
+
message: string;
|
|
92
|
+
}): void;
|
|
93
|
+
/**
|
|
94
|
+
* Mark the channel as disconnected. The resolver attempts up to
|
|
95
|
+
* `MAX_RECOVERY_RETRIES` re-handshakes before transitioning to
|
|
96
|
+
* `error`.
|
|
97
|
+
*/
|
|
98
|
+
notifyDisconnect(err?: {
|
|
99
|
+
code: string;
|
|
100
|
+
message: string;
|
|
101
|
+
}): void;
|
|
102
|
+
/**
|
|
103
|
+
* Inject a broadcast envelope (for test harnesses + direct unit
|
|
104
|
+
* tests). The resolver's attached `message` listener invokes this
|
|
105
|
+
* automatically on legitimate envelopes.
|
|
106
|
+
*/
|
|
107
|
+
ingestBroadcast(identity: ProvisionalIdentity, eventOrigin: string): void;
|
|
108
|
+
}
|
|
109
|
+
export declare function createIdentityResolver(opts: CreateResolverOpts): IdentityResolver;
|
|
110
|
+
export { STORAGE_KEY };
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-116 D3 + D5 + D10 — preview identity resolver.
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates the carrier chain (broadcast > storage > URL), the
|
|
5
|
+
* state machine (`identity-state-machine.ts`), and the side effects
|
|
6
|
+
* (storage writes on broadcast confirmation, postMessage listener
|
|
7
|
+
* attach/detach, carrier-miss timeout, recovery backoff).
|
|
8
|
+
*
|
|
9
|
+
* The resolver itself is framework-agnostic : it knows nothing about
|
|
10
|
+
* React, Next, Convex. The runtime store (PR4) is the framework-aware
|
|
11
|
+
* consumer that ties the resolver to the PreviewGrant + Convex client
|
|
12
|
+
* lifecycle.
|
|
13
|
+
*
|
|
14
|
+
* D5 invariants (codex R6) :
|
|
15
|
+
* - `init()` is async + retriable, returns Promise<void>, never
|
|
16
|
+
* rejects ; failure modes surface via `state === 'error'`.
|
|
17
|
+
* - Compare-and-set on state transitions ; concurrent `start()` calls
|
|
18
|
+
* resolve to "first wins, subsequent observe new state and bail".
|
|
19
|
+
* - HMR-durability via the runtime cell (PR4 wires globalThis ; the
|
|
20
|
+
* resolver itself is a single per-iframe instance owned by store.ts).
|
|
21
|
+
*/
|
|
22
|
+
import { INITIAL_SNAPSHOT, MAX_RECOVERY_RETRIES, transition, } from './identity-state-machine.js';
|
|
23
|
+
import { attachBroadcastListener, postSessionRequest } from './carriers/postmessage.js';
|
|
24
|
+
import { STORAGE_KEY, clearStoredAfterAuthFailure, writeStored, } from './carriers/storage.js';
|
|
25
|
+
/** Default carrier-miss timeout (D5). 3 seconds matches the ADR. */
|
|
26
|
+
export const DEFAULT_CARRIER_TIMEOUT_MS = 3_000;
|
|
27
|
+
/** Recovery backoff (ms) — exponential, capped. D4. */
|
|
28
|
+
export const RECOVERY_BACKOFF_MS = [200, 800, 2_000];
|
|
29
|
+
export function createIdentityResolver(opts) {
|
|
30
|
+
const expectedProjectId = opts.expectedProjectId;
|
|
31
|
+
if (typeof expectedProjectId !== 'string' || expectedProjectId.length === 0) {
|
|
32
|
+
throw new Error('createIdentityResolver: expectedProjectId is required');
|
|
33
|
+
}
|
|
34
|
+
const now = opts.now ?? Date.now;
|
|
35
|
+
const carrierTimeoutMs = opts.carrierTimeoutMs ?? DEFAULT_CARRIER_TIMEOUT_MS;
|
|
36
|
+
const storageWriter = opts.storageWriter ?? ((id) => void writeStored(id, now));
|
|
37
|
+
const storageClearer = opts.storageClearer ?? clearStoredAfterAuthFailure;
|
|
38
|
+
// Resolver-local state cell. PR4's store.ts persists this on
|
|
39
|
+
// `globalThis` to survive HMR. For PR1 the resolver is a fresh per-
|
|
40
|
+
// creation instance ; tests that span HMR cycles construct a new
|
|
41
|
+
// resolver explicitly.
|
|
42
|
+
let snap = { ...INITIAL_SNAPSHOT };
|
|
43
|
+
let inFlightStartPromise = null;
|
|
44
|
+
let carrierTimeoutTimer = null;
|
|
45
|
+
let detachBroadcast = null;
|
|
46
|
+
let recoveryTimer = null;
|
|
47
|
+
// Locked parent origin after the first confirmed broadcast (D2).
|
|
48
|
+
let lockedParentOrigin = null;
|
|
49
|
+
// Listeners — Set so subscribe/unsubscribe is O(1).
|
|
50
|
+
const listeners = new Set();
|
|
51
|
+
function notify() {
|
|
52
|
+
const current = snap;
|
|
53
|
+
for (const listener of listeners) {
|
|
54
|
+
try {
|
|
55
|
+
listener(current);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Defensive — a listener exception must not break the fan-out.
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function dispatch(event) {
|
|
63
|
+
const prev = snap;
|
|
64
|
+
const next = transition(prev, event);
|
|
65
|
+
if (next === prev)
|
|
66
|
+
return;
|
|
67
|
+
snap = next;
|
|
68
|
+
notify();
|
|
69
|
+
}
|
|
70
|
+
function emitSessionRequest(reason) {
|
|
71
|
+
postSessionRequest({
|
|
72
|
+
projectId: expectedProjectId,
|
|
73
|
+
reason,
|
|
74
|
+
parent: opts.parent,
|
|
75
|
+
now,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
function attachBroadcast() {
|
|
79
|
+
if (detachBroadcast !== null)
|
|
80
|
+
return;
|
|
81
|
+
detachBroadcast = attachBroadcastListener({
|
|
82
|
+
expectedProjectId,
|
|
83
|
+
win: opts.win,
|
|
84
|
+
onBroadcast: (identity, eventOrigin) => {
|
|
85
|
+
// Origin-lock — refuse subsequent broadcasts from any other
|
|
86
|
+
// origin (D2). The first confirmed broadcast wins.
|
|
87
|
+
if (lockedParentOrigin === null) {
|
|
88
|
+
lockedParentOrigin = eventOrigin;
|
|
89
|
+
}
|
|
90
|
+
else if (lockedParentOrigin !== eventOrigin) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
ingestBroadcastInternal(identity);
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
function ingestBroadcastInternal(identity) {
|
|
98
|
+
// D10 reject path — projectId mismatch when an identity is already
|
|
99
|
+
// established. Bail BEFORE dispatching so the state machine never
|
|
100
|
+
// sees the rejected event AND the storage writer never fires.
|
|
101
|
+
if (snap.identity !== null &&
|
|
102
|
+
snap.identity.projectId !== identity.projectId) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
// Dispatch and observe whether the state machine accepted the
|
|
106
|
+
// broadcast. Acceptance is detected by comparing post-dispatch
|
|
107
|
+
// identity tuple to the incoming one.
|
|
108
|
+
dispatch({ type: 'broadcast-received', identity, now: now() });
|
|
109
|
+
const after = snap;
|
|
110
|
+
const accepted = after.identity !== null &&
|
|
111
|
+
after.identity.sessionId === identity.sessionId &&
|
|
112
|
+
after.identity.projectId === identity.projectId;
|
|
113
|
+
if (accepted) {
|
|
114
|
+
// D7 A3 write timing — fires on every confirmed broadcast
|
|
115
|
+
// (Confirm / Promote / Replace AND idempotent no-ops so the TTL
|
|
116
|
+
// refreshes). Hardened mode storage exceptions are silent.
|
|
117
|
+
try {
|
|
118
|
+
storageWriter(after.identity);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
/* hardened mode — silent */
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// First broadcast = clear the carrier-miss timeout.
|
|
125
|
+
if (after.state !== 'resolving-identity' &&
|
|
126
|
+
carrierTimeoutTimer !== null) {
|
|
127
|
+
clearTimeout(carrierTimeoutTimer);
|
|
128
|
+
carrierTimeoutTimer = null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function readCarrierChain() {
|
|
132
|
+
const ctx = { expectedProjectId, now };
|
|
133
|
+
// Storage first (tier-2), URL last (tier-3 bootstrap). Live
|
|
134
|
+
// broadcast (tier-1) is reactive ; it does not appear in this
|
|
135
|
+
// chain.
|
|
136
|
+
for (const carrierKey of ['storage', 'url']) {
|
|
137
|
+
const carrier = opts.carriers[carrierKey];
|
|
138
|
+
if (!carrier)
|
|
139
|
+
continue;
|
|
140
|
+
const result = carrier.read(ctx);
|
|
141
|
+
if (result.kind === 'hit') {
|
|
142
|
+
return { source: carrier.name, identity: result.identity };
|
|
143
|
+
}
|
|
144
|
+
// miss / error → continue to next carrier.
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
function start() {
|
|
149
|
+
// Compare-and-set : only the first caller transitions idle →
|
|
150
|
+
// resolving-identity. Concurrent callers (StrictMode double-mount,
|
|
151
|
+
// HMR re-mount) observe the in-flight promise and await it.
|
|
152
|
+
if (snap.state !== 'idle' && snap.state !== 'disconnected') {
|
|
153
|
+
return inFlightStartPromise ?? Promise.resolve();
|
|
154
|
+
}
|
|
155
|
+
// CRITICAL — set inFlightStartPromise BEFORE any notify() (codex
|
|
156
|
+
// R6 #2). A synchronous reentrancy via the listener fan-out must
|
|
157
|
+
// see the slot populated, not null.
|
|
158
|
+
const flight = new Promise((resolve) => {
|
|
159
|
+
const unsubscribe = subscribe((current) => {
|
|
160
|
+
if (current.state === 'handshaking' ||
|
|
161
|
+
current.state === 'authenticated' ||
|
|
162
|
+
current.state === 'error') {
|
|
163
|
+
unsubscribe();
|
|
164
|
+
inFlightStartPromise = null;
|
|
165
|
+
resolve();
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
inFlightStartPromise = flight;
|
|
170
|
+
attachBroadcast();
|
|
171
|
+
dispatch({ type: 'start' });
|
|
172
|
+
// Carrier read-pass — synchronous.
|
|
173
|
+
const carrierHit = readCarrierChain();
|
|
174
|
+
if (carrierHit !== null) {
|
|
175
|
+
dispatch({
|
|
176
|
+
type: 'carrier-hit',
|
|
177
|
+
source: carrierHit.source,
|
|
178
|
+
identity: carrierHit.identity,
|
|
179
|
+
now: now(),
|
|
180
|
+
});
|
|
181
|
+
// ADR-131 C5 — URL / storage carriers do NOT carry parentOrigin
|
|
182
|
+
// (only the broadcast carrier does — `postmessage.ts` line 115).
|
|
183
|
+
// Without this emission the iframe's `identity.parentOrigin`
|
|
184
|
+
// stays `null` forever on the fast carrier-hit path, which
|
|
185
|
+
// breaks downstream preview message validators
|
|
186
|
+
// (validators reject unconditionally when
|
|
187
|
+
// parentOrigin === null). Emit a `session-request` so Studio
|
|
188
|
+
// sends back a broadcast that promotes identity with the locked
|
|
189
|
+
// parentOrigin (`identity-state-machine.ts:246` promotion
|
|
190
|
+
// branch). The Studio side already handles unsolicited drains
|
|
191
|
+
// via the queue+drain pattern in `use-preview-auth-bridge.ts` —
|
|
192
|
+
// the broadcast lands ~O(1ms) later, well before the first user
|
|
193
|
+
// edit triggers a build envelope.
|
|
194
|
+
emitSessionRequest('boot');
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
// No carrier hit — arm the timeout AND post the session-request.
|
|
198
|
+
// Broadcast may still arrive within the window and resolve the
|
|
199
|
+
// identity ; if not, the timeout fires `error`.
|
|
200
|
+
emitSessionRequest('boot');
|
|
201
|
+
carrierTimeoutTimer = setTimeout(() => {
|
|
202
|
+
carrierTimeoutTimer = null;
|
|
203
|
+
dispatch({ type: 'carrier-timeout' });
|
|
204
|
+
}, carrierTimeoutMs);
|
|
205
|
+
}
|
|
206
|
+
return flight;
|
|
207
|
+
}
|
|
208
|
+
function stop() {
|
|
209
|
+
if (carrierTimeoutTimer !== null) {
|
|
210
|
+
clearTimeout(carrierTimeoutTimer);
|
|
211
|
+
carrierTimeoutTimer = null;
|
|
212
|
+
}
|
|
213
|
+
if (recoveryTimer !== null) {
|
|
214
|
+
clearTimeout(recoveryTimer);
|
|
215
|
+
recoveryTimer = null;
|
|
216
|
+
}
|
|
217
|
+
if (detachBroadcast !== null) {
|
|
218
|
+
detachBroadcast();
|
|
219
|
+
detachBroadcast = null;
|
|
220
|
+
}
|
|
221
|
+
inFlightStartPromise = null;
|
|
222
|
+
}
|
|
223
|
+
function reset() {
|
|
224
|
+
stop();
|
|
225
|
+
snap = { ...INITIAL_SNAPSHOT };
|
|
226
|
+
listeners.clear();
|
|
227
|
+
lockedParentOrigin = null;
|
|
228
|
+
}
|
|
229
|
+
function subscribe(listener) {
|
|
230
|
+
listeners.add(listener);
|
|
231
|
+
return () => {
|
|
232
|
+
listeners.delete(listener);
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function notifyAuthSucceeded() {
|
|
236
|
+
dispatch({ type: 'auth-succeeded' });
|
|
237
|
+
}
|
|
238
|
+
function notifyAuthFailed(err) {
|
|
239
|
+
dispatch({ type: 'auth-failed', code: err.code, message: err.message });
|
|
240
|
+
if (snap.state === 'error') {
|
|
241
|
+
// Embedded failures invalidate the carrier. Autonomous views retain
|
|
242
|
+
// their public hint so a second reload can still offer Exit Preview.
|
|
243
|
+
try {
|
|
244
|
+
storageClearer();
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
/* hardened mode — silent */
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function scheduleRecovery() {
|
|
252
|
+
if (recoveryTimer !== null)
|
|
253
|
+
return;
|
|
254
|
+
if (snap.retryCount >= MAX_RECOVERY_RETRIES) {
|
|
255
|
+
dispatch({
|
|
256
|
+
type: 'recovery-exhausted',
|
|
257
|
+
code: 'identity_recovery_exhausted',
|
|
258
|
+
message: 'Preview connection lost and recovery attempts exhausted. Reopen the document in Storystick Studio to retry.',
|
|
259
|
+
});
|
|
260
|
+
try {
|
|
261
|
+
storageClearer();
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
/* hardened mode — silent */
|
|
265
|
+
}
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
// retryCount on the snapshot is incremented BY recovery-begin so the
|
|
269
|
+
// index for the backoff array is the current retryCount (0 → 200ms,
|
|
270
|
+
// 1 → 800ms, 2 → 2000ms).
|
|
271
|
+
const delay = RECOVERY_BACKOFF_MS[snap.retryCount] ??
|
|
272
|
+
RECOVERY_BACKOFF_MS[RECOVERY_BACKOFF_MS.length - 1];
|
|
273
|
+
recoveryTimer = setTimeout(() => {
|
|
274
|
+
recoveryTimer = null;
|
|
275
|
+
// Bail if state moved (e.g. broadcast already lifted us out of
|
|
276
|
+
// disconnected mid-backoff).
|
|
277
|
+
if (snap.state !== 'disconnected')
|
|
278
|
+
return;
|
|
279
|
+
dispatch({ type: 'recovery-begin' });
|
|
280
|
+
// After recovery-begin we are back in resolving-identity ; emit a
|
|
281
|
+
// recovery session-request and arm the carrier-miss timeout. We do
|
|
282
|
+
// NOT re-read storage/URL : on recovery, the identity should come
|
|
283
|
+
// from a fresh broadcast, not a stale tier-2 entry.
|
|
284
|
+
emitSessionRequest('recovery');
|
|
285
|
+
if (carrierTimeoutTimer !== null)
|
|
286
|
+
clearTimeout(carrierTimeoutTimer);
|
|
287
|
+
carrierTimeoutTimer = setTimeout(() => {
|
|
288
|
+
carrierTimeoutTimer = null;
|
|
289
|
+
dispatch({ type: 'carrier-timeout' });
|
|
290
|
+
// If carrier-timeout fired during recovery, we're now in error
|
|
291
|
+
// — clear storage to avoid auto-restoring on the next mount.
|
|
292
|
+
if (snap.state === 'error') {
|
|
293
|
+
try {
|
|
294
|
+
storageClearer();
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
/* hardened mode */
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}, carrierTimeoutMs);
|
|
301
|
+
}, delay);
|
|
302
|
+
}
|
|
303
|
+
function notifyDisconnect(err) {
|
|
304
|
+
dispatch({ type: 'disconnect', code: err?.code, message: err?.message });
|
|
305
|
+
if (snap.state === 'disconnected') {
|
|
306
|
+
scheduleRecovery();
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function ingestBroadcast(identity, eventOrigin) {
|
|
310
|
+
if (lockedParentOrigin === null) {
|
|
311
|
+
lockedParentOrigin = eventOrigin;
|
|
312
|
+
}
|
|
313
|
+
else if (lockedParentOrigin !== eventOrigin) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
ingestBroadcastInternal(identity);
|
|
317
|
+
}
|
|
318
|
+
function getSnapshot() {
|
|
319
|
+
return snap;
|
|
320
|
+
}
|
|
321
|
+
return {
|
|
322
|
+
start,
|
|
323
|
+
stop,
|
|
324
|
+
reset,
|
|
325
|
+
getSnapshot,
|
|
326
|
+
subscribe,
|
|
327
|
+
notifyAuthSucceeded,
|
|
328
|
+
notifyAuthFailed,
|
|
329
|
+
notifyDisconnect,
|
|
330
|
+
ingestBroadcast,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
// Re-export STORAGE_KEY for the namespace-pinning test.
|
|
334
|
+
export { STORAGE_KEY };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Public URL hint parsing only; session admission remains server-owned. */
|
|
2
|
+
export declare const PREVIEW_SESSION_QUERY_PARAM = "_ss_session";
|
|
3
|
+
export declare function readPreviewSessionIdFromSearch(search: string, paramName?: string): string | null;
|
|
4
|
+
export declare function hasPreviewSessionParam(search: string, paramName?: string): boolean;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Public URL hint parsing only; session admission remains server-owned. */
|
|
2
|
+
export const PREVIEW_SESSION_QUERY_PARAM = '_ss_session';
|
|
3
|
+
export function readPreviewSessionIdFromSearch(search, paramName = PREVIEW_SESSION_QUERY_PARAM) {
|
|
4
|
+
try {
|
|
5
|
+
const sessionId = new URLSearchParams(search).get(paramName);
|
|
6
|
+
return sessionId !== null && sessionId.length > 0 ? sessionId : null;
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function hasPreviewSessionParam(search, paramName = PREVIEW_SESSION_QUERY_PARAM) {
|
|
13
|
+
try {
|
|
14
|
+
return new URLSearchParams(search).has(paramName);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|