fontdue-js 3.0.0-alpha8 → 3.0.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 (146) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +253 -1
  3. package/dist/__generated__/CartOrderCompleteOrderMutation.graphql.d.ts +1 -1
  4. package/dist/__generated__/CartOrderCompleteOrderMutation.graphql.js +9 -3
  5. package/dist/__generated__/CartOrderRemoveDiscountMutation.graphql.d.ts +1 -1
  6. package/dist/__generated__/CartOrderRemoveDiscountMutation.graphql.js +9 -3
  7. package/dist/__generated__/CartOrderUpdateMutation.graphql.d.ts +1 -1
  8. package/dist/__generated__/CartOrderUpdateMutation.graphql.js +9 -3
  9. package/dist/__generated__/CartQuery.graphql.d.ts +1 -1
  10. package/dist/__generated__/CartQuery.graphql.js +9 -3
  11. package/dist/__generated__/CartStateUpdateMutation.graphql.d.ts +1 -1
  12. package/dist/__generated__/CartStateUpdateMutation.graphql.js +9 -3
  13. package/dist/__generated__/CharacterViewerIDQuery.graphql.d.ts +1 -1
  14. package/dist/__generated__/CharacterViewerIDQuery.graphql.js +9 -3
  15. package/dist/__generated__/CharacterViewerSlugQuery.graphql.d.ts +1 -1
  16. package/dist/__generated__/CharacterViewerSlugQuery.graphql.js +9 -3
  17. package/dist/__generated__/CharacterViewerStyleRefetchQuery.graphql.d.ts +1 -1
  18. package/dist/__generated__/CharacterViewerStyleRefetchQuery.graphql.js +9 -3
  19. package/dist/__generated__/CheckoutUpdateCustomerMutation.graphql.d.ts +1 -1
  20. package/dist/__generated__/CheckoutUpdateCustomerMutation.graphql.js +9 -3
  21. package/dist/__generated__/CheckoutUpdateOrderMutation.graphql.d.ts +1 -1
  22. package/dist/__generated__/CheckoutUpdateOrderMutation.graphql.js +9 -3
  23. package/dist/__generated__/CollectionAa_Query.graphql.d.ts +1 -1
  24. package/dist/__generated__/CollectionAa_Query.graphql.js +9 -3
  25. package/dist/__generated__/FontFamiliesQuery.graphql.d.ts +1 -1
  26. package/dist/__generated__/FontFamiliesQuery.graphql.js +9 -3
  27. package/dist/__generated__/FontdueAdminToolbarQuery.graphql.d.ts +20 -0
  28. package/dist/__generated__/FontdueAdminToolbarQuery.graphql.js +80 -0
  29. package/dist/__generated__/FontdueAdminToolbarTokenMutation.graphql.d.ts +18 -0
  30. package/dist/__generated__/FontdueAdminToolbarTokenMutation.graphql.js +56 -0
  31. package/dist/__generated__/PrecartAddToCartMutation.graphql.d.ts +1 -1
  32. package/dist/__generated__/PrecartAddToCartMutation.graphql.js +9 -3
  33. package/dist/__generated__/StoreModalCartQuery.graphql.d.ts +1 -1
  34. package/dist/__generated__/StoreModalCartQuery.graphql.js +9 -3
  35. package/dist/__generated__/StoreModalContainerQuery.graphql.d.ts +1 -1
  36. package/dist/__generated__/StoreModalContainerQuery.graphql.js +9 -3
  37. package/dist/__generated__/StoreModalIndexQuery.graphql.d.ts +1 -1
  38. package/dist/__generated__/StoreModalIndexQuery.graphql.js +9 -3
  39. package/dist/__generated__/StoreModalProductQuery.graphql.d.ts +1 -1
  40. package/dist/__generated__/StoreModalProductQuery.graphql.js +9 -3
  41. package/dist/__generated__/StoreModalProductRefetchQuery.graphql.d.ts +1 -1
  42. package/dist/__generated__/StoreModalProductRefetchQuery.graphql.js +9 -3
  43. package/dist/__generated__/TestFontsFormUpdateCustomerMutation.graphql.d.ts +1 -1
  44. package/dist/__generated__/TestFontsFormUpdateCustomerMutation.graphql.js +9 -3
  45. package/dist/__generated__/TypeTesterStandaloneChangedStylesQuery.graphql.d.ts +1 -1
  46. package/dist/__generated__/TypeTesterStandaloneChangedStylesQuery.graphql.js +9 -3
  47. package/dist/__generated__/TypeTesterStandaloneQuery.graphql.d.ts +1 -1
  48. package/dist/__generated__/TypeTesterStandaloneQuery.graphql.js +9 -3
  49. package/dist/__generated__/TypeTestersChangedStylesQuery.graphql.d.ts +1 -1
  50. package/dist/__generated__/TypeTestersChangedStylesQuery.graphql.js +9 -3
  51. package/dist/__generated__/TypeTestersIDQuery.graphql.d.ts +1 -1
  52. package/dist/__generated__/TypeTestersIDQuery.graphql.js +9 -3
  53. package/dist/__generated__/TypeTestersRefetchQuery.graphql.d.ts +1 -1
  54. package/dist/__generated__/TypeTestersRefetchQuery.graphql.js +9 -3
  55. package/dist/__generated__/TypeTestersSlugQuery.graphql.d.ts +1 -1
  56. package/dist/__generated__/TypeTestersSlugQuery.graphql.js +9 -3
  57. package/dist/__generated__/orderTrackingUpdateOrderTrackingMutation.graphql.js +1 -8
  58. package/dist/__generated__/useFontStyle_fontStyle.graphql.d.ts +2 -1
  59. package/dist/__generated__/useFontStyle_fontStyle.graphql.js +8 -2
  60. package/dist/__tests__/createFontdueFetch.test.js +276 -0
  61. package/dist/__tests__/imageLoader.test.js +62 -0
  62. package/dist/__tests__/metricFallback.test.js +74 -0
  63. package/dist/__tests__/networkFetch.test.js +188 -0
  64. package/dist/__tests__/nextAdapter.test.js +273 -18
  65. package/dist/__tests__/preview.test.js +217 -0
  66. package/dist/__tests__/previewServer.test.js +118 -0
  67. package/dist/__tests__/previewState.test.js +63 -0
  68. package/dist/__tests__/serverConfig.test.js +62 -0
  69. package/dist/components/BuyButton/index.d.ts +2 -2
  70. package/dist/components/BuyButton/index.js +3 -3
  71. package/dist/components/Cart/CartOrder.js +9 -1
  72. package/dist/components/Cart/orderTracking.js +8 -15
  73. package/dist/components/CharacterViewer/index.d.ts +2 -2
  74. package/dist/components/CharacterViewer/index.js +20 -11
  75. package/dist/components/ConfigContext.d.ts +21 -2
  76. package/dist/components/ConfigContext.js +12 -2
  77. package/dist/components/ConnectionErrorToolbar.d.ts +1 -0
  78. package/dist/components/ConnectionErrorToolbar.js +106 -0
  79. package/dist/components/FontStyle/index.d.ts +2 -0
  80. package/dist/components/FontStyle/index.js +4 -2
  81. package/dist/components/FontdueAdminToolbar/index.d.ts +2 -0
  82. package/dist/components/FontdueAdminToolbar/index.js +299 -0
  83. package/dist/components/FontdueAdminToolbar/previewState.d.ts +7 -0
  84. package/dist/components/FontdueAdminToolbar/previewState.js +58 -0
  85. package/dist/components/FontdueContextProvider/index.js +4 -2
  86. package/dist/components/FontdueProvider/index.js +6 -1
  87. package/dist/components/FontdueProvider/index.server.d.ts +1 -0
  88. package/dist/components/FontdueProvider/index.server.js +10 -0
  89. package/dist/components/NewsletterSignup/index.d.ts +2 -2
  90. package/dist/components/NewsletterSignup/index.js +2 -2
  91. package/dist/components/Root/index.js +16 -2
  92. package/dist/components/TestFontsForm/index.d.ts +2 -2
  93. package/dist/components/TestFontsForm/index.js +2 -2
  94. package/dist/components/TypeTester/TypeTesterStandalone.d.ts +2 -2
  95. package/dist/components/TypeTester/TypeTesterStandalone.js +2 -2
  96. package/dist/components/TypeTester/index.js +3 -1
  97. package/dist/components/TypeTester/useTypeTesterStyler.d.ts +3 -1
  98. package/dist/components/TypeTester/useTypeTesterStyler.js +70 -20
  99. package/dist/components/TypeTesters/index.d.ts +2 -2
  100. package/dist/components/TypeTesters/index.js +3 -3
  101. package/dist/components/elements/StoreModalUnifiedCheckout.js +8 -0
  102. package/dist/components/useFontStyle.d.ts +8 -0
  103. package/dist/components/useFontStyle.js +14 -4
  104. package/dist/corsError.d.ts +1 -5
  105. package/dist/corsError.js +23 -13
  106. package/dist/data/unicodeNamesUrl.d.ts +2 -0
  107. package/dist/data/unicodeNamesUrl.js +18 -0
  108. package/dist/data/unicodeNamesVersion.d.ts +1 -0
  109. package/dist/data/unicodeNamesVersion.js +4 -0
  110. package/dist/fallbackFontData.d.ts +2 -0
  111. package/dist/fallbackFontData.js +10 -0
  112. package/dist/fontdue.css +231 -4
  113. package/dist/loadFontdueProviderQuery.d.ts +2 -1
  114. package/dist/loadFontdueProviderQuery.js +5 -2
  115. package/dist/metricFallback.d.ts +48 -0
  116. package/dist/metricFallback.js +98 -0
  117. package/dist/next/config.d.ts +1 -5
  118. package/dist/next/config.js +14 -1
  119. package/dist/next/image-loader.js +22 -3
  120. package/dist/next/index.d.ts +1 -2
  121. package/dist/next/index.js +14 -6
  122. package/dist/next/registerSingleTenantResolver.d.ts +1 -0
  123. package/dist/next/registerSingleTenantResolver.js +35 -0
  124. package/dist/next/revalidate.js +1 -1
  125. package/dist/next/tenant.d.ts +10 -2
  126. package/dist/next/tenant.js +111 -16
  127. package/dist/preview/constants.d.ts +9 -0
  128. package/dist/preview/constants.js +117 -0
  129. package/dist/preview/index.d.ts +53 -0
  130. package/dist/preview/index.js +190 -0
  131. package/dist/preview/server.d.ts +20 -0
  132. package/dist/preview/server.js +89 -0
  133. package/dist/relay/environment.d.ts +8 -0
  134. package/dist/relay/environment.js +81 -25
  135. package/dist/relay/loadSerializableQuery.d.ts +13 -3
  136. package/dist/relay/loadSerializableQuery.js +2 -0
  137. package/dist/relay/serverConfig.d.ts +5 -1
  138. package/dist/relay/serverConfig.js +83 -8
  139. package/dist/scripts/publishUnicodeData.js +68 -0
  140. package/dist/scripts/updateUnicodeData.js +41 -6
  141. package/dist/server/index.d.ts +37 -0
  142. package/dist/server/index.js +160 -0
  143. package/package.json +5 -1
  144. package/types/next-headers.d.ts +9 -0
  145. package/types/next-navigation.d.ts +10 -0
  146. package/vitest.config.ts +9 -0
@@ -5,8 +5,10 @@
5
5
  // NEXT_PUBLIC_FONTDUE_IMAGE_HOST is set; see config.ts.
6
6
  //
7
7
  // NEXT_PUBLIC_FONTDUE_IMAGE_ORIGINS (comma-separated hostnames) should
8
- // mirror the transformation host's allowed-origins list. Sources on other
9
- // hosts e.g. the /logo endpoint a site serves from its own (possibly
8
+ // mirror the transformation host's allowed-origins list. A leading "*."
9
+ // wildcard matches any subdomain ("*.fontdue.xyz" "cdn.fontdue.xyz", not
10
+ // the apex or a lookalike); a scheme/port on an entry is ignored. Sources on
11
+ // other hosts — e.g. the /logo endpoint a site serves from its own (possibly
10
12
  // customer-owned) domain, which can't be allowlisted — are served as-is
11
13
  // rather than as transform URLs Cloudflare would refuse (ERROR 9401).
12
14
  //
@@ -14,6 +16,23 @@
14
16
  // reads at build time, so it must stay dependency-free, and the variables
15
17
  // have to be present when `next build` runs (not just at serve time).
16
18
 
19
+ // Reduce an allowlist entry to a bare hostname (or "*."-prefixed wildcard),
20
+ // tolerating an optional scheme, path, or port so "https://*.fontdue.xyz"
21
+ // behaves like "*.fontdue.xyz".
22
+ function originHostname(entry) {
23
+ return entry.trim().replace(/^https?:\/\//i, '').split('/')[0].split(':')[0];
24
+ }
25
+ function matchesOrigin(hostname, entry) {
26
+ const origin = originHostname(entry);
27
+ if (!origin) return false;
28
+ if (origin.startsWith('*.')) {
29
+ // ".fontdue.xyz" — endsWith plus the strict length check requires at
30
+ // least one label before the dot, so the apex and lookalikes don't match.
31
+ const suffix = origin.slice(1);
32
+ return hostname.endsWith(suffix) && hostname.length > suffix.length;
33
+ }
34
+ return origin === hostname;
35
+ }
17
36
  function transformable(src) {
18
37
  const origins = process.env.NEXT_PUBLIC_FONTDUE_IMAGE_ORIGINS;
19
38
  if (!origins) return true;
@@ -23,7 +42,7 @@ function transformable(src) {
23
42
  } catch {
24
43
  return false;
25
44
  }
26
- return origins.split(',').some(origin => origin.trim() === hostname);
45
+ return origins.split(',').some(entry => matchesOrigin(hostname, entry));
27
46
  }
28
47
  export default function fontdueImageLoader(_ref) {
29
48
  let {
@@ -1,2 +1 @@
1
- export { isMultiTenant, isValidDomain, fontdueEndpoint, fallbackSiteUrl, fontdueServerConfig, configureFontdueRender, type FontdueEndpoint, } from './tenant.js';
2
- export { setFontdueServerConfig, getFontdueServerConfig, type FontdueServerConfig, } from '../relay/serverConfig.js';
1
+ export { __prepareFontdueRender, type FontdueEndpoint } from './tenant.js';
@@ -2,9 +2,17 @@
2
2
  // The config-time wrapper lives in 'fontdue-js/next/config' and the deploy
3
3
  // hook route handler in 'fontdue-js/next/revalidate'.
4
4
 
5
- export { isMultiTenant, isValidDomain, fontdueEndpoint, fallbackSiteUrl, fontdueServerConfig, configureFontdueRender } from './tenant.js';
6
-
7
- // The per-render config store consumed by fontdue-js's own server-side
8
- // fetches. configureFontdueRender covers the common case; these are exported
9
- // for apps that need to set or inspect the config directly.
10
- export { setFontdueServerConfig, getFontdueServerConfig } from '../relay/serverConfig.js';
5
+ // Single-tenant apps need NO per-render setup and nothing from this entrypoint:
6
+ // mounting <FontdueProvider> registers the ambient resolver that configures
7
+ // every server fetch your own (createFontdueFetch) and the embedded
8
+ // components' preloads, including the admin preview token (see
9
+ // ../components/FontdueProvider/index.server.tsx -> registerSingleTenantResolver).
10
+ //
11
+ // __prepareFontdueRender is the internal multi-tenant API (double-underscored)
12
+ // used only by the Fontdue-hosted next-template — not part of the foundry
13
+ // surface — and FontdueEndpoint is its return type. The remaining tenant/config
14
+ // helpers (isMultiTenant, isValidDomain, endpointForDomain, fontdueServerConfig,
15
+ // configureFontdueRender, setFontdueServerConfig) are deliberately not
16
+ // re-exported here; the modules that need them import them directly from
17
+ // './tenant.js' / '../relay/serverConfig.js'.
18
+ export { __prepareFontdueRender } from './tenant.js';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,35 @@
1
+ // Registers the single-tenant ambient config resolver (env URL +
2
+ // draftMode()/cookies() preview token, resolved per fetch) so a single-tenant
3
+ // foundry that mounts <FontdueProvider> never has to call a per-render setup
4
+ // function. The FontdueProvider react-server entrypoint imports this module as
5
+ // a side effect — in the call-free model the app no longer imports
6
+ // 'fontdue-js/next' itself, so the provider mount is the only thing guaranteed
7
+ // to wire the resolver up.
8
+ //
9
+ // Why this lives apart from ./tenant.ts: it statically imports ONLY the
10
+ // browser-safe registerAmbientConfigResolver. The Next request APIs
11
+ // (next/headers, next/navigation) the resolver needs sit behind a lazy
12
+ // `import('./tenant.js')` in the resolver body, which runs only when a server
13
+ // fetch actually resolves config — i.e. only under Next. That keeps next/* off
14
+ // the provider entrypoint's eager import graph, so mounting <FontdueProvider>
15
+ // stays safe for any non-Next RSC consumer. (Today only Next resolves the
16
+ // react-server export condition, but the decoupling makes that a structural
17
+ // invariant rather than an incidental one.)
18
+ import { registerAmbientConfigResolver } from '../relay/serverConfig.js';
19
+ registerAmbientConfigResolver(async () => {
20
+ // Loaded lazily so next/headers + next/navigation never enter the
21
+ // provider's static graph; config is read per fetch from Next's request
22
+ // context (draftMode()/cookies()), which also covers soft navigations that
23
+ // re-render only the page segment.
24
+ const {
25
+ isMultiTenant,
26
+ singleTenantUrl,
27
+ buildRenderConfig
28
+ } = await import('./tenant.js');
29
+ // Multi-tenant drives config through the React.cache slot
30
+ // (__prepareFontdueRender), which needs the per-request route param, so the
31
+ // resolver stays out of its way. The slot also wins the merge, so a
32
+ // multi-tenant __prepareFontdueRender call still overrides this when present.
33
+ if (isMultiTenant || !singleTenantUrl) return undefined;
34
+ return buildRenderConfig(new URL(singleTenantUrl).host);
35
+ });
@@ -7,7 +7,7 @@
7
7
  // receive the tenant in the URL, e.g.
8
8
  // POST /api/revalidate?domain=acme.fontdue.com
9
9
  // and only that tenant's cache is purged (pages and embed data share the
10
- // per-domain tag — see fontdueEndpoint/fontdueServerConfig in ./tenant).
10
+ // per-domain tag — see endpointForDomain/fontdueServerConfig in ./tenant).
11
11
  // Single-tenant deployments use the parameterless form and purge everything
12
12
  // carrying the 'graphql' tag.
13
13
 
@@ -1,7 +1,10 @@
1
1
  import { type FontdueServerConfig } from '../relay/serverConfig.js';
2
2
  export declare const isMultiTenant: boolean;
3
+ export declare const singleTenantUrl: string | undefined;
3
4
  export declare function isValidDomain(domain: string): boolean;
4
5
  export interface FontdueEndpoint {
6
+ /** The site domain this endpoint resolves. */
7
+ domain: string;
5
8
  /** Base URL the app's own GraphQL fetches should target. */
6
9
  origin: string;
7
10
  /** Headers the Fontdue server needs to resolve the tenant. */
@@ -12,7 +15,12 @@ export interface FontdueEndpoint {
12
15
  */
13
16
  tags: string[];
14
17
  }
15
- export declare function fontdueEndpoint(domain: string): FontdueEndpoint;
16
- export declare function fallbackSiteUrl(domain: string): string;
18
+ export declare function endpointForDomain(domain: string): FontdueEndpoint;
17
19
  export declare function fontdueServerConfig(domain: string): FontdueServerConfig;
18
20
  export declare function configureFontdueRender(domain: string): FontdueEndpoint | null;
21
+ interface RenderProps {
22
+ params: Promise<Record<string, string | string[] | undefined>>;
23
+ }
24
+ export declare function buildRenderConfig(domain: string): Promise<FontdueServerConfig>;
25
+ export declare function __prepareFontdueRender(props: RenderProps): Promise<FontdueEndpoint>;
26
+ export {};
@@ -20,13 +20,17 @@
20
20
  //
21
21
  // The fontdue-js components embedded in pages fetch the same way: their
22
22
  // server-side preloads read the per-render config set by
23
- // configureFontdueRender, and in the browser they fetch the relative
24
- // /graphql on the page's own origin so multi-tenant mode needs no
25
- // NEXT_PUBLIC_FONTDUE_URL at all.
23
+ // __prepareFontdueRender (multi-tenant) or, in single-tenant apps, the ambient
24
+ // resolver that <FontdueProvider> registersno per-render setup call — and in
25
+ // the browser they fetch the relative /graphql on the page's own origin, so
26
+ // multi-tenant mode needs no NEXT_PUBLIC_FONTDUE_URL at all.
26
27
 
28
+ import { notFound, unstable_rethrow } from 'next/navigation';
29
+ import { cookies, draftMode } from 'next/headers';
27
30
  import { setFontdueServerConfig } from '../relay/serverConfig.js';
31
+ import { PREVIEW_TOKEN_COOKIE, previewAuthHeaders } from '../preview/index.js';
28
32
  export const isMultiTenant = process.env.FONTDUE_MULTI_TENANT === '1';
29
- const singleTenantUrl = process.env.NEXT_PUBLIC_FONTDUE_URL;
33
+ export const singleTenantUrl = process.env.NEXT_PUBLIC_FONTDUE_URL;
30
34
  const internalOrigin = process.env.FONTDUE_ORIGIN;
31
35
  const proxySecret = process.env.FONTDUE_PROXY_SECRET;
32
36
 
@@ -38,20 +42,19 @@ export function isValidDomain(domain) {
38
42
  }
39
43
  // Where to fetch GraphQL for a given tenant domain, plus any headers needed
40
44
  // for the Fontdue server to resolve that tenant.
41
- export function fontdueEndpoint(domain) {
45
+ export function endpointForDomain(domain) {
42
46
  const tags = ['graphql', `graphql:${domain}`];
43
47
  if (!isMultiTenant) {
44
- if (!singleTenantUrl) {
45
- throw new Error('fontdue-js/next: set NEXT_PUBLIC_FONTDUE_URL (single-tenant) or FONTDUE_MULTI_TENANT=1 (multi-tenant).');
46
- }
47
48
  return {
48
- origin: singleTenantUrl,
49
+ domain,
50
+ origin: requireSingleTenantUrl(),
49
51
  headers: {},
50
52
  tags
51
53
  };
52
54
  }
53
55
  if (internalOrigin) {
54
56
  return {
57
+ domain,
55
58
  origin: internalOrigin,
56
59
  headers: {
57
60
  'x-forwarded-host': domain,
@@ -63,15 +66,17 @@ export function fontdueEndpoint(domain) {
63
66
  };
64
67
  }
65
68
  return {
69
+ domain,
66
70
  origin: `https://${domain}`,
67
71
  headers: {},
68
72
  tags
69
73
  };
70
74
  }
71
-
72
- // metadataBase / sitemap fallback when the site URL setting is empty.
73
- export function fallbackSiteUrl(domain) {
74
- return isMultiTenant ? `https://${domain}` : 'http://localhost:3000';
75
+ function requireSingleTenantUrl() {
76
+ if (!singleTenantUrl) {
77
+ throw new Error('fontdue-js/next: set NEXT_PUBLIC_FONTDUE_URL (single-tenant) or FONTDUE_MULTI_TENANT=1 (multi-tenant).');
78
+ }
79
+ return singleTenantUrl;
75
80
  }
76
81
 
77
82
  // Per-render config for fontdue-js's own server-side fetches: same endpoint
@@ -82,7 +87,7 @@ export function fontdueServerConfig(domain) {
82
87
  const {
83
88
  origin,
84
89
  headers
85
- } = fontdueEndpoint(domain);
90
+ } = endpointForDomain(domain);
86
91
  return {
87
92
  url: origin,
88
93
  headers,
@@ -101,5 +106,95 @@ export function fontdueServerConfig(domain) {
101
106
  export function configureFontdueRender(domain) {
102
107
  if (!isValidDomain(domain)) return null;
103
108
  setFontdueServerConfig(fontdueServerConfig(domain));
104
- return fontdueEndpoint(domain);
105
- }
109
+ return endpointForDomain(domain);
110
+ }
111
+
112
+ // The props every page, layout and generateMetadata receives. Any params
113
+ // object is structurally compatible; only `domain` is read.
114
+
115
+ // Build this render's server config for `domain` (origin, headers, per-site
116
+ // cache tags) and install it for the rest of the pass. When a logged-in admin
117
+ // is in preview (Next draft mode), fold the token into the headers and drop the
118
+ // cache tags, so every server fetch in the render — the app's own and the
119
+ // embedded components' preloads — reveals hidden fonts and stays live
120
+ // (uncached, never landing in a shared cache).
121
+ export async function buildRenderConfig(domain) {
122
+ const config = fontdueServerConfig(domain);
123
+ const previewHeaders = await readPreviewHeaders();
124
+ return previewHeaders ? {
125
+ ...config,
126
+ headers: {
127
+ ...config.headers,
128
+ ...previewHeaders
129
+ },
130
+ cacheTags: undefined
131
+ } : config;
132
+ }
133
+ async function applyRenderConfig(domain) {
134
+ setFontdueServerConfig(await buildRenderConfig(domain));
135
+ }
136
+
137
+ // Internal multi-tenant API (double-underscored, not part of the public
138
+ // foundry surface): the one line at the top of every page, layout and
139
+ // generateMetadata body in the multi-tenant [domain] route tree (the
140
+ // Fontdue-hosted service):
141
+ //
142
+ // await __prepareFontdueRender(props);
143
+ //
144
+ // Reads the request's site from the route param, 404s anything that isn't a
145
+ // plain hostname, and configures the render (including preview) for that site.
146
+ // It runs per entry point: a soft navigation re-renders only the page segment
147
+ // with a fresh render store. Route handlers are not React renders — use the
148
+ // returned endpoint explicitly there.
149
+ export async function __prepareFontdueRender(props) {
150
+ const {
151
+ domain
152
+ } = await props.params;
153
+ if (typeof domain !== 'string' || !isValidDomain(domain)) notFound();
154
+ await applyRenderConfig(domain);
155
+ return endpointForDomain(domain);
156
+ }
157
+
158
+ // The admin preview token for this render, as Authorization headers, or
159
+ // undefined when not previewing. Draft mode gates it: reading draftMode().
160
+ // isEnabled is static-safe (it returns false during static generation and only
161
+ // forces dynamic rendering when actually enabled), and the token cookie is only
162
+ // read in the preview branch.
163
+ //
164
+ // The catch handles two very different throws, and the difference is load-
165
+ // bearing for preview:
166
+ //
167
+ // 1. Next's dynamic-rendering bailout. When an admin is previewing, reading
168
+ // cookies() during a prerender/static pass throws a control-flow error
169
+ // (DynamicServerError / prerender interrupt) whose whole purpose is to
170
+ // tell Next "abandon the static render and go dynamic." This is the ONLY
171
+ // signal that takes a route in generateStaticParams off the full-route
172
+ // cache for the preview request. Swallowing it (as this used to) let Next
173
+ // serve the cached, token-less *public* render in preview, so the embedded
174
+ // components' own server preloads (createNetworkFetch) never saw the token
175
+ // — hidden fonts stayed hidden and @required(THROW) embeds like
176
+ // <BuyButton> crashed the server render (FD-712). unstable_rethrow re-
177
+ // throws exactly these control-flow errors so the route bails to a
178
+ // dynamic, token-bearing render. Public renders never hit this branch
179
+ // (draftMode().isEnabled is false → early return), so they stay static.
180
+ // 2. A genuine "no request scope" throw (older runtimes calling
181
+ // draftMode()/cookies() with no request at all): not a control-flow error,
182
+ // so unstable_rethrow is a no-op and we fall through to "not previewing".
183
+ async function readPreviewHeaders() {
184
+ try {
185
+ var _await$cookies$get;
186
+ if (!(await draftMode()).isEnabled) return undefined;
187
+ const token = (_await$cookies$get = (await cookies()).get(PREVIEW_TOKEN_COOKIE)) === null || _await$cookies$get === void 0 ? void 0 : _await$cookies$get.value;
188
+ return token ? previewAuthHeaders(token) : undefined;
189
+ } catch (error) {
190
+ unstable_rethrow(error);
191
+ return undefined;
192
+ }
193
+ }
194
+
195
+ // The single-tenant ambient resolver that lets a foundry skip per-render setup
196
+ // is registered from ./registerSingleTenantResolver.ts (imported as a side
197
+ // effect by the FontdueProvider react-server entrypoint), not here — so this
198
+ // module's static next/* imports stay off that entrypoint's eager graph. The
199
+ // resolver lazy-imports buildRenderConfig + singleTenantUrl + isMultiTenant
200
+ // from this module at fetch time.
@@ -0,0 +1,9 @@
1
+ export declare const PREVIEW_TOKEN_COOKIE = "fontdue_preview_token";
2
+ export declare const PREVIEW_MARKER_COOKIE = "fontdue_preview";
3
+ export declare const PREVIEW_ENDPOINT = "/api/preview";
4
+ export declare const DEFAULT_PREVIEW_TTL_MS: number;
5
+ export declare const PREVIEW_MARKER_GRACE_MS: number;
6
+ export declare const PREVIEW_HEADER = "fontdue-preview";
7
+ export declare function hasPreviewMarkerCookie(): boolean;
8
+ export declare function getPreviewExpiry(): number | undefined;
9
+ export declare function setPreviewMarkerCookie(on: boolean | number): void;
@@ -0,0 +1,117 @@
1
+ // Cookie + endpoint conventions for the admin preview contract.
2
+ //
3
+ // Kept in their own module (no Web/Node APIs) so the client-side toolbar can
4
+ // import them without pulling in the server-only route handler — see ./index.
5
+
6
+ // httpOnly cookie carrying the logged-in admin's short-lived admin token.
7
+ // Only ever read server-side, where it's forwarded as
8
+ // `Authorization: Bearer <token>` so GraphQL reveals hidden (unpublished) fonts.
9
+ export const PREVIEW_TOKEN_COOKIE = 'fontdue_preview_token';
10
+
11
+ // Readable twin of the token cookie. Lets the client toolbar tell preview is
12
+ // on without exposing the token itself.
13
+ export const PREVIEW_MARKER_COOKIE = 'fontdue_preview';
14
+
15
+ // Default path the toolbar POSTs (enter) / DELETEs (exit) to. Relative, so it
16
+ // hits the storefront's own origin. Override per app via
17
+ // FontdueConfig.preview.endpoint (and mount the route to match).
18
+ export const PREVIEW_ENDPOINT = '/api/preview';
19
+
20
+ // Canonical preview-token lifetime, in ms. The token is a stateless
21
+ // `Phoenix.Token` with a fixed 1-hour `max_age` (see the backend
22
+ // `Fontage.Token` `:admin_token` `verify` — `max_age: 60 * 60`). It is the
23
+ // source of truth; this constant only exists as a *fallback* for when the
24
+ // `createAdminToken` mutation hasn't yet been taught to return its real
25
+ // `expiresAt` (the backend half ships separately). Keep these two in lockstep.
26
+ export const DEFAULT_PREVIEW_TTL_MS = 60 * 60 * 1000;
27
+
28
+ // How long past the token's expiry the readable marker cookie should live.
29
+ // The marker outlives the token on purpose: once the token lapses, the toolbar
30
+ // must still find the marker so it can show the "preview expired" warning and
31
+ // offer "re-enter" — rather than the cookie silently vanishing and the toolbar
32
+ // snapping back to "not previewing" (which would hide the very problem from the
33
+ // admin). A few hours of grace comfortably covers a backgrounded tab.
34
+ export const PREVIEW_MARKER_GRACE_MS = 4 * 60 * 60 * 1000;
35
+
36
+ // Request header storefront fetches send to declare preview intent explicitly.
37
+ // The GraphQL server reveals hidden (unpublished) fonts only when this is
38
+ // "true" AND the request is an authenticated admin — so merely *being* logged
39
+ // in (an admin session cookie riding a storefront fetch) never leaks hidden
40
+ // fonts outside preview. "false" forces the public view even for an admin; an
41
+ // absent header (older fontdue-js clients) keeps the legacy "any admin sees
42
+ // hidden" behavior, so older integrations are unaffected. See the matching
43
+ // server logic in FontageWeb.Schema.Context.
44
+ export const PREVIEW_HEADER = 'fontdue-preview';
45
+
46
+ // The marker cookie's *value* now encodes the token's expiry as epoch-ms, e.g.
47
+ // `fontdue_preview=1718640000000`. Previously it was a bare `1`; that legacy
48
+ // value still parses (as 1ms after the epoch → always "expired"), so any stale
49
+ // `=1` cookie left from an older client degrades safely to the warning state
50
+ // rather than reading as a live preview. Empty / unset means "not previewing".
51
+
52
+ // Raw value of the marker cookie on the current document, or undefined when the
53
+ // cookie is absent or we're not in a browser. Browser-only and guarded so it
54
+ // stays inert during server rendering.
55
+ function readPreviewMarkerValue() {
56
+ if (typeof document === 'undefined') return undefined;
57
+ for (const part of document.cookie.split('; ')) {
58
+ const idx = part.indexOf('=');
59
+ if (idx === -1) continue;
60
+ if (part.slice(0, idx) !== PREVIEW_MARKER_COOKIE) continue;
61
+ const value = part.slice(idx + 1);
62
+ return value === '' ? undefined : value;
63
+ }
64
+ return undefined;
65
+ }
66
+
67
+ // Whether the readable preview marker cookie is present on the current
68
+ // document. Lets the admin toolbar and the client-side Relay network layer tell
69
+ // that an admin entered preview, so storefront fetches can send PREVIEW_HEADER
70
+ // "true" while the marker is set and "false" otherwise. Presence — not
71
+ // freshness — gates the header: a just-expired token still needs its render to
72
+ // stay out of the public cache until the admin re-enters or exits, and the
73
+ // server independently rejects a dead token, so a marker with a past expiry
74
+ // never actually leaks hidden fonts.
75
+ export function hasPreviewMarkerCookie() {
76
+ return readPreviewMarkerValue() !== undefined;
77
+ }
78
+
79
+ // Expiry (epoch-ms) stored in the marker cookie, or undefined when the marker
80
+ // is absent or its value isn't a finite number. The toolbar compares this to
81
+ // `Date.now()` to choose between the "previewing" and "preview expired" states.
82
+ export function getPreviewExpiry() {
83
+ const value = readPreviewMarkerValue();
84
+ if (value === undefined) return undefined;
85
+ const expiry = Number(value);
86
+ return Number.isFinite(expiry) ? expiry : undefined;
87
+ }
88
+
89
+ // Set or clear the readable preview marker cookie directly in the browser.
90
+ //
91
+ // The script-tag (CDN) embed has no server preview route to broker an httpOnly
92
+ // token through — and needs none: it is client-only, so its GraphQL fetches
93
+ // carry the admin's Fontdue session cookie cross-origin (SameSite=None) and the
94
+ // server reveals hidden fonts on `fontdue-preview: true` alone. This marker is
95
+ // what flips the client-side Relay layer to send that header (see
96
+ // hasPreviewMarkerCookie). It is NOT a credential — the admin session still
97
+ // gates the reveal, so a non-admin setting it just sees the public view.
98
+ //
99
+ // When entering (`on` truthy), `on` is the token's expiry as epoch-ms; the
100
+ // cookie stores it and is given a Max-Age that outlives the token by
101
+ // PREVIEW_MARKER_GRACE_MS so the toolbar can reach the "expired" warning state.
102
+ // Passing `false` clears the cookie. (A bare `true` keeps older call sites
103
+ // working by falling back to now + DEFAULT_PREVIEW_TTL_MS.)
104
+ //
105
+ // Secure is set on https (the marker stays JS-readable; Secure only restricts
106
+ // the transport) and omitted on http so it still persists in local dev.
107
+ export function setPreviewMarkerCookie(on) {
108
+ if (typeof document === 'undefined') return;
109
+ const secure = typeof location !== 'undefined' && location.protocol === 'https:' ? '; Secure' : '';
110
+ if (on === false) {
111
+ document.cookie = `${PREVIEW_MARKER_COOKIE}=; Path=/; SameSite=Lax${secure}; Max-Age=0`;
112
+ return;
113
+ }
114
+ const expiresAt = typeof on === 'number' ? on : Date.now() + DEFAULT_PREVIEW_TTL_MS;
115
+ const maxAgeSeconds = Math.max(0, Math.ceil((expiresAt - Date.now() + PREVIEW_MARKER_GRACE_MS) / 1000));
116
+ document.cookie = `${PREVIEW_MARKER_COOKIE}=${expiresAt}; Path=/; SameSite=Lax${secure}; Max-Age=${maxAgeSeconds}`;
117
+ }
@@ -0,0 +1,53 @@
1
+ import { PREVIEW_TOKEN_COOKIE, PREVIEW_MARKER_COOKIE, PREVIEW_ENDPOINT, DEFAULT_PREVIEW_TTL_MS, PREVIEW_MARKER_GRACE_MS } from './constants.js';
2
+ export { PREVIEW_TOKEN_COOKIE, PREVIEW_MARKER_COOKIE, PREVIEW_ENDPOINT, DEFAULT_PREVIEW_TTL_MS, PREVIEW_MARKER_GRACE_MS, };
3
+ export interface PreviewCookieOptions {
4
+ /**
5
+ * Whether to set the cookies' Secure attribute. Defaults to true on https
6
+ * requests and false otherwise, so cookies work over http in local dev.
7
+ */
8
+ secure?: boolean;
9
+ /** SameSite attribute. Defaults to 'lax' — the toolbar is same-site. */
10
+ sameSite?: 'lax' | 'strict' | 'none';
11
+ /** Cookie path. Defaults to '/'. */
12
+ path?: string;
13
+ }
14
+ /**
15
+ * Web-standard route handler implementing the portable preview contract. Wire
16
+ * it into your framework's route at the path the toolbar targets
17
+ * (`/api/preview` by default):
18
+ *
19
+ * // Astro: src/pages/api/preview.ts
20
+ * import { handlePreviewRequest } from 'fontdue-js/preview';
21
+ * export const ALL: APIRoute = ({ request }) => handlePreviewRequest(request);
22
+ *
23
+ * // React Router 7: app/routes/api.preview.ts
24
+ * import { handlePreviewRequest } from 'fontdue-js/preview';
25
+ * export const action = ({ request }: Route.ActionArgs) =>
26
+ * handlePreviewRequest(request);
27
+ *
28
+ * POST expects a JSON body `{ token: string, expiresAt?: string | number }`;
29
+ * DELETE takes no body. `expiresAt` (ISO-8601 or epoch-ms) is the token's
30
+ * expiry; it's stored in the readable marker cookie so the toolbar can warn
31
+ * when preview has lapsed, and defaults to now + DEFAULT_PREVIEW_TTL_MS when
32
+ * omitted. The token is opaque here — the Fontdue GraphQL server validates it
33
+ * on each request, so a forged token simply reveals nothing. Frameworks that
34
+ * cache HTML should also bypass their shared/CDN cache when
35
+ * PREVIEW_MARKER_COOKIE is present, so an admin preview is never served to the
36
+ * public.
37
+ */
38
+ export declare function handlePreviewRequest(request: Request, options?: PreviewCookieOptions): Promise<Response>;
39
+ /**
40
+ * Extract the preview token from a Cookie header string, e.g.
41
+ * `readPreviewToken(request.headers.get('cookie'))`. This is the canonical
42
+ * reader — it reverses the encoding `handlePreviewRequest` applies, so the
43
+ * token comes back verbatim. Returns undefined when not in preview.
44
+ */
45
+ export declare function readPreviewToken(cookieHeader: string | null | undefined): string | undefined;
46
+ /**
47
+ * Headers to merge into a server-side GraphQL fetch so the request reveals
48
+ * hidden fonts. Spread the result into your app's fetch headers, into
49
+ * fontdue-js's per-render `serverConfig.headers`, or into the `headers` option
50
+ * of a preload helper (loadFontdueProviderQuery, loadTypeTesterQuery, …).
51
+ * Returns {} when there is no token, so it's safe to spread unconditionally.
52
+ */
53
+ export declare function previewAuthHeaders(token: string | null | undefined): Record<string, string>;