@storystick/next 0.0.0-stage → 0.2.0-rc.0

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 +141 -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 +24 -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,12 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { normalizePathname } from '@storystick/client/normalize-pathname';
3
+ /** Internal tags share one bounded representation at read and invalidation. */
4
+ export function storystickCacheTag(tag) {
5
+ const prefix = 'storystick:pathname:';
6
+ const canonical = tag.startsWith(prefix)
7
+ ? prefix + normalizePathname(tag.slice(prefix.length))
8
+ : tag;
9
+ return canonical.length <= 256
10
+ ? canonical
11
+ : `storystick:sha256:${createHash('sha256').update(canonical).digest('hex')}`;
12
+ }
@@ -0,0 +1 @@
1
+ export { stripPreviewProvenance } from '@storystick/types/preview-provenance';
package/dist/clean.js ADDED
@@ -0,0 +1 @@
1
+ export { stripPreviewProvenance } from '@storystick/types/preview-provenance';
@@ -0,0 +1,29 @@
1
+ type EnvLike = Record<string, string | undefined>;
2
+ export declare const STORYSTICK_UNKNOWN_CODE_VERSION = "dev-unknown";
3
+ /** Opt-in, deployment-specific identity used by schema-generation pairing. */
4
+ export declare function resolveStorystickApplicationBuildId(input?: {
5
+ readonly env?: EnvLike;
6
+ }): string | undefined;
7
+ /** Build-system supplied SHA-256 of the deployed application artifact. */
8
+ export declare function resolveStorystickApplicationBuildDigest(input?: {
9
+ readonly env?: EnvLike;
10
+ }): string | undefined;
11
+ /**
12
+ * Browser-visible deploy signature. This is the fallback path for apps
13
+ * that mount the client component directly.
14
+ */
15
+ export declare function resolveStorystickBrowserCodeVersion(input?: {
16
+ readonly codeVersion?: string;
17
+ readonly env?: EnvLike;
18
+ }): string;
19
+ /**
20
+ * Server-visible deploy signature. The root Next export uses this from
21
+ * Server Components and passes the resolved value into the client runtime,
22
+ * keeping Preview snapshots and RSC sidecars on one codeVersion without
23
+ * requiring user-written props.
24
+ */
25
+ export declare function resolveStorystickServerCodeVersion(input?: {
26
+ readonly codeVersion?: string;
27
+ readonly env?: EnvLike;
28
+ }): string;
29
+ export {};
@@ -0,0 +1,48 @@
1
+ import { isContentShapeBuildId } from '@storystick/types';
2
+ export const STORYSTICK_UNKNOWN_CODE_VERSION = 'dev-unknown';
3
+ /** Opt-in, deployment-specific identity used by schema-generation pairing. */
4
+ export function resolveStorystickApplicationBuildId(input = {}) {
5
+ const value = present((input.env ?? process.env).STORYSTICK_APP_BUILD_ID);
6
+ if (value !== undefined && !isContentShapeBuildId(value)) {
7
+ throw new TypeError('STORYSTICK_APP_BUILD_ID must be an app:-prefixed URL-safe identifier of at most 255 characters.');
8
+ }
9
+ return value;
10
+ }
11
+ /** Build-system supplied SHA-256 of the deployed application artifact. */
12
+ export function resolveStorystickApplicationBuildDigest(input = {}) {
13
+ const value = present((input.env ?? process.env).STORYSTICK_APP_BUILD_DIGEST);
14
+ if (value !== undefined && !/^[a-f0-9]{64}$/.test(value)) {
15
+ throw new TypeError('STORYSTICK_APP_BUILD_DIGEST must be a lowercase SHA-256 hex digest.');
16
+ }
17
+ return value;
18
+ }
19
+ function present(value) {
20
+ return value && value.length > 0 ? value : undefined;
21
+ }
22
+ /**
23
+ * Browser-visible deploy signature. This is the fallback path for apps
24
+ * that mount the client component directly.
25
+ */
26
+ export function resolveStorystickBrowserCodeVersion(input = {}) {
27
+ const env = input.env ?? process.env;
28
+ return (present(input.codeVersion) ??
29
+ present(env.NEXT_PUBLIC_STORYSTICK_CODE_VERSION) ??
30
+ present(env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA) ??
31
+ STORYSTICK_UNKNOWN_CODE_VERSION);
32
+ }
33
+ /**
34
+ * Server-visible deploy signature. The root Next export uses this from
35
+ * Server Components and passes the resolved value into the client runtime,
36
+ * keeping Preview snapshots and RSC sidecars on one codeVersion without
37
+ * requiring user-written props.
38
+ */
39
+ export function resolveStorystickServerCodeVersion(input = {}) {
40
+ const env = input.env ?? process.env;
41
+ return (present(input.codeVersion) ??
42
+ present(env.STORYSTICK_CODE_VERSION) ??
43
+ present(env.NEXT_BUILD_ID) ??
44
+ present(env.VERCEL_GIT_COMMIT_SHA) ??
45
+ present(env.NEXT_PUBLIC_STORYSTICK_CODE_VERSION) ??
46
+ present(env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA) ??
47
+ STORYSTICK_UNKNOWN_CODE_VERSION);
48
+ }
package/dist/file.d.ts ADDED
@@ -0,0 +1 @@
1
+ export { StorystickFile, resolveFile, type FileStatus, type ResolveFileOptions, type ResolvedStorystickFile, type StorystickFileComponentProps, type StorystickFileFailure, type StorystickFileFallback, type StorystickFileField, type StorystickFileProps, } from '@storystick/react/file';
package/dist/file.js ADDED
@@ -0,0 +1 @@
1
+ export { StorystickFile, resolveFile, } from '@storystick/react/file';
@@ -0,0 +1 @@
1
+ export * from '@storystick/client/seo';
@@ -0,0 +1 @@
1
+ export * from '@storystick/client/seo';
@@ -0,0 +1,11 @@
1
+ import { resolveImage } from '@storystick/react/image';
2
+ import type { ResolveImageOptions, StorystickImageField, StorystickImageProps } from '@storystick/media';
3
+ import type { ImageProps as NextImageProps } from 'next/image';
4
+ export { resolveImage };
5
+ /** The native renderer is framework-neutral; Next adds only Next Image props. */
6
+ export { StorystickImage, type StorystickImageComponentProps, type StorystickImageFailure, type StorystickImageFallback, } from '@storystick/react/image';
7
+ export type GetNextImagePropsOptions = ResolveImageOptions & {
8
+ unoptimized?: boolean;
9
+ };
10
+ export declare function getNextImageProps(image: StorystickImageField | null | undefined, options?: GetNextImagePropsOptions): (Pick<NextImageProps, 'src' | 'alt' | 'width' | 'height' | 'sizes' | 'unoptimized' | 'blurDataURL' | 'placeholder'> & Pick<StorystickImageProps, 'data-ss-e'>) | null;
11
+ export type { ResolvedStorystickImage, StorystickAsset, StorystickAssetSidecar, StorystickImageField, } from '@storystick/media';
package/dist/image.js ADDED
@@ -0,0 +1,22 @@
1
+ import { resolveImage } from '@storystick/react/image';
2
+ export { resolveImage };
3
+ /** The native renderer is framework-neutral; Next adds only Next Image props. */
4
+ export { StorystickImage, } from '@storystick/react/image';
5
+ export function getNextImageProps(image, options = {}) {
6
+ const resolved = resolveImage(image, options);
7
+ if (resolved.status !== 'ok')
8
+ return null;
9
+ const props = resolved.props;
10
+ return {
11
+ src: props.src,
12
+ alt: props.alt,
13
+ width: props.width,
14
+ height: props.height,
15
+ ...(props.sizes ? { sizes: props.sizes } : {}),
16
+ ...(resolved.blurDataURL
17
+ ? { blurDataURL: resolved.blurDataURL, placeholder: 'blur' }
18
+ : {}),
19
+ ...('data-ss-e' in props ? { 'data-ss-e': props['data-ss-e'] } : {}),
20
+ unoptimized: options.unoptimized ?? true,
21
+ };
22
+ }
@@ -0,0 +1,2 @@
1
+ export * from './rendering.js';
2
+ export { useQuery } from './query.js';
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ // The server entry shares these primitives without re-exporting the client hook.
2
+ export * from './rendering.js';
3
+ export { useQuery } from './query.js';
@@ -0,0 +1,3 @@
1
+ export * from './rendering.js';
2
+ import type { StorystickProps } from './storystick.js';
3
+ export declare function Storystick(props?: StorystickProps): Promise<import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>>;
@@ -0,0 +1,12 @@
1
+ // Server-only assembly of the existing mount. Client imports keep the same
2
+ // entry and never pull next/headers or credentials into their bundle.
3
+ export * from './rendering.js';
4
+ import { renderStorystick } from './storystick.js';
5
+ import { readPreviewServerRenderReceipt } from './server-runtime.js';
6
+ export async function Storystick(props = {}) {
7
+ const { projectId, endpoint, gatewaySyncUrl, publishableKey, basePath, codeVersion } = props;
8
+ return renderStorystick({
9
+ projectId, endpoint, gatewaySyncUrl, publishableKey, basePath, codeVersion,
10
+ previewServerRenderReceipt: await readPreviewServerRenderReceipt(projectId),
11
+ });
12
+ }
package/dist/link.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import * as React from 'react';
2
+ import NextLink from 'next/link';
3
+ import type { StorystickLinkProps as ReactStorystickLinkProps } from '@storystick/react/link';
4
+ export type StorystickLinkProps = ReactStorystickLinkProps & {
5
+ readonly prefetch?: React.ComponentProps<typeof NextLink>['prefetch'];
6
+ };
7
+ /** Next router adapter for the framework-neutral Storystick link. */
8
+ export declare function StorystickLink({ prefetch, ...props }: StorystickLinkProps): React.ReactElement;
9
+ export { resolveLink } from '@storystick/react/link';
10
+ export type { StorystickLinkDocument, StorystickLinkOptions, StorystickLinkValue, } from '@storystick/react/link';
package/dist/link.js ADDED
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import NextLink from 'next/link';
3
+ import { StorystickLinkAdapter, } from '@storystick/react/internal/link-adapter';
4
+ /** Next router adapter for the framework-neutral Storystick link. */
5
+ export function StorystickLink({ prefetch, ...props }) {
6
+ return (_jsx(StorystickLinkAdapter, { ...props, internalComponent: NextLink, internalComponentProps: { prefetch } }));
7
+ }
8
+ export { resolveLink } from '@storystick/react/link';
@@ -0,0 +1,29 @@
1
+ /**
2
+ * `@storystick/next/preview` — server-only PreviewGrant route adapters.
3
+ *
4
+ * Mounted by customers in their App Router as one catch-all route:
5
+ *
6
+ * // app/api/storystick/[...action]/route.ts
7
+ * import { createRouteHandler } from '@storystick/next/preview'
8
+ * const storystick = createRouteHandler()
9
+ * export const GET = storystick.GET
10
+ * export const POST = storystick.POST
11
+ *
12
+ * Server-only by design: the root `@storystick/next` entry MUST NOT re-export
13
+ * from here. These handlers import `next/headers::cookies` + `draftMode` and
14
+ * server-side grant exchange machinery; pulling them into the root entry would drag
15
+ * server-only code into Server Components that import `createServerClient` /
16
+ * `fetchDocument` / `fetchQuery`.
17
+ */
18
+ import 'server-only';
19
+ export { createRouteHandler } from './route-handler.js';
20
+ export { createStorystickPreviewHandler } from './route-initial.js';
21
+ export { createStorystickPreviewTokenHandler } from './route-token.js';
22
+ export { createStorystickPreviewDisableHandler } from './route-disable.js';
23
+ export { createStorystickPreviewAuthoringHandler } from './route-authoring.js';
24
+ export { createStorystickPreviewProvenanceRuntimeHandler } from './route-provenance-runtime.js';
25
+ export type { RouteHandlerOptions, RouteHandlers, } from './route-handler.js';
26
+ export type { PreviewHandlerOptions } from './route-initial.js';
27
+ export type { PreviewTokenHandlerOptions } from './route-token.js';
28
+ export type { PreviewDisableHandlerOptions } from './route-disable.js';
29
+ export type { PreviewProvenanceRuntimeHandlerOptions } from './route-provenance-runtime.js';
@@ -0,0 +1,24 @@
1
+ /**
2
+ * `@storystick/next/preview` — server-only PreviewGrant route adapters.
3
+ *
4
+ * Mounted by customers in their App Router as one catch-all route:
5
+ *
6
+ * // app/api/storystick/[...action]/route.ts
7
+ * import { createRouteHandler } from '@storystick/next/preview'
8
+ * const storystick = createRouteHandler()
9
+ * export const GET = storystick.GET
10
+ * export const POST = storystick.POST
11
+ *
12
+ * Server-only by design: the root `@storystick/next` entry MUST NOT re-export
13
+ * from here. These handlers import `next/headers::cookies` + `draftMode` and
14
+ * server-side grant exchange machinery; pulling them into the root entry would drag
15
+ * server-only code into Server Components that import `createServerClient` /
16
+ * `fetchDocument` / `fetchQuery`.
17
+ */
18
+ import 'server-only';
19
+ export { createRouteHandler } from './route-handler.js';
20
+ export { createStorystickPreviewHandler } from './route-initial.js';
21
+ export { createStorystickPreviewTokenHandler } from './route-token.js';
22
+ export { createStorystickPreviewDisableHandler } from './route-disable.js';
23
+ export { createStorystickPreviewAuthoringHandler } from './route-authoring.js';
24
+ export { createStorystickPreviewProvenanceRuntimeHandler } from './route-provenance-runtime.js';
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Internal helpers shared by the Preview activation and token routes.
3
+ *
4
+ * Server-only. Not re-exported from `@storystick/next/preview`.
5
+ *
6
+ * ADR-131 C9 — endpoint resolution flows through `resolveInfra()` which
7
+ * shares its `resolveProject` cache with `server.ts` (same per-process
8
+ * Map in `@storystick/client/resolve.ts`). The `STORYSTICK_ENDPOINT` env
9
+ * escape hatch is preserved as an undocumented self-host override.
10
+ */
11
+ import type { NextRequest } from 'next/server';
12
+ export type PreviewTransport = {
13
+ endpoint: string;
14
+ projectId: string;
15
+ apiKey: string;
16
+ };
17
+ export declare function createPreviewControlClient(token: string): Promise<{
18
+ context: <T = unknown>(input?: {
19
+ sessionId?: string;
20
+ includeProjectionCheckpoint?: boolean;
21
+ includeRouting?: boolean;
22
+ }) => Promise<T>;
23
+ session: (input: {
24
+ sessionId: string;
25
+ mode?: "routing-only";
26
+ }) => Promise<unknown>;
27
+ draft: (input: {
28
+ sessionId: string;
29
+ draftDocumentId?: string;
30
+ locale?: string;
31
+ contextId: string;
32
+ revision: number;
33
+ fallbackPolicy?: "none" | "explicit";
34
+ }) => Promise<unknown>;
35
+ changeContext: (input: {
36
+ commandId: string;
37
+ expectedContextId: string;
38
+ expectedRevision: number;
39
+ perspective: import("@storystick/types/preview-query-targeting").PreviewPerspective | null;
40
+ }) => Promise<{
41
+ status: "applied" | "conflict";
42
+ }>;
43
+ }>;
44
+ export declare function getPreviewTransport(): Promise<PreviewTransport>;
45
+ /**
46
+ * Resolve the browser-visible origin for customer preview requests.
47
+ *
48
+ * Next's `nextUrl.origin` is not always browser-visible: under
49
+ * `next start --hostname 0.0.0.0`, it can become `http://0.0.0.0:*`
50
+ * even though the iframe requested `http://localhost:*`. Prefer Host,
51
+ * falling back to Next's origin only if Host is absent or malformed.
52
+ */
53
+ export declare function resolvePreviewRequestOrigin(request: NextRequest): string;
54
+ /** Reset the transport singleton (testing only). */
55
+ export declare function _resetPreviewTransport(): void;
56
+ /**
57
+ * Allow tests to inject a transport without going through env vars or
58
+ * `resolveProject`. The override is consumed by `getPreviewTransport`
59
+ * on next call. Returns a dispose function that restores prior state.
60
+ */
61
+ export declare function _setPreviewTransport(t: PreviewTransport | null): void;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Internal helpers shared by the Preview activation and token routes.
3
+ *
4
+ * Server-only. Not re-exported from `@storystick/next/preview`.
5
+ *
6
+ * ADR-131 C9 — endpoint resolution flows through `resolveInfra()` which
7
+ * shares its `resolveProject` cache with `server.ts` (same per-process
8
+ * Map in `@storystick/client/resolve.ts`). The `STORYSTICK_ENDPOINT` env
9
+ * escape hatch is preserved as an undocumented self-host override.
10
+ */
11
+ import { resolveInfra } from '@storystick/client/adapter';
12
+ import { previewControlClient } from '@storystick/preview/server/control-client';
13
+ let _previewTransport = null;
14
+ export async function createPreviewControlClient(token) {
15
+ const transport = await getPreviewTransport();
16
+ return previewControlClient(transport.endpoint, token);
17
+ }
18
+ export async function getPreviewTransport() {
19
+ if (_previewTransport)
20
+ return _previewTransport;
21
+ const apiKey = process.env.STORYSTICK_API_KEY;
22
+ if (!apiKey) {
23
+ throw new Error('StorystickError: No STORYSTICK_API_KEY found.\n' +
24
+ 'Add it to your .env.local (ss_sk_live_* for server-side reads):\n\n' +
25
+ ' STORYSTICK_API_KEY=ss_sk_live_...\n');
26
+ }
27
+ const projectId = process.env.STORYSTICK_PROJECT_ID;
28
+ if (!projectId) {
29
+ throw new Error('StorystickError: No STORYSTICK_PROJECT_ID found.\n' +
30
+ 'Add it to your .env.local — it is the 8-char shortId visible in your\n' +
31
+ 'Hub project URL at /p/<label>-<shortId>:\n\n' +
32
+ ' STORYSTICK_PROJECT_ID=a3k9m2x7\n' +
33
+ ' STORYSTICK_API_KEY=ss_sk_live_...\n');
34
+ }
35
+ const directEndpoint = process.env.STORYSTICK_ENDPOINT;
36
+ const infra = await resolveInfra(projectId, {
37
+ endpoint: directEndpoint || undefined,
38
+ });
39
+ _previewTransport = {
40
+ endpoint: infra.endpoint,
41
+ projectId,
42
+ apiKey,
43
+ };
44
+ return _previewTransport;
45
+ }
46
+ function firstHeaderValue(value) {
47
+ if (value === null)
48
+ return null;
49
+ const first = value.split(',')[0]?.trim();
50
+ return first && first.length > 0 ? first : null;
51
+ }
52
+ function safeHostHeaderOrigin(protocol, host) {
53
+ // Host is allowed to carry a port and bracketed IPv6 literal, but not URL
54
+ // delimiters/userinfo/control characters. If it is malformed, fail closed
55
+ // to Next's own origin instead of reflecting an arbitrary header string.
56
+ if (/[\u0000-\u001F\u007F\s/@\\]/.test(host))
57
+ return null;
58
+ try {
59
+ const url = new URL(`${protocol}://${host}`);
60
+ if (url.hostname.length === 0)
61
+ return null;
62
+ return url.origin;
63
+ }
64
+ catch {
65
+ return null;
66
+ }
67
+ }
68
+ /**
69
+ * Resolve the browser-visible origin for customer preview requests.
70
+ *
71
+ * Next's `nextUrl.origin` is not always browser-visible: under
72
+ * `next start --hostname 0.0.0.0`, it can become `http://0.0.0.0:*`
73
+ * even though the iframe requested `http://localhost:*`. Prefer Host,
74
+ * falling back to Next's origin only if Host is absent or malformed.
75
+ */
76
+ export function resolvePreviewRequestOrigin(request) {
77
+ const protocol = request.nextUrl.protocol === 'https:' ? 'https' : 'http';
78
+ const host = firstHeaderValue(request.headers.get('host'));
79
+ if (host !== null) {
80
+ const origin = safeHostHeaderOrigin(protocol, host);
81
+ if (origin !== null)
82
+ return origin;
83
+ }
84
+ return request.nextUrl.origin;
85
+ }
86
+ /** Reset the transport singleton (testing only). */
87
+ export function _resetPreviewTransport() {
88
+ _previewTransport = null;
89
+ }
90
+ /**
91
+ * Allow tests to inject a transport without going through env vars or
92
+ * `resolveProject`. The override is consumed by `getPreviewTransport`
93
+ * on next call. Returns a dispose function that restores prior state.
94
+ */
95
+ export function _setPreviewTransport(t) {
96
+ _previewTransport = t;
97
+ }
@@ -0,0 +1,4 @@
1
+ import type { ReadPreviewProvenanceSessionOptions, ResolvePreviewProvenanceRouteIdentityOptions, PreviewProvenanceRouteIdentity } from '@storystick/preview/server/provenance-runtime';
2
+ export type { ReadPreviewProvenanceSessionOptions, ResolvePreviewProvenanceRouteIdentityOptions, PreviewProvenanceRouteIdentity } from '@storystick/preview/server/provenance-runtime';
3
+ export declare function readPreviewProvenanceSessionId(options?: ReadPreviewProvenanceSessionOptions): Promise<string | null>;
4
+ export declare function resolvePreviewProvenanceRouteIdentity(options: ResolvePreviewProvenanceRouteIdentityOptions): Promise<PreviewProvenanceRouteIdentity>;
@@ -0,0 +1,63 @@
1
+ import { getPreviewTransport } from "./internal.js";
2
+ import { cookies, headers } from 'next/headers';
3
+ import { readPreviewGrantCookie, } from '@storystick/preview/server/extract-token';
4
+ export async function readPreviewProvenanceSessionId(options = {}) {
5
+ try {
6
+ const sessionId = options.sessionId ?? null;
7
+ const requestHeaders = await headers();
8
+ const host = requestHeaders.get('x-forwarded-host') ?? requestHeaders.get('host');
9
+ if (!host)
10
+ return null;
11
+ const requestOrigin = `${requestHeaders.get('x-forwarded-proto') ?? inferProtocol(host)}://${host}`;
12
+ const grant = await readPreviewGrantCookie((await cookies()), { ...await getPreviewTransport(), requestOrigin });
13
+ return grant ? sessionId : null;
14
+ }
15
+ catch {
16
+ return null;
17
+ }
18
+ }
19
+ export async function resolvePreviewProvenanceRouteIdentity(options) {
20
+ const routeUrl = await derivePreviewProvenanceRouteUrl(options);
21
+ return {
22
+ routeUrl,
23
+ routeHash: await sha256Hex(routeUrl),
24
+ variantId: null,
25
+ };
26
+ }
27
+ async function derivePreviewProvenanceRouteUrl(options) {
28
+ try {
29
+ const requestHeaders = await headers();
30
+ const host = requestHeaders.get('x-forwarded-host') ?? requestHeaders.get('host');
31
+ const proto = requestHeaders.get('x-forwarded-proto') ?? inferProtocol(host);
32
+ const origin = host ? `${proto}://${host}` : '';
33
+ const path = options.fallbackPathname ?? '/';
34
+ return withPreviewSessionQuery(`${origin}${path || '/'}`, options.sessionId);
35
+ }
36
+ catch {
37
+ return withPreviewSessionQuery(options.fallbackPathname ?? '/', options.sessionId);
38
+ }
39
+ }
40
+ function inferProtocol(host) {
41
+ if (host === null)
42
+ return 'https';
43
+ const normalized = host.toLowerCase();
44
+ return normalized.startsWith('localhost') ||
45
+ normalized.startsWith('127.0.0.1') ||
46
+ normalized.startsWith('[::1]') ||
47
+ normalized.startsWith('::1')
48
+ ? 'http'
49
+ : 'https';
50
+ }
51
+ function withPreviewSessionQuery(routeUrl, sessionId) {
52
+ if (routeUrl.includes('_ss_session='))
53
+ return routeUrl;
54
+ const separator = routeUrl.includes('?') ? '&' : '?';
55
+ return `${routeUrl}${separator}_ss_session=${encodeURIComponent(sessionId)}`;
56
+ }
57
+ async function sha256Hex(input) {
58
+ const bytes = new TextEncoder().encode(input);
59
+ const digest = await crypto.subtle.digest('SHA-256', bytes);
60
+ return Array.from(new Uint8Array(digest))
61
+ .map((byte) => byte.toString(16).padStart(2, '0'))
62
+ .join('');
63
+ }
@@ -0,0 +1,42 @@
1
+ import type { StorystickPreviewProvenanceConfig } from '@storystick/preview/server/provenance-runtime';
2
+ export type { StorystickPreviewProvenanceConfig } from '@storystick/preview/server/provenance-runtime';
3
+ import { type ReactElement } from 'react';
4
+ import type { PreviewProvenanceEditableTextPathManifest } from '@storystick/preview/provenance';
5
+ type EnvLike = Record<string, string | undefined>;
6
+ export interface ResolveStorystickPreviewProvenanceConfigOptions {
7
+ readonly env?: EnvLike;
8
+ readonly sessionId?: string;
9
+ }
10
+ export interface ResolveStorystickPreviewProvenanceManifestOptions {
11
+ readonly importer: () => Promise<unknown>;
12
+ /** The module the importer stands for. Reported verbatim in diagnostics. */
13
+ readonly specifier?: string;
14
+ /** Overrides the console reporter. */
15
+ readonly onDiagnostic?: (diagnostic: PreviewProvenanceManifestDiagnostic) => void;
16
+ }
17
+ export type PreviewProvenanceManifestFailure =
18
+ /** The module did not resolve at all — missing file, or missing alias. */
19
+ 'module-unresolved'
20
+ /** The module resolved but carries no usable manifest export. */
21
+ | 'module-malformed';
22
+ export interface PreviewProvenanceManifestDiagnostic {
23
+ readonly reason: PreviewProvenanceManifestFailure;
24
+ readonly specifier: string;
25
+ readonly cause?: string;
26
+ }
27
+ export declare function unstable_resolveStorystickPreviewProvenanceConfig(options?: ResolveStorystickPreviewProvenanceConfigOptions): StorystickPreviewProvenanceConfig;
28
+ export declare function unstable_renderStorystickPreviewNoIndex(config?: Pick<StorystickPreviewProvenanceConfig, 'enabled'>): ReactElement | null;
29
+ /**
30
+ * Loads the generated provenance manifest.
31
+ *
32
+ * The manifest is what makes rendered text editable. When it fails to load,
33
+ * every text-level binding silently disappears and the preview reads as "this
34
+ * page has nothing editable" rather than "the manifest did not load" — the two
35
+ * are indistinguishable from the outside, which is why the failure is reported
36
+ * here instead of being swallowed.
37
+ *
38
+ * Resolution still fails closed to an empty manifest: a manifest that will not
39
+ * load must never take a production render down with it.
40
+ */
41
+ export declare function unstable_resolveStorystickPreviewProvenanceManifest(options: ResolveStorystickPreviewProvenanceManifestOptions): Promise<PreviewProvenanceEditableTextPathManifest>;
42
+ export declare function formatPreviewProvenanceManifestFailure(diagnostic: PreviewProvenanceManifestDiagnostic): string;
@@ -0,0 +1,132 @@
1
+ import { createElement } from 'react';
2
+ import { resolveStorystickServerCodeVersion } from '../code-version.js';
3
+ const DEFAULT_PREVIEW_PROVENANCE_SESSION_ID = 'storystick_preview_provenance';
4
+ const PREVIEW_PROVENANCE_SESSION_ID_ENV = 'STORYSTICK_PREVIEW_PROVENANCE_SESSION_ID';
5
+ function present(value) {
6
+ return value && value.length > 0 ? value : undefined;
7
+ }
8
+ export function unstable_resolveStorystickPreviewProvenanceConfig(options = {}) {
9
+ const env = options.env ?? process.env;
10
+ const enabled = env.STORYSTICK_PREVIEW_PROVENANCE === '1';
11
+ const fixtureEnabled = enabled && env.STORYSTICK_PREVIEW_PROVENANCE_FIXTURE === '1';
12
+ const sessionId = options.sessionId ??
13
+ present(env[PREVIEW_PROVENANCE_SESSION_ID_ENV]) ??
14
+ DEFAULT_PREVIEW_PROVENANCE_SESSION_ID;
15
+ return {
16
+ enabled,
17
+ fixtureEnabled,
18
+ sessionId,
19
+ codeVersion: resolveStorystickServerCodeVersion({ env }),
20
+ fixture: fixtureEnabled
21
+ ? {
22
+ enabled: true,
23
+ sessionId,
24
+ }
25
+ : null,
26
+ };
27
+ }
28
+ export function unstable_renderStorystickPreviewNoIndex(config = unstable_resolveStorystickPreviewProvenanceConfig()) {
29
+ return config.enabled
30
+ ? createElement('meta', {
31
+ name: 'robots',
32
+ content: 'noindex,nofollow,noarchive',
33
+ })
34
+ : null;
35
+ }
36
+ /**
37
+ * Loads the generated provenance manifest.
38
+ *
39
+ * The manifest is what makes rendered text editable. When it fails to load,
40
+ * every text-level binding silently disappears and the preview reads as "this
41
+ * page has nothing editable" rather than "the manifest did not load" — the two
42
+ * are indistinguishable from the outside, which is why the failure is reported
43
+ * here instead of being swallowed.
44
+ *
45
+ * Resolution still fails closed to an empty manifest: a manifest that will not
46
+ * load must never take a production render down with it.
47
+ */
48
+ export async function unstable_resolveStorystickPreviewProvenanceManifest(options) {
49
+ const importer = options.importer;
50
+ const specifier = options.specifier ?? 'Storystick site definition';
51
+ const report = options.onDiagnostic ?? reportPreviewProvenanceManifestFailure;
52
+ let module;
53
+ try {
54
+ module = await importer();
55
+ }
56
+ catch (error) {
57
+ report({
58
+ reason: 'module-unresolved',
59
+ specifier,
60
+ cause: describeError(error),
61
+ });
62
+ return EMPTY_PREVIEW_PROVENANCE_MANIFEST;
63
+ }
64
+ const manifest = isRecord(module)
65
+ ? module.previewProvenanceEditableTextPathManifest
66
+ : undefined;
67
+ if (!isRecord(manifest)) {
68
+ report({
69
+ reason: 'module-malformed',
70
+ specifier,
71
+ cause: `expected an exported \`previewProvenanceEditableTextPathManifest\` object, found ${describeShape(manifest)}`,
72
+ });
73
+ return EMPTY_PREVIEW_PROVENANCE_MANIFEST;
74
+ }
75
+ return manifest;
76
+ }
77
+ const EMPTY_PREVIEW_PROVENANCE_MANIFEST = {};
78
+ /**
79
+ * Reported once per reason in production — the same broken deploy re-renders
80
+ * thousands of routes and one legible line per cause is enough. In development
81
+ * every attempt reports, so the message comes back after each reload until the
82
+ * manifest actually loads.
83
+ */
84
+ const reportedPreviewProvenanceManifestFailures = new Set();
85
+ function reportPreviewProvenanceManifestFailure(diagnostic) {
86
+ if (process.env.NODE_ENV === 'production') {
87
+ if (reportedPreviewProvenanceManifestFailures.has(diagnostic.reason))
88
+ return;
89
+ reportedPreviewProvenanceManifestFailures.add(diagnostic.reason);
90
+ }
91
+ console.error(formatPreviewProvenanceManifestFailure(diagnostic));
92
+ }
93
+ export function formatPreviewProvenanceManifestFailure(diagnostic) {
94
+ const code = diagnostic.reason === 'module-unresolved'
95
+ ? 'PREVIEW_PROVENANCE_MANIFEST_UNRESOLVED'
96
+ : 'PREVIEW_PROVENANCE_MANIFEST_MALFORMED';
97
+ const headline = diagnostic.reason === 'module-unresolved'
98
+ ? `could not load "${diagnostic.specifier}".`
99
+ : `loaded "${diagnostic.specifier}", but it exports no usable manifest.`;
100
+ const fix = diagnostic.reason === 'module-unresolved'
101
+ ? [
102
+ ' 1. Generate it — `storystick sync` writes generated/storystick/preview-provenance.ts.',
103
+ ' 2. Pass the generated definition to createSite once, in your Storystick configuration.',
104
+ ]
105
+ : [
106
+ ' 1. Regenerate it — `storystick sync` rewrites generated/storystick/preview-provenance.ts.',
107
+ ' 2. Do not hand-edit the generated file; a partial or stale copy loads without exporting the manifest.',
108
+ ];
109
+ return [
110
+ `StorystickError: ${code} — ${headline}`,
111
+ 'Preview provenance is enabled (STORYSTICK_PREVIEW_PROVENANCE=1), so this is not a no-op: every text-level binding is dropped. The page still renders, and nothing on it is editable.',
112
+ ...(diagnostic.cause ? [`Cause: ${diagnostic.cause}`] : []),
113
+ 'Fix:',
114
+ ...fix,
115
+ ].join('\n');
116
+ }
117
+ function describeError(error) {
118
+ if (error instanceof Error) {
119
+ return error.message.length > 0 ? error.message : error.name;
120
+ }
121
+ return String(error);
122
+ }
123
+ function describeShape(value) {
124
+ if (value === undefined)
125
+ return 'no such export';
126
+ if (value === null)
127
+ return 'null';
128
+ return Array.isArray(value) ? 'an array' : typeof value;
129
+ }
130
+ function isRecord(value) {
131
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
132
+ }