@storystick/next 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 +31 -0
- package/README.md +522 -2
- package/dist/.source-hash +1 -0
- package/dist/cache-tags.d.ts +2 -0
- package/dist/cache-tags.js +12 -0
- package/dist/clean.d.ts +1 -0
- package/dist/clean.js +1 -0
- package/dist/code-version.d.ts +29 -0
- package/dist/code-version.js +48 -0
- package/dist/file.d.ts +1 -0
- package/dist/file.js +1 -0
- package/dist/i18n-seo.d.ts +1 -0
- package/dist/i18n-seo.js +1 -0
- package/dist/image.d.ts +11 -0
- package/dist/image.js +22 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/index.react-server.d.ts +3 -0
- package/dist/index.react-server.js +12 -0
- package/dist/link.d.ts +10 -0
- package/dist/link.js +8 -0
- package/dist/preview/index.d.ts +29 -0
- package/dist/preview/index.js +24 -0
- package/dist/preview/internal.d.ts +61 -0
- package/dist/preview/internal.js +97 -0
- package/dist/preview/preview-request-identity.d.ts +4 -0
- package/dist/preview/preview-request-identity.js +63 -0
- package/dist/preview/provenance-config.d.ts +42 -0
- package/dist/preview/provenance-config.js +132 -0
- package/dist/preview/provenance-frame-builder.d.ts +6 -0
- package/dist/preview/provenance-frame-builder.js +10 -0
- package/dist/preview/provenance-response.d.ts +5 -0
- package/dist/preview/provenance-response.js +9 -0
- package/dist/preview/provenance-runtime-adapter.d.ts +9 -0
- package/dist/preview/provenance-runtime-adapter.js +42 -0
- package/dist/preview/render-frame-provenance.d.ts +23 -0
- package/dist/preview/render-frame-provenance.js +47 -0
- package/dist/preview/route-authoring.d.ts +3 -0
- package/dist/preview/route-authoring.js +80 -0
- package/dist/preview/route-context.d.ts +3 -0
- package/dist/preview/route-context.js +38 -0
- package/dist/preview/route-disable.d.ts +7 -0
- package/dist/preview/route-disable.js +63 -0
- package/dist/preview/route-document-provenance.d.ts +23 -0
- package/dist/preview/route-document-provenance.js +46 -0
- package/dist/preview/route-errors.d.ts +1 -0
- package/dist/preview/route-errors.js +1 -0
- package/dist/preview/route-handler.d.ts +36 -0
- package/dist/preview/route-handler.js +139 -0
- package/dist/preview/route-initial.d.ts +9 -0
- package/dist/preview/route-initial.js +156 -0
- package/dist/preview/route-provenance-runtime.d.ts +34 -0
- package/dist/preview/route-provenance-runtime.js +90 -0
- package/dist/preview/route-token.d.ts +6 -0
- package/dist/preview/route-token.js +46 -0
- package/dist/preview/server-preview-internals.d.ts +3 -0
- package/dist/preview/server-preview-internals.js +3 -0
- package/dist/preview/slice-locale.d.ts +5 -0
- package/dist/preview/slice-locale.js +15 -0
- package/dist/preview-error-surface.d.ts +7 -0
- package/dist/preview-error-surface.js +20 -0
- package/dist/preview-exit.d.ts +7 -0
- package/dist/preview-exit.js +71 -0
- package/dist/preview-metadata.d.ts +15 -0
- package/dist/preview-metadata.js +13 -0
- package/dist/preview-mode-latch.d.ts +133 -0
- package/dist/preview-mode-latch.js +231 -0
- package/dist/preview-nav-location.d.ts +12 -0
- package/dist/preview-nav-location.js +149 -0
- package/dist/preview-nav.d.ts +94 -0
- package/dist/preview-nav.js +523 -0
- package/dist/preview-refresh-coordinator.d.ts +26 -0
- package/dist/preview-refresh-coordinator.js +93 -0
- package/dist/preview-render-error.d.ts +11 -0
- package/dist/preview-render-error.js +36 -0
- package/dist/preview-render-recovery.d.ts +9 -0
- package/dist/preview-render-recovery.js +42 -0
- package/dist/preview-session-recovery.d.ts +1 -0
- package/dist/preview-session-recovery.js +1 -0
- package/dist/preview-standalone-host.d.ts +3 -0
- package/dist/preview-standalone-host.js +55 -0
- package/dist/preview-text.d.ts +2 -0
- package/dist/preview-text.js +1 -0
- package/dist/preview-view-bootstrap.d.ts +5 -0
- package/dist/preview-view-bootstrap.js +13 -0
- package/dist/public-delivery.d.ts +6 -0
- package/dist/public-delivery.js +7 -0
- package/dist/query-fetch-options.d.ts +7 -0
- package/dist/query-fetch-options.js +193 -0
- package/dist/query.d.ts +2 -0
- package/dist/query.js +1 -0
- package/dist/rendering.d.ts +33 -0
- package/dist/rendering.js +42 -0
- package/dist/revalidate.d.ts +12 -0
- package/dist/revalidate.js +33 -0
- package/dist/rich-text.d.ts +2 -0
- package/dist/rich-text.js +1 -0
- package/dist/server-runtime.d.ts +208 -0
- package/dist/server-runtime.js +1584 -0
- package/dist/server.d.ts +8 -0
- package/dist/server.js +6 -0
- package/dist/slice-zone.d.ts +2 -0
- package/dist/slice-zone.js +1 -0
- package/dist/storystick-component-types.d.ts +26 -0
- package/dist/storystick-component-types.js +1 -0
- package/dist/storystick-component.d.ts +10 -0
- package/dist/storystick-component.js +45 -0
- package/dist/storystick-error-recovery.d.ts +12 -0
- package/dist/storystick-error-recovery.js +11 -0
- package/dist/storystick-preview-runtime.d.ts +30 -0
- package/dist/storystick-preview-runtime.js +561 -0
- package/dist/storystick.d.ts +24 -0
- package/dist/storystick.js +23 -0
- package/dist/testing.d.ts +9 -0
- package/dist/testing.js +40 -0
- package/dist/video.d.ts +1 -0
- package/dist/video.js +1 -0
- package/package.json +128 -3
- package/recipes/README.md +91 -0
- package/recipes/resource-mirror.ts +49 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/** One-shot PreviewGrant activation route. */
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
3
|
+
import { draftMode, cookies } from 'next/headers';
|
|
4
|
+
import { PREVIEW_ACTIVATION_QUERY_PARAM, PreviewActivationError, PreviewGrantExchangeError, PreviewRedirectError, activatePreviewGrantForServer, assertSafePreviewRedirect, classifyPreviewGrantError, isSafePreviewSessionId, parsePreviewActivation, previewCookieAttributes, } from '@storystick/preview/server/grant';
|
|
5
|
+
import { getPreviewTransport, resolvePreviewRequestOrigin } from './internal.js';
|
|
6
|
+
import { primePreviewGrantAccess } from '@storystick/preview/server/extract-token';
|
|
7
|
+
import { resolveStorystickApplicationBuildId } from '../code-version.js';
|
|
8
|
+
import { buildPreviewGrantCookieFromActivation, setPreviewGrantCookieOnResponse, } from '@storystick/preview/server/route-session-cookie';
|
|
9
|
+
import { previewInitialRouteErrorResponse, } from './route-errors.js';
|
|
10
|
+
import { PREVIEW_SESSION_QUERY_PARAM, PREVIEW_VARIANT_QUERY_PARAM, PREVIEW_RELEASE_QUERY_PARAM, PreviewScopeConflictError, createPreviewScope, writePreviewScopeToSearchParams, } from '@storystick/preview/identity';
|
|
11
|
+
const DEFAULTS = {
|
|
12
|
+
redirectParam: 'redirect',
|
|
13
|
+
basePath: '',
|
|
14
|
+
};
|
|
15
|
+
export function createStorystickPreviewHandler(options = {}) {
|
|
16
|
+
const redirectParam = options.redirectParam ?? DEFAULTS.redirectParam;
|
|
17
|
+
const basePath = options.basePath ?? DEFAULTS.basePath;
|
|
18
|
+
return async function GET(request) {
|
|
19
|
+
const url = request.nextUrl;
|
|
20
|
+
const sessionId = url.searchParams.get(PREVIEW_SESSION_QUERY_PARAM);
|
|
21
|
+
if (!isSafePreviewSessionId(sessionId)) {
|
|
22
|
+
return withNoReferrer(previewInitialRouteErrorResponse(request, {
|
|
23
|
+
status: 400, code: 'PREVIEW_SESSION_MISSING', message: 'preview session selector is required',
|
|
24
|
+
category: 'validation', recoverable: true, retryable: false,
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
let activation;
|
|
28
|
+
try {
|
|
29
|
+
activation = parsePreviewActivation(url.searchParams.get(PREVIEW_ACTIVATION_QUERY_PARAM));
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (error instanceof PreviewActivationError) {
|
|
33
|
+
return withNoReferrer(previewInitialRouteErrorResponse(request, {
|
|
34
|
+
status: 400, code: 'PREVIEW_ACTIVATION_INVALID', message: 'preview activation is missing or malformed',
|
|
35
|
+
category: 'validation', recoverable: true, retryable: false,
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
let safeRedirect;
|
|
41
|
+
try {
|
|
42
|
+
safeRedirect = assertSafePreviewRedirect(url.searchParams.get(redirectParam) || basePath || '/', basePath);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (error instanceof PreviewRedirectError) {
|
|
46
|
+
console.warn('[storystick/preview] unsafe redirect rejected');
|
|
47
|
+
return withNoReferrer(previewInitialRouteErrorResponse(request, {
|
|
48
|
+
status: 400, code: 'PREVIEW_REDIRECT_UNSAFE', message: 'unsafe redirect',
|
|
49
|
+
category: 'validation', recoverable: true, retryable: false,
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
let previewScope;
|
|
55
|
+
try {
|
|
56
|
+
previewScope = createPreviewScope({
|
|
57
|
+
variantId: url.searchParams.get(PREVIEW_VARIANT_QUERY_PARAM),
|
|
58
|
+
releaseId: url.searchParams.get(PREVIEW_RELEASE_QUERY_PARAM),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (error instanceof PreviewScopeConflictError) {
|
|
63
|
+
return withNoReferrer(previewInitialRouteErrorResponse(request, {
|
|
64
|
+
status: 400, code: 'PREVIEW_SCOPE_CONFLICT', message: 'preview cannot select a variant and a release together',
|
|
65
|
+
category: 'validation', recoverable: true, retryable: false,
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
const requestOrigin = resolvePreviewRequestOrigin(request);
|
|
71
|
+
const transport = await getPreviewTransport();
|
|
72
|
+
const cookieStore = await cookies();
|
|
73
|
+
const scope = { ...transport, requestOrigin };
|
|
74
|
+
let exchange;
|
|
75
|
+
try {
|
|
76
|
+
exchange = await activatePreviewGrantForServer({
|
|
77
|
+
endpoint: transport.endpoint,
|
|
78
|
+
apiKey: transport.apiKey,
|
|
79
|
+
applicationBuildId: resolveStorystickApplicationBuildId(),
|
|
80
|
+
grantId: activation.grantId,
|
|
81
|
+
activationSecret: activation.activationSecret,
|
|
82
|
+
sessionId,
|
|
83
|
+
requestOrigin,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (error instanceof PreviewGrantExchangeError) {
|
|
88
|
+
const publicError = classifyPreviewGrantError(error, 'activate');
|
|
89
|
+
console.warn('[storystick/preview] grant activation failed', {
|
|
90
|
+
code: publicError.code,
|
|
91
|
+
status: publicError.status,
|
|
92
|
+
upstreamCode: error.code,
|
|
93
|
+
upstreamRef: error.ref,
|
|
94
|
+
});
|
|
95
|
+
return withNoReferrer(previewInitialRouteErrorResponse(request, publicError));
|
|
96
|
+
}
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
if (exchange.sessionId !== sessionId ||
|
|
100
|
+
exchange.grantId !== activation.grantId ||
|
|
101
|
+
exchange.previewOrigin !== requestOrigin ||
|
|
102
|
+
exchange.projectId !== (await getPreviewTransport()).projectId ||
|
|
103
|
+
exchange.grantExpiresAt <= Date.now()) {
|
|
104
|
+
return withNoReferrer(previewInitialRouteErrorResponse(request, {
|
|
105
|
+
status: 502, code: 'PREVIEW_GRANT_BINDING_MISMATCH', message: 'preview unavailable',
|
|
106
|
+
category: 'auth', recoverable: true, retryable: false,
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
const cookie = buildPreviewGrantCookieFromActivation(exchange, Date.now(), resolveStorystickApplicationBuildId());
|
|
110
|
+
const draft = await draftMode();
|
|
111
|
+
draft.enable();
|
|
112
|
+
const finalUrl = scrubPreviewBootstrap(safeRedirect, requestOrigin);
|
|
113
|
+
finalUrl.searchParams.set(PREVIEW_SESSION_QUERY_PARAM, sessionId);
|
|
114
|
+
writePreviewScopeToSearchParams(finalUrl.searchParams, previewScope);
|
|
115
|
+
const response = NextResponse.redirect(finalUrl, { status: 302 });
|
|
116
|
+
const requestUrl = new URL(url.pathname + url.search, requestOrigin).toString();
|
|
117
|
+
if (previewCookieAttributes({ expiresIn: 0, requestUrl }).partitioned) {
|
|
118
|
+
// Draft Mode owns the build-bound value and cache semantics. Its cookie
|
|
119
|
+
// must travel in the same partition as the signed Storystick admission.
|
|
120
|
+
// Next exposes the freshly enabled cookie through this mutable store;
|
|
121
|
+
// response cookies take precedence when Next merges its Set-Cookie values.
|
|
122
|
+
const draftCookie = cookieStore.get('__prerender_bypass');
|
|
123
|
+
if (!draftCookie?.value) {
|
|
124
|
+
throw new Error('[storystick/preview] Draft Mode did not emit its cookie');
|
|
125
|
+
}
|
|
126
|
+
response.cookies.set({
|
|
127
|
+
...draftCookie,
|
|
128
|
+
httpOnly: true,
|
|
129
|
+
secure: true,
|
|
130
|
+
sameSite: 'none',
|
|
131
|
+
partitioned: true,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
primePreviewGrantAccess({ ...cookie, contentShapeCapability: exchange.contentShapeCapability,
|
|
135
|
+
admissionSequence: exchange.admissionSequence }, { ...scope, applicationBuildId: cookie.applicationBuildId });
|
|
136
|
+
await setPreviewGrantCookieOnResponse({
|
|
137
|
+
response, scope, reader: cookieStore, sequence: exchange.admissionSequence,
|
|
138
|
+
cookie,
|
|
139
|
+
requestUrl,
|
|
140
|
+
basePath,
|
|
141
|
+
});
|
|
142
|
+
response.headers.set('Cache-Control', 'no-store');
|
|
143
|
+
response.headers.set('Referrer-Policy', 'no-referrer');
|
|
144
|
+
return response;
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function scrubPreviewBootstrap(path, requestOrigin) {
|
|
148
|
+
const result = new URL(path, requestOrigin);
|
|
149
|
+
result.searchParams.delete(PREVIEW_ACTIVATION_QUERY_PARAM);
|
|
150
|
+
result.searchParams.delete(PREVIEW_SESSION_QUERY_PARAM);
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
function withNoReferrer(response) {
|
|
154
|
+
response.headers.set('Referrer-Policy', 'no-referrer');
|
|
155
|
+
return response;
|
|
156
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createStorystickPreviewProvenanceRuntimeHandler` — preview-only
|
|
3
|
+
* Overlay Frame source runtime route.
|
|
4
|
+
*
|
|
5
|
+
* Lower-level standalone factory. The recommended customer mount is
|
|
6
|
+
* `createRouteHandler()` at `app/api/storystick/[...action]/route.ts`.
|
|
7
|
+
* This factory still backs the `/api/storystick/provenance-runtime` action
|
|
8
|
+
* inside the one-file dispatcher:
|
|
9
|
+
*
|
|
10
|
+
* // app/api/storystick/[...action]/route.ts
|
|
11
|
+
* import { createRouteHandler } from '@storystick/next/preview'
|
|
12
|
+
* const storystick = createRouteHandler()
|
|
13
|
+
* export const GET = storystick.GET
|
|
14
|
+
* export const POST = storystick.POST
|
|
15
|
+
*
|
|
16
|
+
* The handler lives under the existing server-only `@storystick/next/preview`
|
|
17
|
+
* subpath, not the root client-safe entry. It gates first, then dynamically
|
|
18
|
+
* imports the runtime script so customer static/client bundles do not carry
|
|
19
|
+
* provenance selectors or message strings.
|
|
20
|
+
*/
|
|
21
|
+
import type { NextRequest } from 'next/server';
|
|
22
|
+
export interface PreviewProvenanceRuntimeHandlerOptions {
|
|
23
|
+
/**
|
|
24
|
+
* Optional server policy. Enabled by default for trusted Preview sessions.
|
|
25
|
+
*/
|
|
26
|
+
enabled?: boolean | (() => boolean | Promise<boolean>);
|
|
27
|
+
/**
|
|
28
|
+
* Require a valid Storystick preview cookie before serving the runtime.
|
|
29
|
+
* Defaults to true. Fixture-only tests can set this to false when they do
|
|
30
|
+
* not run through the real Preview cookie exchange.
|
|
31
|
+
*/
|
|
32
|
+
requirePreviewCookie?: boolean;
|
|
33
|
+
}
|
|
34
|
+
export declare function createStorystickPreviewProvenanceRuntimeHandler(options?: PreviewProvenanceRuntimeHandlerOptions): (request: NextRequest) => Promise<Response>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createStorystickPreviewProvenanceRuntimeHandler` — preview-only
|
|
3
|
+
* Overlay Frame source runtime route.
|
|
4
|
+
*
|
|
5
|
+
* Lower-level standalone factory. The recommended customer mount is
|
|
6
|
+
* `createRouteHandler()` at `app/api/storystick/[...action]/route.ts`.
|
|
7
|
+
* This factory still backs the `/api/storystick/provenance-runtime` action
|
|
8
|
+
* inside the one-file dispatcher:
|
|
9
|
+
*
|
|
10
|
+
* // app/api/storystick/[...action]/route.ts
|
|
11
|
+
* import { createRouteHandler } from '@storystick/next/preview'
|
|
12
|
+
* const storystick = createRouteHandler()
|
|
13
|
+
* export const GET = storystick.GET
|
|
14
|
+
* export const POST = storystick.POST
|
|
15
|
+
*
|
|
16
|
+
* The handler lives under the existing server-only `@storystick/next/preview`
|
|
17
|
+
* subpath, not the root client-safe entry. It gates first, then dynamically
|
|
18
|
+
* imports the runtime script so customer static/client bundles do not carry
|
|
19
|
+
* provenance selectors or message strings.
|
|
20
|
+
*/
|
|
21
|
+
import { cookies } from 'next/headers';
|
|
22
|
+
import { getPreviewTransport } from "./internal.js";
|
|
23
|
+
import { readPreviewGrantCookie } from '@storystick/preview/server/extract-token';
|
|
24
|
+
import { resolvePreviewRequestOrigin } from './internal.js';
|
|
25
|
+
import { unstable_resolveStorystickPreviewProvenanceConfig } from './provenance-config.js';
|
|
26
|
+
export function createStorystickPreviewProvenanceRuntimeHandler(options = {}) {
|
|
27
|
+
return async function GET(request) {
|
|
28
|
+
if (!(await isEnabled(options.enabled)))
|
|
29
|
+
return notFound();
|
|
30
|
+
if (shouldRequirePreviewCookie(options.requirePreviewCookie)) {
|
|
31
|
+
const active = await hasValidPreviewGrant(request);
|
|
32
|
+
if (!active)
|
|
33
|
+
return notFound();
|
|
34
|
+
}
|
|
35
|
+
const { previewProvenanceRuntimeScript } = await import('@storystick/preview/provenance');
|
|
36
|
+
return new Response(previewProvenanceRuntimeScript, {
|
|
37
|
+
headers: runtimeHeaders(),
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function shouldRequirePreviewCookie(option) {
|
|
42
|
+
if (typeof option === 'boolean')
|
|
43
|
+
return option;
|
|
44
|
+
return !unstable_resolveStorystickPreviewProvenanceConfig().fixtureEnabled;
|
|
45
|
+
}
|
|
46
|
+
async function isEnabled(gate) {
|
|
47
|
+
if (typeof gate === 'function')
|
|
48
|
+
return Boolean(await gate());
|
|
49
|
+
if (typeof gate === 'boolean')
|
|
50
|
+
return gate;
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
async function hasValidPreviewGrant(request) {
|
|
54
|
+
try {
|
|
55
|
+
const cookieStore = await cookies();
|
|
56
|
+
const grant = await readPreviewGrantCookie(cookieStore, { ...await getPreviewTransport(), requestOrigin: resolvePreviewRequestOrigin(request) });
|
|
57
|
+
if (!grant)
|
|
58
|
+
return false;
|
|
59
|
+
if (grant.previewOrigin !== resolvePreviewRequestOrigin(request))
|
|
60
|
+
return false;
|
|
61
|
+
// The origin-bound, HttpOnly preview cookie is the authority for this
|
|
62
|
+
// runtime. Requiring Next's separate Draft Mode cookie here makes the
|
|
63
|
+
// reporter depend on an implementation detail of the redirect response:
|
|
64
|
+
// in local production lanes that cookie is not retained, even while the
|
|
65
|
+
// verified preview cookie and the authenticated iframe session are live.
|
|
66
|
+
// Serving this script cannot grant preview access on its own; every
|
|
67
|
+
// privileged path still validates the same preview cookie/server token.
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function runtimeHeaders() {
|
|
75
|
+
return {
|
|
76
|
+
'Cache-Control': 'no-store',
|
|
77
|
+
'Content-Type': 'application/javascript; charset=utf-8',
|
|
78
|
+
'X-Content-Type-Options': 'nosniff',
|
|
79
|
+
'X-Robots-Tag': 'noindex, nofollow, noarchive',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function notFound() {
|
|
83
|
+
return new Response('Not found', {
|
|
84
|
+
status: 404,
|
|
85
|
+
headers: {
|
|
86
|
+
'Cache-Control': 'no-store',
|
|
87
|
+
'X-Robots-Tag': 'noindex, nofollow, noarchive',
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Read the installed admission; credential renewal never overwrites it. */
|
|
2
|
+
import type { NextRequest } from 'next/server';
|
|
3
|
+
export interface PreviewTokenHandlerOptions {
|
|
4
|
+
basePath?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function createStorystickPreviewTokenHandler(options?: PreviewTokenHandlerOptions): (request: NextRequest) => Promise<Response>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { cookies, draftMode } from 'next/headers';
|
|
3
|
+
import { PREVIEW_SESSION_QUERY_PARAM } from '@storystick/preview/identity';
|
|
4
|
+
import { isSafePreviewSessionId } from '@storystick/preview/server/grant';
|
|
5
|
+
import { resolveStorystickApplicationBuildId } from '../code-version.js';
|
|
6
|
+
import { createPreviewControlClient, getPreviewTransport, resolvePreviewRequestOrigin } from './internal.js';
|
|
7
|
+
import { resolvePreviewGrantAccess } from '@storystick/preview/server/extract-token';
|
|
8
|
+
import { prunePreviewAdmissionCookies } from '@storystick/preview/server/route-session-cookie';
|
|
9
|
+
import { previewControlErrorResponse, previewErrorResponse } from './route-errors.js';
|
|
10
|
+
export function createStorystickPreviewTokenHandler(options = {}) {
|
|
11
|
+
return async function POST(request) {
|
|
12
|
+
const sessionId = request.nextUrl.searchParams.get(PREVIEW_SESSION_QUERY_PARAM);
|
|
13
|
+
if (!isSafePreviewSessionId(sessionId))
|
|
14
|
+
return previewErrorResponse(400, 'PREVIEW_SESSION_MISSING', 'Preview session selector is required.');
|
|
15
|
+
const requestOrigin = resolvePreviewRequestOrigin(request);
|
|
16
|
+
if (request.headers.get('origin') !== requestOrigin)
|
|
17
|
+
return previewErrorResponse(403, 'PREVIEW_ORIGIN_MISMATCH', 'A same-origin Preview request is required.');
|
|
18
|
+
try {
|
|
19
|
+
const transport = await getPreviewTransport();
|
|
20
|
+
const reader = await cookies();
|
|
21
|
+
const scope = { ...transport, requestOrigin };
|
|
22
|
+
const now = Date.now();
|
|
23
|
+
const fields = await resolvePreviewGrantAccess(reader, { ...scope, applicationBuildId: resolveStorystickApplicationBuildId(), now });
|
|
24
|
+
if (fields === null)
|
|
25
|
+
return previewErrorResponse(401, 'PREVIEW_CONTEXT_ENDED', 'Preview has been closed.');
|
|
26
|
+
if (!fields)
|
|
27
|
+
return previewErrorResponse(401, 'PREVIEW_GRANT_MISSING', 'Open Preview from Studio again.');
|
|
28
|
+
if (process.env.NODE_ENV === 'production' && !process.env.STORYSTICK_LANE_ID && !(await draftMode()).isEnabled) {
|
|
29
|
+
return previewErrorResponse(409, 'CONTENT_SHAPE_RELOAD_REQUIRED', 'Reopen Preview from Studio after the site deployment changed.');
|
|
30
|
+
}
|
|
31
|
+
const client = await createPreviewControlClient(fields.token);
|
|
32
|
+
await client.context({ sessionId });
|
|
33
|
+
const remainingMs = Math.max(0, fields.tokenExpiresAt - now);
|
|
34
|
+
const response = NextResponse.json({ token: fields.token, expiresAt: fields.tokenExpiresAt,
|
|
35
|
+
refreshAfter: now + Math.max(1000, Math.floor(remainingMs * 0.8)), sessionId, projectId: fields.projectId }, { headers: { 'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff' } });
|
|
36
|
+
// Safe even if this response arrives after a newer activation: all deletions
|
|
37
|
+
// have lower sequence AND were observed in this particular request.
|
|
38
|
+
prunePreviewAdmissionCookies({ response, reader, scope, requestUrl: requestOrigin + request.nextUrl.pathname,
|
|
39
|
+
basePath: options.basePath ?? '' }, fields.admissionSequence);
|
|
40
|
+
return response;
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
return previewControlErrorResponse(error);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { unstable_createPreviewReadIdentityTransport as createPreviewReadIdentityTransport, } from '@storystick/preview/server/read-identity';
|
|
2
|
+
export { unstable_resolveStorystickPreviewProvenanceConfig as resolvePreviewProvenanceConfig, unstable_resolveStorystickPreviewProvenanceManifest as resolvePreviewProvenanceManifest, } from './provenance-config.js';
|
|
3
|
+
export { unstable_attachPreviewRouteMetadataContext as attachPreviewRouteMetadataContext, unstable_attachPreviewRouteRenderContext as attachPreviewRouteRenderContext, unstable_buildPreviewRouteDocumentFrame as buildPreviewRouteDocumentFrame, unstable_collectPreviewRouteDocumentSliceTypes as collectPreviewRouteDocumentSliceTypes, } from './route-document-provenance.js';
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { unstable_createPreviewReadIdentityTransport as createPreviewReadIdentityTransport, } from '@storystick/preview/server/read-identity';
|
|
2
|
+
export { unstable_resolveStorystickPreviewProvenanceConfig as resolvePreviewProvenanceConfig, unstable_resolveStorystickPreviewProvenanceManifest as resolvePreviewProvenanceManifest, } from './provenance-config.js';
|
|
3
|
+
export { unstable_attachPreviewRouteMetadataContext as attachPreviewRouteMetadataContext, unstable_attachPreviewRouteRenderContext as attachPreviewRouteRenderContext, unstable_buildPreviewRouteDocumentFrame as buildPreviewRouteDocumentFrame, unstable_collectPreviewRouteDocumentSliceTypes as collectPreviewRouteDocumentSliceTypes, } from './route-document-provenance.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import 'server-only';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
import type { PreviewSliceScopeInvocation } from '@storystick/react/internal/preview-provenance';
|
|
4
|
+
export declare function readNativeSliceLocale(): Promise<string | undefined>;
|
|
5
|
+
export declare function runNativeSlice(input: PreviewSliceScopeInvocation): Promise<ReactNode>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import 'server-only';
|
|
2
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
3
|
+
import { readContentOrigin } from '@storystick/client/internal';
|
|
4
|
+
import { readDocumentLocaleRouting } from '@storystick/client/locale-routing';
|
|
5
|
+
// Framework adapter only: an async slice inherits its explicit document locale.
|
|
6
|
+
// No query collection or render boundary is attached to the component.
|
|
7
|
+
const localeScope = new AsyncLocalStorage();
|
|
8
|
+
export async function readNativeSliceLocale() {
|
|
9
|
+
return localeScope.getStore();
|
|
10
|
+
}
|
|
11
|
+
export async function runNativeSlice(input) {
|
|
12
|
+
const origin = readContentOrigin(input.props.slice);
|
|
13
|
+
const locale = readDocumentLocaleRouting(origin?._localeRouting)?.locale;
|
|
14
|
+
return localeScope.run(locale, () => input.component(input.props));
|
|
15
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface PreviewErrorSurfaceProps {
|
|
2
|
+
code?: string | null;
|
|
3
|
+
studioUrl?: string | null;
|
|
4
|
+
}
|
|
5
|
+
/** A terminal Preview state owns its viewport, without changing customer DOM.
|
|
6
|
+
* Isolate product styling from the customer's page and its error boundary. */
|
|
7
|
+
export declare function PreviewErrorSurface({ code, studioUrl }: PreviewErrorSurfaceProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { createPortal } from 'react-dom';
|
|
5
|
+
import { lookupSdkCopy } from '@storystick/preview/sdk-error-copy';
|
|
6
|
+
import { PreviewPageState } from '@storystick/react/internal/preview-page-state';
|
|
7
|
+
/** A terminal Preview state owns its viewport, without changing customer DOM.
|
|
8
|
+
* Isolate product styling from the customer's page and its error boundary. */
|
|
9
|
+
export function PreviewErrorSurface({ code, studioUrl }) {
|
|
10
|
+
const copy = lookupSdkCopy(code);
|
|
11
|
+
const host = useRef(null);
|
|
12
|
+
const [root, setRoot] = useState(null);
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (host.current)
|
|
15
|
+
setRoot(host.current.shadowRoot ?? host.current.attachShadow({ mode: 'open' }));
|
|
16
|
+
}, []);
|
|
17
|
+
return _jsx("div", { ref: host, "data-storystick-preview-error": "", "data-storystick-preview-controls": "", style: {
|
|
18
|
+
all: 'initial', position: 'fixed', inset: 0, zIndex: 2147483646, background: '#fff',
|
|
19
|
+
}, children: root ? createPortal(_jsx(PreviewPageState, { title: copy.title, description: copy.body, role: "alert", actions: studioUrl ? _jsx("a", { "data-ss-preview-page-action": "", href: studioUrl, children: "Return to Studio" }) : null, children: code || copy.commonCauses.length ? _jsxs("details", { children: [_jsx("summary", { children: "Technical details" }), code ? _jsx("pre", { children: code }) : null, copy.commonCauses.length ? _jsx("ul", { style: { textAlign: 'left', paddingLeft: 18 }, children: copy.commonCauses.map(cause => _jsx("li", { children: cause }, cause)) }) : null] }) : null }), root) : null });
|
|
20
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type PreviewReadContext } from '@storystick/types/preview-query-targeting';
|
|
2
|
+
type DisablePreviewResult = 'disabled' | 'conflict';
|
|
3
|
+
/** Shared same-origin exit contract for user controls and observed context end. */
|
|
4
|
+
export declare function disablePreview(url: string, context: PreviewReadContext | null, contextUrl?: string): Promise<DisablePreviewResult>;
|
|
5
|
+
export declare function hasPendingPreviewExit(url: string, context: PreviewReadContext | null): boolean;
|
|
6
|
+
export declare function publicPreviewLocation(href: string): string;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { readStandalonePreviewSessionId } from '@storystick/preview/identity/browser-context';
|
|
2
|
+
import { isPreviewReadContext } from '@storystick/types/preview-query-targeting';
|
|
3
|
+
const activeDisableRequests = new Map();
|
|
4
|
+
function previewExitKey(url, context, contextUrl) {
|
|
5
|
+
return JSON.stringify([url, context?.contextId ?? null, context?.revision ?? null, context ? null : contextUrl ?? null]);
|
|
6
|
+
}
|
|
7
|
+
/** Shared same-origin exit contract for user controls and observed context end. */
|
|
8
|
+
export function disablePreview(url, context, contextUrl) {
|
|
9
|
+
const key = previewExitKey(url, context, contextUrl);
|
|
10
|
+
const activeRequest = activeDisableRequests.get(key);
|
|
11
|
+
if (activeRequest)
|
|
12
|
+
return activeRequest;
|
|
13
|
+
const request = requestPreviewDisable(url, context, contextUrl);
|
|
14
|
+
activeDisableRequests.set(key, request);
|
|
15
|
+
void request.finally(() => {
|
|
16
|
+
if (activeDisableRequests.get(key) === request)
|
|
17
|
+
activeDisableRequests.delete(key);
|
|
18
|
+
}).catch(() => undefined);
|
|
19
|
+
return request;
|
|
20
|
+
}
|
|
21
|
+
export function hasPendingPreviewExit(url, context) {
|
|
22
|
+
return activeDisableRequests.has(previewExitKey(url, context));
|
|
23
|
+
}
|
|
24
|
+
async function requestPreviewDisable(url, context, contextUrl) {
|
|
25
|
+
if (!context && contextUrl) {
|
|
26
|
+
const sessionId = readStandalonePreviewSessionId();
|
|
27
|
+
const addressedUrl = new URL(contextUrl, window.location.origin);
|
|
28
|
+
if (sessionId)
|
|
29
|
+
addressedUrl.searchParams.set('_ss_session', sessionId);
|
|
30
|
+
const observed = await fetch(addressedUrl.pathname + addressedUrl.search, { credentials: 'same-origin', cache: 'no-store', signal: AbortSignal.timeout(10000) });
|
|
31
|
+
if (observed.ok) {
|
|
32
|
+
const result = await observed.json();
|
|
33
|
+
if (!result || typeof result !== 'object' || !('context' in result) || !isPreviewReadContext(result.context))
|
|
34
|
+
throw new Error('Preview context unavailable');
|
|
35
|
+
context = result.context;
|
|
36
|
+
}
|
|
37
|
+
else if (observed.status !== 401 && observed.status !== 403) {
|
|
38
|
+
const failure = await observed.json().catch(() => null);
|
|
39
|
+
// An expired view or changed build can be left locally. Never replace it
|
|
40
|
+
// with another window's context merely to issue an Exit command.
|
|
41
|
+
if (!failure || typeof failure !== 'object' || !('error' in failure) ||
|
|
42
|
+
!failure.error || typeof failure.error !== 'object' || !('code' in failure.error) ||
|
|
43
|
+
!['CONTENT_SHAPE_RELOAD_REQUIRED', 'PREVIEW_VIEW_UNAVAILABLE', 'PREVIEW_CONTEXT_INVALID'].includes(String(failure.error.code)))
|
|
44
|
+
throw new Error('Preview context unavailable');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const response = await fetch(url, {
|
|
48
|
+
method: 'POST', credentials: 'same-origin', cache: 'no-store', signal: AbortSignal.timeout(10000),
|
|
49
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
50
|
+
body: JSON.stringify(context ? {
|
|
51
|
+
commandId: `leave_${context.contextId}_${context.revision}`,
|
|
52
|
+
expectedContextId: context.contextId, expectedRevision: context.revision, perspective: null,
|
|
53
|
+
} : {}),
|
|
54
|
+
});
|
|
55
|
+
if (response.status === 409)
|
|
56
|
+
return 'conflict';
|
|
57
|
+
if (!response.ok)
|
|
58
|
+
throw new Error('Preview exit unavailable');
|
|
59
|
+
const result = await response.json();
|
|
60
|
+
if (!result || typeof result !== 'object' || !('status' in result) || result.status !== 'disabled') {
|
|
61
|
+
throw new Error('Preview exit unavailable');
|
|
62
|
+
}
|
|
63
|
+
return 'disabled';
|
|
64
|
+
}
|
|
65
|
+
export function publicPreviewLocation(href) {
|
|
66
|
+
const url = new URL(href);
|
|
67
|
+
for (const name of [...url.searchParams.keys()])
|
|
68
|
+
if (name.startsWith('_ss_'))
|
|
69
|
+
url.searchParams.delete(name);
|
|
70
|
+
return url.pathname + url.search + url.hash;
|
|
71
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Metadata } from 'next';
|
|
2
|
+
type EnvLike = Record<string, string | undefined>;
|
|
3
|
+
export interface StorystickPreviewRobotsOptions {
|
|
4
|
+
readonly enabled?: boolean;
|
|
5
|
+
readonly env?: EnvLike;
|
|
6
|
+
}
|
|
7
|
+
export declare const STORYSTICK_PREVIEW_ROBOTS: {
|
|
8
|
+
readonly index: false;
|
|
9
|
+
readonly follow: false;
|
|
10
|
+
readonly noarchive: true;
|
|
11
|
+
};
|
|
12
|
+
/** Internal legacy configuration used by the render adapter; not an application flag. */
|
|
13
|
+
export declare function isStorystickPreviewProvenanceEnabled(env?: EnvLike): boolean;
|
|
14
|
+
export declare function storystickPreviewRobots(options?: StorystickPreviewRobotsOptions): Metadata['robots'] | undefined;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const STORYSTICK_PREVIEW_ROBOTS = {
|
|
2
|
+
index: false, follow: false, noarchive: true,
|
|
3
|
+
};
|
|
4
|
+
/** Internal legacy configuration used by the render adapter; not an application flag. */
|
|
5
|
+
export function isStorystickPreviewProvenanceEnabled(env = defaultEnv()) {
|
|
6
|
+
return env.STORYSTICK_PREVIEW_PROVENANCE === '1';
|
|
7
|
+
}
|
|
8
|
+
export function storystickPreviewRobots(options = {}) {
|
|
9
|
+
return (options.enabled ?? isStorystickPreviewProvenanceEnabled(options.env)) ? { ...STORYSTICK_PREVIEW_ROBOTS } : undefined;
|
|
10
|
+
}
|
|
11
|
+
function defaultEnv() {
|
|
12
|
+
return typeof process === 'undefined' ? {} : process.env;
|
|
13
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preview identity latch (ADR-113 D2 + follow-up).
|
|
3
|
+
*
|
|
4
|
+
* Both customer SDK hooks (`useStorystickPreviewNavigation` and
|
|
5
|
+
* `useStorystickPreviewLocation`) plus the forward-path session
|
|
6
|
+
* preservation in `preview-nav.ts` need a stable answer to two
|
|
7
|
+
* questions:
|
|
8
|
+
*
|
|
9
|
+
* 1. *"Is this page running inside a Storystick preview iframe?"*
|
|
10
|
+
* (boolean — gates whether to register handlers)
|
|
11
|
+
* 2. *"What is the active `_ss_session` identifier?"*
|
|
12
|
+
* (string — appended to Studio-dispatched destinations so
|
|
13
|
+
* subsequent App Router navigates stay inside the preview
|
|
14
|
+
* session)
|
|
15
|
+
*
|
|
16
|
+
* The original heuristic — re-derive from `window.location.search` on
|
|
17
|
+
* every render — breaks the moment the customer's own intra-iframe
|
|
18
|
+
* nav drops `_ss_session` from the URL: the gate flips to false (PR5
|
|
19
|
+
* fix), and the destination injection loses the marker (this fix).
|
|
20
|
+
*
|
|
21
|
+
* t0 boot: /?_ss_session=X → active=true, sessionId=X
|
|
22
|
+
* t1 nav: /pricing → active=true, sessionId=X (cached)
|
|
23
|
+
* t2 onward: /pricing → active=true, sessionId=X (cached)
|
|
24
|
+
*
|
|
25
|
+
* The latch lives on `globalThis` (not module scope) so it survives
|
|
26
|
+
* HMR: Next/Fast Refresh re-evaluates modules but never replaces the
|
|
27
|
+
* `globalThis` object, so a module-scope `let` would be lost on HMR
|
|
28
|
+
* while a `globalThis.__storystick_preview_mode_latched__` write is
|
|
29
|
+
* preserved by construction.
|
|
30
|
+
*
|
|
31
|
+
* **Backward-compatible shape**: `sessionId` is OPTIONAL on the
|
|
32
|
+
* stored object. An older `@storystick/next` version that wrote just
|
|
33
|
+
* `{ active }` (pre-follow-up) is read by the new code as
|
|
34
|
+
* `existing.sessionId === undefined` — the new code then runs
|
|
35
|
+
* opportunistic backfill: if the URL currently has `_ss_session`, it
|
|
36
|
+
* upgrades the in-place latch object so subsequent reads pick up the
|
|
37
|
+
* id. Symmetric direction (new writer / old reader) is trivially
|
|
38
|
+
* safe — the old reader only ever inspected `.active`.
|
|
39
|
+
*
|
|
40
|
+
* **Why not bump the namespace key**: two `@storystick/next` versions
|
|
41
|
+
* in the same process MUST share the active-preview decision (the
|
|
42
|
+
* gate). Bumping the key would partition the gate too, reintroducing
|
|
43
|
+
* the pre-PR5 bug for the older copy. Codex R4 follow-up consult
|
|
44
|
+
* 2026-05-14: keep namespace, extend shape.
|
|
45
|
+
*
|
|
46
|
+
* **Tri-state via object presence**: a raw boolean cannot distinguish
|
|
47
|
+
* "latched and false" from "not latched yet". The `PreviewLatch`
|
|
48
|
+
* object is the sentinel: `existing !== undefined` means latched
|
|
49
|
+
* (active or not); the boolean inside carries the decision.
|
|
50
|
+
*
|
|
51
|
+
* **Namespace stability**: `__storystick_preview_mode_latched__` is
|
|
52
|
+
* the single, underscore-prefixed key. The literal-string grep test
|
|
53
|
+
* in `preview-mode-latch.test.ts` pins it; do not rename even if
|
|
54
|
+
* "mode" is now an under-description (the latch carries identity too).
|
|
55
|
+
*/
|
|
56
|
+
interface PreviewLatch {
|
|
57
|
+
active: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Captured `_ss_session` value from the URL at boot. `null` when
|
|
60
|
+
* the page is not in preview mode (top-level) or when the latch
|
|
61
|
+
* was written by an old SDK version that did not yet track the
|
|
62
|
+
* session id. New SDK code opportunistically backfills this field
|
|
63
|
+
* via `isPreviewModeLatched()` when the URL still carries the
|
|
64
|
+
* param — see "Backward-compatible shape" above.
|
|
65
|
+
*/
|
|
66
|
+
sessionId?: string | null;
|
|
67
|
+
/**
|
|
68
|
+
* Captured `_ss_variant` URL override for this iframe. This is not
|
|
69
|
+
* preview identity; it is a per-iframe read-scope override.
|
|
70
|
+
*/
|
|
71
|
+
variantId?: string | null;
|
|
72
|
+
/** Captured `_ss_release` per-iframe read-scope override. */
|
|
73
|
+
releaseId?: string | null;
|
|
74
|
+
}
|
|
75
|
+
declare global {
|
|
76
|
+
var __storystick_preview_mode_latched__: PreviewLatch | undefined;
|
|
77
|
+
}
|
|
78
|
+
import { type PreviewScope } from '@storystick/preview/identity';
|
|
79
|
+
/**
|
|
80
|
+
* Return whether the current page is running inside a Storystick
|
|
81
|
+
* preview iframe. Latches on first valid call: subsequent calls return
|
|
82
|
+
* the cached value even after the URL drops `_ss_session` (customer
|
|
83
|
+
* intra-iframe nav).
|
|
84
|
+
*
|
|
85
|
+
* Also performs opportunistic sessionId backfill: if the existing
|
|
86
|
+
* latch is missing `sessionId` (either old SDK shape or first mount
|
|
87
|
+
* happened mid-customer-nav) AND the URL currently carries
|
|
88
|
+
* `_ss_session`, the value is written into the latch in place so
|
|
89
|
+
* `getLatchedPreviewSessionId()` returns it on subsequent calls.
|
|
90
|
+
*
|
|
91
|
+
* SSR / no-`window` calls return false without latching — they cannot
|
|
92
|
+
* know yet, and we must not freeze the wrong answer for the eventual
|
|
93
|
+
* client hydration.
|
|
94
|
+
*/
|
|
95
|
+
export declare function isPreviewModeLatched(): boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Return the latched `_ss_session` identifier. `null` when the latch
|
|
98
|
+
* has not run, the page is not in preview mode, or the latch was
|
|
99
|
+
* captured before the param was available (an old SDK version, or a
|
|
100
|
+
* first read that happened mid-customer-nav).
|
|
101
|
+
*
|
|
102
|
+
* Callers in the SDK forward path (`preservePreviewSession`) use this
|
|
103
|
+
* as the primary source over `window.location.search`: the latch is
|
|
104
|
+
* Studio-authoritative (set at boot from the URL Studio constructed),
|
|
105
|
+
* while the live URL is customer-mutable surface that intra-iframe
|
|
106
|
+
* nav routinely strips.
|
|
107
|
+
*
|
|
108
|
+
* The caller is responsible for invoking `isPreviewModeLatched()`
|
|
109
|
+
* first (or otherwise gating on preview mode) before consuming this
|
|
110
|
+
* value — outside preview the latch is never written and this
|
|
111
|
+
* returns `null` unconditionally.
|
|
112
|
+
*/
|
|
113
|
+
export declare function getLatchedPreviewSessionId(): string | null;
|
|
114
|
+
export declare function readPreviewSessionIdForNavigation(): string | null;
|
|
115
|
+
export declare function readPreviewVariantIdForNavigation(): string | null;
|
|
116
|
+
export declare function readPreviewReleaseIdForNavigation(): string | null;
|
|
117
|
+
/**
|
|
118
|
+
* Replace the latched request scope after a trusted Studio navigation.
|
|
119
|
+
* Living scope deliberately clears both selectors so a later customer-side
|
|
120
|
+
* navigation cannot resurrect an older variant or release from the boot URL.
|
|
121
|
+
*/
|
|
122
|
+
export declare function setLatchedPreviewScope(scope: PreviewScope): void;
|
|
123
|
+
/**
|
|
124
|
+
* Test-only reset. Vitest's module isolation re-imports modules between
|
|
125
|
+
* tests but `globalThis` is shared across the suite, so a latch set in
|
|
126
|
+
* test A would leak into test B. Tests call this in `beforeEach` to
|
|
127
|
+
* start each case with a fresh latch.
|
|
128
|
+
*
|
|
129
|
+
* Production code MUST NOT call this — clearing the latch reintroduces
|
|
130
|
+
* the very bug it exists to prevent.
|
|
131
|
+
*/
|
|
132
|
+
export declare function _resetPreviewModeLatchForTests(): void;
|
|
133
|
+
export {};
|