@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.
Files changed (121) hide show
  1. package/LICENSE +112 -0
  2. package/PUBLIC-API.md +31 -0
  3. package/README.md +522 -2
  4. package/dist/.source-hash +1 -0
  5. package/dist/cache-tags.d.ts +2 -0
  6. package/dist/cache-tags.js +12 -0
  7. package/dist/clean.d.ts +1 -0
  8. package/dist/clean.js +1 -0
  9. package/dist/code-version.d.ts +29 -0
  10. package/dist/code-version.js +48 -0
  11. package/dist/file.d.ts +1 -0
  12. package/dist/file.js +1 -0
  13. package/dist/i18n-seo.d.ts +1 -0
  14. package/dist/i18n-seo.js +1 -0
  15. package/dist/image.d.ts +11 -0
  16. package/dist/image.js +22 -0
  17. package/dist/index.d.ts +2 -0
  18. package/dist/index.js +3 -0
  19. package/dist/index.react-server.d.ts +3 -0
  20. package/dist/index.react-server.js +12 -0
  21. package/dist/link.d.ts +10 -0
  22. package/dist/link.js +8 -0
  23. package/dist/preview/index.d.ts +29 -0
  24. package/dist/preview/index.js +24 -0
  25. package/dist/preview/internal.d.ts +61 -0
  26. package/dist/preview/internal.js +97 -0
  27. package/dist/preview/preview-request-identity.d.ts +4 -0
  28. package/dist/preview/preview-request-identity.js +63 -0
  29. package/dist/preview/provenance-config.d.ts +42 -0
  30. package/dist/preview/provenance-config.js +132 -0
  31. package/dist/preview/provenance-frame-builder.d.ts +6 -0
  32. package/dist/preview/provenance-frame-builder.js +10 -0
  33. package/dist/preview/provenance-response.d.ts +5 -0
  34. package/dist/preview/provenance-response.js +9 -0
  35. package/dist/preview/provenance-runtime-adapter.d.ts +9 -0
  36. package/dist/preview/provenance-runtime-adapter.js +42 -0
  37. package/dist/preview/render-frame-provenance.d.ts +23 -0
  38. package/dist/preview/render-frame-provenance.js +47 -0
  39. package/dist/preview/route-authoring.d.ts +3 -0
  40. package/dist/preview/route-authoring.js +80 -0
  41. package/dist/preview/route-context.d.ts +3 -0
  42. package/dist/preview/route-context.js +38 -0
  43. package/dist/preview/route-disable.d.ts +7 -0
  44. package/dist/preview/route-disable.js +63 -0
  45. package/dist/preview/route-document-provenance.d.ts +23 -0
  46. package/dist/preview/route-document-provenance.js +46 -0
  47. package/dist/preview/route-errors.d.ts +1 -0
  48. package/dist/preview/route-errors.js +1 -0
  49. package/dist/preview/route-handler.d.ts +36 -0
  50. package/dist/preview/route-handler.js +139 -0
  51. package/dist/preview/route-initial.d.ts +9 -0
  52. package/dist/preview/route-initial.js +156 -0
  53. package/dist/preview/route-provenance-runtime.d.ts +34 -0
  54. package/dist/preview/route-provenance-runtime.js +90 -0
  55. package/dist/preview/route-token.d.ts +6 -0
  56. package/dist/preview/route-token.js +46 -0
  57. package/dist/preview/server-preview-internals.d.ts +3 -0
  58. package/dist/preview/server-preview-internals.js +3 -0
  59. package/dist/preview/slice-locale.d.ts +5 -0
  60. package/dist/preview/slice-locale.js +15 -0
  61. package/dist/preview-error-surface.d.ts +7 -0
  62. package/dist/preview-error-surface.js +20 -0
  63. package/dist/preview-exit.d.ts +7 -0
  64. package/dist/preview-exit.js +71 -0
  65. package/dist/preview-metadata.d.ts +15 -0
  66. package/dist/preview-metadata.js +13 -0
  67. package/dist/preview-mode-latch.d.ts +133 -0
  68. package/dist/preview-mode-latch.js +231 -0
  69. package/dist/preview-nav-location.d.ts +12 -0
  70. package/dist/preview-nav-location.js +149 -0
  71. package/dist/preview-nav.d.ts +94 -0
  72. package/dist/preview-nav.js +523 -0
  73. package/dist/preview-refresh-coordinator.d.ts +26 -0
  74. package/dist/preview-refresh-coordinator.js +93 -0
  75. package/dist/preview-render-error.d.ts +11 -0
  76. package/dist/preview-render-error.js +36 -0
  77. package/dist/preview-render-recovery.d.ts +9 -0
  78. package/dist/preview-render-recovery.js +42 -0
  79. package/dist/preview-session-recovery.d.ts +1 -0
  80. package/dist/preview-session-recovery.js +1 -0
  81. package/dist/preview-standalone-host.d.ts +3 -0
  82. package/dist/preview-standalone-host.js +55 -0
  83. package/dist/preview-text.d.ts +2 -0
  84. package/dist/preview-text.js +1 -0
  85. package/dist/preview-view-bootstrap.d.ts +5 -0
  86. package/dist/preview-view-bootstrap.js +13 -0
  87. package/dist/public-delivery.d.ts +6 -0
  88. package/dist/public-delivery.js +7 -0
  89. package/dist/query-fetch-options.d.ts +7 -0
  90. package/dist/query-fetch-options.js +193 -0
  91. package/dist/query.d.ts +2 -0
  92. package/dist/query.js +1 -0
  93. package/dist/rendering.d.ts +33 -0
  94. package/dist/rendering.js +42 -0
  95. package/dist/revalidate.d.ts +12 -0
  96. package/dist/revalidate.js +33 -0
  97. package/dist/rich-text.d.ts +2 -0
  98. package/dist/rich-text.js +1 -0
  99. package/dist/server-runtime.d.ts +208 -0
  100. package/dist/server-runtime.js +1584 -0
  101. package/dist/server.d.ts +8 -0
  102. package/dist/server.js +6 -0
  103. package/dist/slice-zone.d.ts +2 -0
  104. package/dist/slice-zone.js +1 -0
  105. package/dist/storystick-component-types.d.ts +26 -0
  106. package/dist/storystick-component-types.js +1 -0
  107. package/dist/storystick-component.d.ts +10 -0
  108. package/dist/storystick-component.js +45 -0
  109. package/dist/storystick-error-recovery.d.ts +12 -0
  110. package/dist/storystick-error-recovery.js +11 -0
  111. package/dist/storystick-preview-runtime.d.ts +30 -0
  112. package/dist/storystick-preview-runtime.js +561 -0
  113. package/dist/storystick.d.ts +24 -0
  114. package/dist/storystick.js +23 -0
  115. package/dist/testing.d.ts +9 -0
  116. package/dist/testing.js +40 -0
  117. package/dist/video.d.ts +1 -0
  118. package/dist/video.js +1 -0
  119. package/package.json +128 -3
  120. package/recipes/README.md +91 -0
  121. 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 {};