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.
- package/CHANGELOG.md +23 -0
- package/README.md +253 -1
- package/dist/__generated__/CartOrderCompleteOrderMutation.graphql.d.ts +1 -1
- package/dist/__generated__/CartOrderCompleteOrderMutation.graphql.js +9 -3
- package/dist/__generated__/CartOrderRemoveDiscountMutation.graphql.d.ts +1 -1
- package/dist/__generated__/CartOrderRemoveDiscountMutation.graphql.js +9 -3
- package/dist/__generated__/CartOrderUpdateMutation.graphql.d.ts +1 -1
- package/dist/__generated__/CartOrderUpdateMutation.graphql.js +9 -3
- package/dist/__generated__/CartQuery.graphql.d.ts +1 -1
- package/dist/__generated__/CartQuery.graphql.js +9 -3
- package/dist/__generated__/CartStateUpdateMutation.graphql.d.ts +1 -1
- package/dist/__generated__/CartStateUpdateMutation.graphql.js +9 -3
- package/dist/__generated__/CharacterViewerIDQuery.graphql.d.ts +1 -1
- package/dist/__generated__/CharacterViewerIDQuery.graphql.js +9 -3
- package/dist/__generated__/CharacterViewerSlugQuery.graphql.d.ts +1 -1
- package/dist/__generated__/CharacterViewerSlugQuery.graphql.js +9 -3
- package/dist/__generated__/CharacterViewerStyleRefetchQuery.graphql.d.ts +1 -1
- package/dist/__generated__/CharacterViewerStyleRefetchQuery.graphql.js +9 -3
- package/dist/__generated__/CheckoutUpdateCustomerMutation.graphql.d.ts +1 -1
- package/dist/__generated__/CheckoutUpdateCustomerMutation.graphql.js +9 -3
- package/dist/__generated__/CheckoutUpdateOrderMutation.graphql.d.ts +1 -1
- package/dist/__generated__/CheckoutUpdateOrderMutation.graphql.js +9 -3
- package/dist/__generated__/CollectionAa_Query.graphql.d.ts +1 -1
- package/dist/__generated__/CollectionAa_Query.graphql.js +9 -3
- package/dist/__generated__/FontFamiliesQuery.graphql.d.ts +1 -1
- package/dist/__generated__/FontFamiliesQuery.graphql.js +9 -3
- package/dist/__generated__/FontdueAdminToolbarQuery.graphql.d.ts +20 -0
- package/dist/__generated__/FontdueAdminToolbarQuery.graphql.js +80 -0
- package/dist/__generated__/FontdueAdminToolbarTokenMutation.graphql.d.ts +18 -0
- package/dist/__generated__/FontdueAdminToolbarTokenMutation.graphql.js +56 -0
- package/dist/__generated__/PrecartAddToCartMutation.graphql.d.ts +1 -1
- package/dist/__generated__/PrecartAddToCartMutation.graphql.js +9 -3
- package/dist/__generated__/StoreModalCartQuery.graphql.d.ts +1 -1
- package/dist/__generated__/StoreModalCartQuery.graphql.js +9 -3
- package/dist/__generated__/StoreModalContainerQuery.graphql.d.ts +1 -1
- package/dist/__generated__/StoreModalContainerQuery.graphql.js +9 -3
- package/dist/__generated__/StoreModalIndexQuery.graphql.d.ts +1 -1
- package/dist/__generated__/StoreModalIndexQuery.graphql.js +9 -3
- package/dist/__generated__/StoreModalProductQuery.graphql.d.ts +1 -1
- package/dist/__generated__/StoreModalProductQuery.graphql.js +9 -3
- package/dist/__generated__/StoreModalProductRefetchQuery.graphql.d.ts +1 -1
- package/dist/__generated__/StoreModalProductRefetchQuery.graphql.js +9 -3
- package/dist/__generated__/TestFontsFormUpdateCustomerMutation.graphql.d.ts +1 -1
- package/dist/__generated__/TestFontsFormUpdateCustomerMutation.graphql.js +9 -3
- package/dist/__generated__/TypeTesterStandaloneChangedStylesQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTesterStandaloneChangedStylesQuery.graphql.js +9 -3
- package/dist/__generated__/TypeTesterStandaloneQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTesterStandaloneQuery.graphql.js +9 -3
- package/dist/__generated__/TypeTestersChangedStylesQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTestersChangedStylesQuery.graphql.js +9 -3
- package/dist/__generated__/TypeTestersIDQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTestersIDQuery.graphql.js +9 -3
- package/dist/__generated__/TypeTestersRefetchQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTestersRefetchQuery.graphql.js +9 -3
- package/dist/__generated__/TypeTestersSlugQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTestersSlugQuery.graphql.js +9 -3
- package/dist/__generated__/orderTrackingUpdateOrderTrackingMutation.graphql.js +1 -8
- package/dist/__generated__/useFontStyle_fontStyle.graphql.d.ts +2 -1
- package/dist/__generated__/useFontStyle_fontStyle.graphql.js +8 -2
- package/dist/__tests__/createFontdueFetch.test.js +276 -0
- package/dist/__tests__/imageLoader.test.js +62 -0
- package/dist/__tests__/metricFallback.test.js +74 -0
- package/dist/__tests__/networkFetch.test.js +188 -0
- package/dist/__tests__/nextAdapter.test.js +273 -18
- package/dist/__tests__/preview.test.js +217 -0
- package/dist/__tests__/previewServer.test.js +118 -0
- package/dist/__tests__/previewState.test.js +63 -0
- package/dist/__tests__/serverConfig.test.js +62 -0
- package/dist/components/BuyButton/index.d.ts +2 -2
- package/dist/components/BuyButton/index.js +3 -3
- package/dist/components/Cart/CartOrder.js +9 -1
- package/dist/components/Cart/orderTracking.js +8 -15
- package/dist/components/CharacterViewer/index.d.ts +2 -2
- package/dist/components/CharacterViewer/index.js +20 -11
- package/dist/components/ConfigContext.d.ts +21 -2
- package/dist/components/ConfigContext.js +12 -2
- package/dist/components/ConnectionErrorToolbar.d.ts +1 -0
- package/dist/components/ConnectionErrorToolbar.js +106 -0
- package/dist/components/FontStyle/index.d.ts +2 -0
- package/dist/components/FontStyle/index.js +4 -2
- package/dist/components/FontdueAdminToolbar/index.d.ts +2 -0
- package/dist/components/FontdueAdminToolbar/index.js +299 -0
- package/dist/components/FontdueAdminToolbar/previewState.d.ts +7 -0
- package/dist/components/FontdueAdminToolbar/previewState.js +58 -0
- package/dist/components/FontdueContextProvider/index.js +4 -2
- package/dist/components/FontdueProvider/index.js +6 -1
- package/dist/components/FontdueProvider/index.server.d.ts +1 -0
- package/dist/components/FontdueProvider/index.server.js +10 -0
- package/dist/components/NewsletterSignup/index.d.ts +2 -2
- package/dist/components/NewsletterSignup/index.js +2 -2
- package/dist/components/Root/index.js +16 -2
- package/dist/components/TestFontsForm/index.d.ts +2 -2
- package/dist/components/TestFontsForm/index.js +2 -2
- package/dist/components/TypeTester/TypeTesterStandalone.d.ts +2 -2
- package/dist/components/TypeTester/TypeTesterStandalone.js +2 -2
- package/dist/components/TypeTester/index.js +3 -1
- package/dist/components/TypeTester/useTypeTesterStyler.d.ts +3 -1
- package/dist/components/TypeTester/useTypeTesterStyler.js +70 -20
- package/dist/components/TypeTesters/index.d.ts +2 -2
- package/dist/components/TypeTesters/index.js +3 -3
- package/dist/components/elements/StoreModalUnifiedCheckout.js +8 -0
- package/dist/components/useFontStyle.d.ts +8 -0
- package/dist/components/useFontStyle.js +14 -4
- package/dist/corsError.d.ts +1 -5
- package/dist/corsError.js +23 -13
- package/dist/data/unicodeNamesUrl.d.ts +2 -0
- package/dist/data/unicodeNamesUrl.js +18 -0
- package/dist/data/unicodeNamesVersion.d.ts +1 -0
- package/dist/data/unicodeNamesVersion.js +4 -0
- package/dist/fallbackFontData.d.ts +2 -0
- package/dist/fallbackFontData.js +10 -0
- package/dist/fontdue.css +231 -4
- package/dist/loadFontdueProviderQuery.d.ts +2 -1
- package/dist/loadFontdueProviderQuery.js +5 -2
- package/dist/metricFallback.d.ts +48 -0
- package/dist/metricFallback.js +98 -0
- package/dist/next/config.d.ts +1 -5
- package/dist/next/config.js +14 -1
- package/dist/next/image-loader.js +22 -3
- package/dist/next/index.d.ts +1 -2
- package/dist/next/index.js +14 -6
- package/dist/next/registerSingleTenantResolver.d.ts +1 -0
- package/dist/next/registerSingleTenantResolver.js +35 -0
- package/dist/next/revalidate.js +1 -1
- package/dist/next/tenant.d.ts +10 -2
- package/dist/next/tenant.js +111 -16
- package/dist/preview/constants.d.ts +9 -0
- package/dist/preview/constants.js +117 -0
- package/dist/preview/index.d.ts +53 -0
- package/dist/preview/index.js +190 -0
- package/dist/preview/server.d.ts +20 -0
- package/dist/preview/server.js +89 -0
- package/dist/relay/environment.d.ts +8 -0
- package/dist/relay/environment.js +81 -25
- package/dist/relay/loadSerializableQuery.d.ts +13 -3
- package/dist/relay/loadSerializableQuery.js +2 -0
- package/dist/relay/serverConfig.d.ts +5 -1
- package/dist/relay/serverConfig.js +83 -8
- package/dist/scripts/publishUnicodeData.js +68 -0
- package/dist/scripts/updateUnicodeData.js +41 -6
- package/dist/server/index.d.ts +37 -0
- package/dist/server/index.js +160 -0
- package/package.json +5 -1
- package/types/next-headers.d.ts +9 -0
- package/types/next-navigation.d.ts +10 -0
- 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.
|
|
9
|
-
//
|
|
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(
|
|
45
|
+
return origins.split(',').some(entry => matchesOrigin(hostname, entry));
|
|
27
46
|
}
|
|
28
47
|
export default function fontdueImageLoader(_ref) {
|
|
29
48
|
let {
|
package/dist/next/index.d.ts
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export { setFontdueServerConfig, getFontdueServerConfig, type FontdueServerConfig, } from '../relay/serverConfig.js';
|
|
1
|
+
export { __prepareFontdueRender, type FontdueEndpoint } from './tenant.js';
|
package/dist/next/index.js
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
|
|
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
|
+
});
|
package/dist/next/revalidate.js
CHANGED
|
@@ -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
|
|
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
|
|
package/dist/next/tenant.d.ts
CHANGED
|
@@ -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
|
|
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 {};
|
package/dist/next/tenant.js
CHANGED
|
@@ -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
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
23
|
+
// __prepareFontdueRender (multi-tenant) or, in single-tenant apps, the ambient
|
|
24
|
+
// resolver that <FontdueProvider> registers — no 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
|
|
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
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
} =
|
|
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
|
|
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>;
|