@lynkow/next 0.1.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/dist/index.js ADDED
@@ -0,0 +1,169 @@
1
+ export { REVALIDATE, TAGS, cached, uncached } from './chunk-XNN4SECG.js';
2
+ import { createLocalePaths } from './chunk-4QTGOTFV.js';
3
+ export { ABSOLUTE_HREF, createLocalePaths } from './chunk-4QTGOTFV.js';
4
+ import { createClient } from 'lynkow';
5
+
6
+ // src/config.ts
7
+ var DEFAULT_API_URL = "https://api.lynkow.com";
8
+ var HOST = /^(?:[a-z0-9_-]+(?:\.[a-z0-9_-]+)*\.?|\[[0-9a-f:.]+\])$/i;
9
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
10
+ var LOCALE = /^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/;
11
+ function resolveConfig(input, labels = {}) {
12
+ const problems = [];
13
+ const name = (field) => `\`${labels[field] ?? field}\``;
14
+ const siteId = input.siteId?.trim() ?? "";
15
+ if (!siteId) problems.push(`${name("siteId")} is required`);
16
+ else if (!UUID.test(siteId)) problems.push(`${name("siteId")} is not a site UUID: ${quote(siteId)}`);
17
+ const rawSiteUrl = input.siteUrl?.trim() ?? "";
18
+ if (!rawSiteUrl) problems.push(`${name("siteUrl")} is required`);
19
+ const siteUrl = rawSiteUrl ? parseOrigin(name("siteUrl"), rawSiteUrl, problems) : void 0;
20
+ const rawApiUrl = input.apiUrl?.trim() ?? "";
21
+ const apiUrl = rawApiUrl ? parseOrigin(name("apiUrl"), rawApiUrl, problems) : DEFAULT_API_URL;
22
+ const locales = parseLocales(name("locales"), input.locales, problems);
23
+ const wanted = input.defaultLocale?.trim() ?? "";
24
+ const defaultLocale = wanted ? locales.find((locale) => locale.toLowerCase() === wanted.toLowerCase()) : locales[0];
25
+ if (wanted && locales.length > 0 && !defaultLocale) {
26
+ problems.push(
27
+ `${name("defaultLocale")} is ${quote(wanted)}, which is not one of the locales (${locales.join(", ")})`
28
+ );
29
+ }
30
+ const prefixDefaultLocale = parseFlag(
31
+ name("prefixDefaultLocale"),
32
+ input.prefixDefaultLocale,
33
+ true,
34
+ problems
35
+ );
36
+ const publishableKey = input.publishableKey?.trim() || void 0;
37
+ if (publishableKey && !publishableKey.startsWith("lkw_pk_")) {
38
+ problems.push(
39
+ `${name("publishableKey")} is not a publishable key (lkw_pk_...). This configuration reaches the browser, so it must never hold a secret API key`
40
+ );
41
+ }
42
+ if (problems.length > 0 || !siteUrl || !apiUrl || !defaultLocale) {
43
+ throw new TypeError(`Invalid Lynkow configuration:
44
+ - ${problems.join("\n - ")}`);
45
+ }
46
+ return Object.freeze({
47
+ siteId: siteId.toLowerCase(),
48
+ siteUrl,
49
+ apiUrl,
50
+ publishableKey,
51
+ locales: Object.freeze([...locales]),
52
+ defaultLocale,
53
+ prefixDefaultLocale
54
+ });
55
+ }
56
+ function parseOrigin(field, raw, problems) {
57
+ if (hasUnsafeCharacter(raw)) {
58
+ problems.push(`${field} contains whitespace or a control character`);
59
+ return void 0;
60
+ }
61
+ let url;
62
+ try {
63
+ url = new URL(raw);
64
+ } catch {
65
+ problems.push(`${field} is not an absolute URL${echoUrl(raw)}`);
66
+ return void 0;
67
+ }
68
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
69
+ problems.push(`${field} must use http or https${echoUrl(raw)}`);
70
+ } else if (url.username || url.password) {
71
+ problems.push(`${field} must not carry credentials`);
72
+ } else if (!HOST.test(url.hostname)) {
73
+ problems.push(`${field} has a host with characters no domain carries${echoUrl(raw)}`);
74
+ } else if (!/^\/+$/.test(url.pathname) || url.search || url.hash) {
75
+ problems.push(`${field} must be an origin, with no path, query or fragment${echoUrl(raw)}`);
76
+ } else {
77
+ return url.origin;
78
+ }
79
+ return void 0;
80
+ }
81
+ function parseLocales(field, input, problems) {
82
+ const list = typeof input === "string" ? input.split(",") : [...input ?? []];
83
+ const locales = list.map((locale) => String(locale).trim()).filter(Boolean);
84
+ if (locales.length === 0) {
85
+ problems.push(`${field} must name at least one locale`);
86
+ return [];
87
+ }
88
+ const seen = /* @__PURE__ */ new Set();
89
+ for (const locale of locales) {
90
+ if (!LOCALE.test(locale)) {
91
+ problems.push(`${field} has an invalid locale ${quote(locale)}, expected a code such as "en" or "pt-BR"`);
92
+ }
93
+ const key = locale.toLowerCase();
94
+ if (seen.has(key)) problems.push(`${field} lists ${quote(locale)} twice`);
95
+ seen.add(key);
96
+ }
97
+ return locales;
98
+ }
99
+ function parseFlag(field, value, fallback, problems) {
100
+ if (typeof value === "boolean") return value;
101
+ const normalized = value?.trim().toLowerCase() ?? "";
102
+ if (normalized === "") return fallback;
103
+ if (normalized === "true") return true;
104
+ if (normalized === "false") return false;
105
+ problems.push(`${field} must be true or false, not ${quote(String(value))}`);
106
+ return fallback;
107
+ }
108
+ function hasUnsafeCharacter(value) {
109
+ if (/\s/.test(value)) return true;
110
+ for (let index = 0; index < value.length; index++) {
111
+ const code = value.charCodeAt(index);
112
+ if (code < 32 || code === 127) return true;
113
+ }
114
+ return false;
115
+ }
116
+ function echoUrl(raw) {
117
+ return raw.includes("@") ? "" : `: ${quote(raw)}`;
118
+ }
119
+ function quote(value) {
120
+ return JSON.stringify(value.length > 120 ? `${value.slice(0, 120)}...` : value);
121
+ }
122
+ function createClients(config) {
123
+ const { toLocale } = createLocalePaths(config);
124
+ const clients = /* @__PURE__ */ new Map();
125
+ return function lynkow(locale = config.defaultLocale, { retry = true } = {}) {
126
+ const canonical = toLocale(locale);
127
+ if (!canonical) {
128
+ throw new RangeError(
129
+ `${JSON.stringify(locale)} is not one of the configured locales (${config.locales.join(", ")})`
130
+ );
131
+ }
132
+ const key = retry ? canonical : `${canonical}:no-retry`;
133
+ const existing = clients.get(key);
134
+ if (existing) return existing;
135
+ const client = createClient({
136
+ siteId: config.siteId,
137
+ locale: canonical,
138
+ baseUrl: config.apiUrl,
139
+ ...config.publishableKey ? { publishableKey: config.publishableKey } : {},
140
+ cache: false,
141
+ retry
142
+ });
143
+ clients.set(key, client);
144
+ return client;
145
+ };
146
+ }
147
+
148
+ // src/analytics/events.ts
149
+ function tracker() {
150
+ if (typeof window === "undefined") return null;
151
+ const global = window.LynkowAnalytics;
152
+ return global && typeof global.track === "function" ? global : null;
153
+ }
154
+ function trackEvent(type, data) {
155
+ const global = tracker();
156
+ if (!global) return false;
157
+ global.track({
158
+ event_type: type,
159
+ page_path: window.location.pathname,
160
+ page_url: window.location.href,
161
+ page_title: document.title,
162
+ ...data
163
+ });
164
+ return true;
165
+ }
166
+
167
+ export { createClients, resolveConfig, trackEvent };
168
+ //# sourceMappingURL=index.js.map
169
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/config.ts","../src/clients.ts","../src/analytics/events.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * The configuration a Lynkow site hands this adapter, and the one function\n * that validates it.\n *\n * Nothing in this package reads `process.env`. Next inlines `NEXT_PUBLIC_*`\n * variables at build time by textual substitution, so only a literal\n * `process.env.NEXT_PUBLIC_X` written in the application's own source reaches\n * the browser bundle, and a library cannot spell out names an application\n * chose. The application reads its environment and passes the values here.\n *\n * Everything in the resolved configuration is public, because it reaches\n * client bundles. A server secret, such as the webhook signing secret, never\n * belongs in it.\n */\n\n/** What an application passes to {@link resolveConfig}, typically straight from its environment. */\nexport interface LynkowNextConfigInput {\n /** The site's UUID, shown in the Lynkow admin and returned by the MCP `list_sites` tool. */\n siteId: string | undefined\n /** The public origin of the site, such as `https://www.example.com`. No path, query or fragment. */\n siteUrl: string | undefined\n /**\n * The locales the site serves, as an array or a comma-separated list, in the\n * order they should be advertised. Must mirror the site's enabled locales.\n */\n locales: string | readonly string[] | undefined\n /** The default locale. Defaults to the first of `locales`. */\n defaultLocale?: string | undefined\n /**\n * Whether the default locale carries its prefix in public URLs. `true`, the\n * default, serves `/en/about` and `/fr/a-propos`; `false` serves `/about` and\n * `/fr/a-propos`. The strings `\"true\"` and `\"false\"` are accepted too, so an\n * environment variable can be passed as it is.\n */\n prefixDefaultLocale?: boolean | string | undefined\n /** The Lynkow API origin. Defaults to `https://api.lynkow.com`. */\n apiUrl?: string | undefined\n /** The site's publishable key (`lkw_pk_...`), which storefront reads need. */\n publishableKey?: string | undefined\n}\n\n/** A validated configuration, frozen. Every field is safe to ship to the browser. */\nexport interface LynkowNextConfig {\n readonly siteId: string\n /** The site's origin, with no trailing slash, so `${siteUrl}${path}` is always well formed. */\n readonly siteUrl: string\n /** The API origin, with no trailing slash. */\n readonly apiUrl: string\n readonly publishableKey: string | undefined\n /** The locales as configured, in their configured case. URLs carry them lowercased. */\n readonly locales: readonly string[]\n /** One of `locales`, in its configured case. */\n readonly defaultLocale: string\n readonly prefixDefaultLocale: boolean\n}\n\n/**\n * Names to use for each field in error messages, such as the environment\n * variable the application reads it from.\n */\nexport type LynkowNextConfigLabels = Partial<Record<keyof LynkowNextConfigInput, string>>\n\n/**\n * The SDK's own default API origin, which it does not export. Repeated here\n * because the analytics tracker is a plain script tag rather than an SDK call,\n * and needs a real URL.\n */\nconst DEFAULT_API_URL = 'https://api.lynkow.com'\n\n/**\n * A DNS name or an IP literal. The URL standard lets a host carry `\"`, `'`, `&`\n * or a backtick, and the origin is pasted as text into JSON-LD, XML sitemaps and\n * HTML attributes, so only what a real host is made of passes.\n */\nconst HOST = /^(?:[a-z0-9_-]+(?:\\.[a-z0-9_-]+)*\\.?|\\[[0-9a-f:.]+\\])$/i\n\nconst UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i\n\n/**\n * A language subtag, then optional region or variant subtags: `en`, `fr`,\n * `pt-BR`. Strict on purpose: a locale becomes a path segment, and a value\n * such as `//evil.example` or `en/../fr` would turn a localized link into one\n * that leaves the site or lands on another locale.\n */\nconst LOCALE = /^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/\n\n/**\n * Validates a configuration and fills in its defaults.\n *\n * It throws from a call the application made, never while this module is\n * imported, so a site owner gets an error they can read rather than a stack\n * trace out of a dependency's import graph.\n *\n * @param input - The values, typically read from the application's environment\n * @param labels - Optional names for the fields in error messages, such as\n * `{ siteId: 'NEXT_PUBLIC_LYNKOW_SITE_ID' }`\n * @throws {TypeError} Listing every problem found, one per line\n *\n * @example\n * export const config = resolveConfig({\n * siteId: process.env.NEXT_PUBLIC_LYNKOW_SITE_ID,\n * siteUrl: process.env.NEXT_PUBLIC_SITE_URL,\n * locales: process.env.NEXT_PUBLIC_LOCALES,\n * })\n */\nexport function resolveConfig(\n input: LynkowNextConfigInput,\n labels: LynkowNextConfigLabels = {}\n): LynkowNextConfig {\n const problems: string[] = []\n const name = (field: keyof LynkowNextConfigInput) => `\\`${labels[field] ?? field}\\``\n\n const siteId = input.siteId?.trim() ?? ''\n if (!siteId) problems.push(`${name('siteId')} is required`)\n else if (!UUID.test(siteId)) problems.push(`${name('siteId')} is not a site UUID: ${quote(siteId)}`)\n\n const rawSiteUrl = input.siteUrl?.trim() ?? ''\n if (!rawSiteUrl) problems.push(`${name('siteUrl')} is required`)\n const siteUrl = rawSiteUrl ? parseOrigin(name('siteUrl'), rawSiteUrl, problems) : undefined\n\n const rawApiUrl = input.apiUrl?.trim() ?? ''\n const apiUrl = rawApiUrl ? parseOrigin(name('apiUrl'), rawApiUrl, problems) : DEFAULT_API_URL\n\n const locales = parseLocales(name('locales'), input.locales, problems)\n\n const wanted = input.defaultLocale?.trim() ?? ''\n const defaultLocale = wanted\n ? locales.find((locale) => locale.toLowerCase() === wanted.toLowerCase())\n : locales[0]\n if (wanted && locales.length > 0 && !defaultLocale) {\n problems.push(\n `${name('defaultLocale')} is ${quote(wanted)}, which is not one of the locales (${locales.join(', ')})`\n )\n }\n\n const prefixDefaultLocale = parseFlag(\n name('prefixDefaultLocale'),\n input.prefixDefaultLocale,\n true,\n problems\n )\n\n // An allowlist, not a denylist of `lkw_sk_`: legacy secret tokens are bare\n // hex and still authenticate, and every publishable key is `lkw_pk_`.\n const publishableKey = input.publishableKey?.trim() || undefined\n if (publishableKey && !publishableKey.startsWith('lkw_pk_')) {\n problems.push(\n `${name('publishableKey')} is not a publishable key (lkw_pk_...). This configuration reaches the browser, so it must never hold a secret API key`\n )\n }\n\n if (problems.length > 0 || !siteUrl || !apiUrl || !defaultLocale) {\n throw new TypeError(`Invalid Lynkow configuration:\\n - ${problems.join('\\n - ')}`)\n }\n\n return Object.freeze({\n siteId: siteId.toLowerCase(),\n siteUrl,\n apiUrl,\n publishableKey,\n locales: Object.freeze([...locales]),\n defaultLocale,\n prefixDefaultLocale,\n })\n}\n\n/**\n * Parses an http(s) origin. Whitespace and control characters are refused\n * before the URL parser sees the value, because it silently strips tabs and\n * newlines, and a newline in a site URL written into robots.txt is a line of\n * its own.\n */\nfunction parseOrigin(field: string, raw: string, problems: string[]): string | undefined {\n if (hasUnsafeCharacter(raw)) {\n problems.push(`${field} contains whitespace or a control character`)\n return undefined\n }\n let url: URL\n try {\n url = new URL(raw)\n } catch {\n problems.push(`${field} is not an absolute URL${echoUrl(raw)}`)\n return undefined\n }\n if (url.protocol !== 'http:' && url.protocol !== 'https:') {\n problems.push(`${field} must use http or https${echoUrl(raw)}`)\n } else if (url.username || url.password) {\n problems.push(`${field} must not carry credentials`)\n } else if (!HOST.test(url.hostname)) {\n problems.push(`${field} has a host with characters no domain carries${echoUrl(raw)}`)\n } else if (!/^\\/+$/.test(url.pathname) || url.search || url.hash) {\n problems.push(`${field} must be an origin, with no path, query or fragment${echoUrl(raw)}`)\n } else {\n return url.origin\n }\n return undefined\n}\n\nfunction parseLocales(\n field: string,\n input: string | readonly string[] | undefined,\n problems: string[]\n): string[] {\n const list = typeof input === 'string' ? input.split(',') : [...(input ?? [])]\n const locales = list.map((locale) => String(locale).trim()).filter(Boolean)\n if (locales.length === 0) {\n problems.push(`${field} must name at least one locale`)\n return []\n }\n const seen = new Set<string>()\n for (const locale of locales) {\n if (!LOCALE.test(locale)) {\n problems.push(`${field} has an invalid locale ${quote(locale)}, expected a code such as \"en\" or \"pt-BR\"`)\n }\n const key = locale.toLowerCase()\n if (seen.has(key)) problems.push(`${field} lists ${quote(locale)} twice`)\n seen.add(key)\n }\n return locales\n}\n\nfunction parseFlag(\n field: string,\n value: boolean | string | undefined,\n fallback: boolean,\n problems: string[]\n): boolean {\n if (typeof value === 'boolean') return value\n const normalized = value?.trim().toLowerCase() ?? ''\n if (normalized === '') return fallback\n if (normalized === 'true') return true\n if (normalized === 'false') return false\n problems.push(`${field} must be true or false, not ${quote(String(value))}`)\n return fallback\n}\n\nfunction hasUnsafeCharacter(value: string): boolean {\n if (/\\s/.test(value)) return true\n for (let index = 0; index < value.length; index++) {\n const code = value.charCodeAt(index)\n if (code < 0x20 || code === 0x7f) return true\n }\n return false\n}\n\n/**\n * A URL value appended to an error message, or nothing when it holds an `@`:\n * a value the parser refused can still carry credentials it never extracted.\n */\nfunction echoUrl(raw: string): string {\n return raw.includes('@') ? '' : `: ${quote(raw)}`\n}\n\n/** A value quoted for an error message, cut short so a pasted blob stays readable. */\nfunction quote(value: string): string {\n return JSON.stringify(value.length > 120 ? `${value.slice(0, 120)}...` : value)\n}\n","import { createClient, type Client } from 'lynkow'\nimport type { LynkowNextConfig } from './config.js'\nimport { createLocalePaths } from './locale-paths.js'\n\n/** Options of the client {@link LynkowClients} returns. */\nexport interface ClientOptions {\n /**\n * `false` returns a client that sends each read once instead of retrying a\n * 429 or a 503. Defaults to `true`.\n */\n retry?: boolean\n}\n\n/** Returns the Lynkow client of a locale, the default one when none is given. */\nexport type LynkowClients = (locale?: string, options?: ClientOptions) => Client\n\n/**\n * One Lynkow client per locale, reused across requests.\n *\n * The SDK's own cache stays OFF. On the server it is a process-global\n * unbounded `Map`, which would duplicate and outlive Next's data cache without\n * any of its invalidation. Caching is Next's job, expressed per read with\n * `cached()`.\n *\n * `{ retry: false }` gives a second client for the locale, because the SDK's\n * retry is a client setting rather than a per-read one. Only a read that\n * already knows what to do when it fails should ask for it.\n *\n * FOR READS THAT ARE THE SAME FOR EVERY VISITOR. The clients are shared by\n * every request the process serves, and the SDK keeps a buyer's state on the\n * client instance: the guest cart token and the signed-in customer session.\n * Calling `cart` or `customers` on one of these from server code would hand\n * one visitor's cart or session to the next. Buyer flows run in the browser,\n * or on a client created for that one request.\n *\n * A locale outside the configuration throws a `RangeError` instead of creating\n * a client. The clients live as long as the process, so a locale taken from a\n * URL and passed through unchecked would grow that set without bound. The\n * locale is matched case-insensitively and the client gets its configured\n * spelling.\n */\nexport function createClients(config: LynkowNextConfig): LynkowClients {\n const { toLocale } = createLocalePaths(config)\n const clients = new Map<string, Client>()\n\n return function lynkow(locale = config.defaultLocale, { retry = true } = {}) {\n const canonical = toLocale(locale)\n if (!canonical) {\n throw new RangeError(\n `${JSON.stringify(locale)} is not one of the configured locales (${config.locales.join(', ')})`\n )\n }\n\n const key = retry ? canonical : `${canonical}:no-retry`\n const existing = clients.get(key)\n if (existing) return existing\n\n const client = createClient({\n siteId: config.siteId,\n locale: canonical,\n baseUrl: config.apiUrl,\n ...(config.publishableKey ? { publishableKey: config.publishableKey } : {}),\n cache: false,\n retry,\n })\n clients.set(key, client)\n return client\n }\n}\n","import type { AnalyticsEventType } from 'lynkow'\n\nexport type { AnalyticsEventType }\n\ninterface TrackerGlobal {\n track: (event: Record<string, unknown>) => void\n}\n\nfunction tracker(): TrackerGlobal | null {\n if (typeof window === 'undefined') return null\n const global = (window as unknown as { LynkowAnalytics?: TrackerGlobal }).LynkowAnalytics\n return global && typeof global.track === 'function' ? global : null\n}\n\n/**\n * Sends one custom event through the tracker already on the page.\n *\n * `type` is a CLOSED enum of mechanical interactions, the SDK's\n * `AnalyticsEventType`: there is no free-form event name. A value outside it is\n * a compile error rather than a silently dropped row, since the collector\n * answers `204` to a payload it rejects.\n *\n * Returns `false` when no tracker has loaded, which is never an error: on an\n * `opt-in` site none loads before the visitor accepts analytics. `true` means\n * the event was handed to the tracker, which adds the session id, the visitor\n * id and the timestamp, and drops the event itself when the visitor refused on\n * an `opt-in` or `opt-out` site.\n *\n * The tracker already records pageviews, scroll, clicks, form events and Core\n * Web Vitals. Reach for this only for an interaction that escapes it, such as a\n * form submitted through `fetch` without a real `submit` event.\n */\nexport function trackEvent(type: AnalyticsEventType, data?: Record<string, unknown>): boolean {\n const global = tracker()\n if (!global) return false\n\n global.track({\n event_type: type,\n page_path: window.location.pathname,\n page_url: window.location.href,\n page_title: document.title,\n ...data,\n })\n return true\n}\n"]}
@@ -0,0 +1,236 @@
1
+ import { NextMiddleware, NextRequest, NextResponse } from 'next/server';
2
+ import { Client, Redirect } from 'lynkow';
3
+ import { L as LynkowNextConfig } from '../config-CwDV7jFS.js';
4
+ export { DEFAULT_CMS_ORIGIN } from 'lynkow/visual-editor';
5
+
6
+ /** Options of {@link createLynkowProxy}. */
7
+ interface LynkowProxyOptions {
8
+ config: LynkowNextConfig;
9
+ /** The client of a locale, typically the function `createClients(config)` returns. */
10
+ client: (locale: string) => Client;
11
+ /**
12
+ * The origin allowed to frame the site on a preview request. Defaults to the
13
+ * Lynkow dashboard. Give the visual editor the same value, or neither one.
14
+ */
15
+ cmsOrigin?: string;
16
+ }
17
+ /**
18
+ * The proxy of a Lynkow site: redirects, then locale routing, with the preview
19
+ * headers the visual editor needs.
20
+ *
21
+ * 1. **Redirects**, first, on the path as the visitor typed it. The CMS stores
22
+ * sources that way, a mix of `/old` and `/fr/blog/old` on a multilingual
23
+ * site, so matching after a locale prefix is added would silently break
24
+ * every unprefixed rule. The status code is the one the editor chose, which
25
+ * `redirect()` from `next/navigation` could not give (307 or 308 only). A
26
+ * target an editor wrote without a locale gets the visitor's, so the rule
27
+ * and the locale step cost one hop rather than two.
28
+ * 2. **Locale routing.** With `prefixDefaultLocale` on, a path without a locale
29
+ * is sent (307) to the visitor's best match: the `NEXT_LOCALE` cookie, then
30
+ * `Accept-Language`, then the default. With it off, a path without a locale
31
+ * IS the default locale's page and is served from its `[locale]` route under
32
+ * the same URL, while `/<default>/...` answers 308 to that URL. Either way a
33
+ * locale segment written in another case answers 308 to its lowercase form,
34
+ * the form the API writes, so each page has one URL.
35
+ *
36
+ * The application keeps its own `config.matcher`, which Next requires to be a
37
+ * literal in `src/proxy.ts`. It must exclude everything that is not a page:
38
+ * Next internals, API routes and files.
39
+ *
40
+ * @example
41
+ * // src/proxy.ts
42
+ * import { createClients } from '@lynkow/next'
43
+ * import { createLynkowProxy } from '@lynkow/next/proxy'
44
+ * import { config as site } from './lib/config'
45
+ *
46
+ * export default createLynkowProxy({ config: site, client: createClients(site) })
47
+ *
48
+ * export const config = {
49
+ * matcher: ['/((?!_next/|api/|.*\\.[\\w]+$).*)'],
50
+ * }
51
+ */
52
+ declare function createLynkowProxy({ config, client, cmsOrigin }: LynkowProxyOptions): NextMiddleware;
53
+
54
+ interface LynkowPreviewOptions {
55
+ /**
56
+ * Origin allowed to frame this site, defaulting to {@link DEFAULT_CMS_ORIGIN}.
57
+ *
58
+ * Note the deliberate divergence from `VisualEditorConfig.cmsOrigin`: an EMPTY
59
+ * value here takes the default too, because the safe direction for a CSP is a
60
+ * header that names the real dashboard rather than one that names nothing.
61
+ * The editor half instead refuses to start, since an editor pointed nowhere is
62
+ * inert rather than unsafe.
63
+ */
64
+ cmsOrigin?: string;
65
+ /**
66
+ * @deprecated Ignored. The middleware always looks for `lynkow-preview`.
67
+ *
68
+ * The Lynkow dashboard always opens the preview with `lynkow-preview=true` in
69
+ * the URL, and the visual editor only starts when it sees that exact parameter.
70
+ * Any other name would stop the dashboard from framing the page and leave the
71
+ * editor inactive, so this option no longer has any effect. Remove it from your
72
+ * configuration.
73
+ */
74
+ previewParam?: string;
75
+ }
76
+ /**
77
+ * Creates a Next.js middleware that lets the Lynkow dashboard frame the site in
78
+ * preview mode. It is {@link withLynkowPreview} around a middleware that returns
79
+ * nothing, so every response starts from `NextResponse.next()`.
80
+ *
81
+ * On a request carrying the `lynkow-preview` query parameter, the response gets
82
+ * `Content-Security-Policy: frame-ancestors 'self' <cmsOrigin>` and loses any
83
+ * `X-Frame-Options`. Every other response gets `frame-ancestors 'self'`.
84
+ */
85
+ declare function lynkowPreviewMiddleware(options?: LynkowPreviewOptions): NextMiddleware;
86
+ /**
87
+ * Wraps an existing Next.js middleware so its responses let the Lynkow dashboard
88
+ * frame the site in preview mode.
89
+ *
90
+ * The wrapped middleware's own `Content-Security-Policy` is kept and merged at
91
+ * the directive level. In preview mode (the `lynkow-preview` query parameter),
92
+ * its `frame-ancestors` is replaced by one allowing `cmsOrigin` and
93
+ * `X-Frame-Options` is removed. Outside preview mode, a `frame-ancestors` it
94
+ * already set is left as is, and `frame-ancestors 'self'` is added only when it
95
+ * set none. A middleware returning nothing is treated as `NextResponse.next()`.
96
+ */
97
+ declare function withLynkowPreview(existingMiddleware: NextMiddleware, options?: LynkowPreviewOptions): NextMiddleware;
98
+
99
+ /** Returns the redirect rule matching a pathname, or null. */
100
+ type RedirectMatcher = (pathname: string) => Promise<Redirect | null>;
101
+ /**
102
+ * A redirect lookup with its own in-process cache.
103
+ *
104
+ * Why its own cache, when every other read hands caching to Next. Two facts,
105
+ * both measured against Next 16.3.4 and the live API on 2026-09-08:
106
+ *
107
+ * 1. **Next's fetch cache does not reach the proxy.** `next: { revalidate,
108
+ * tags }` is accepted and ignored there: five identical requests each
109
+ * produced a real network call. So `cached()` would be decoration here,
110
+ * and `revalidateTag()` from the revalidation webhook can never reach this
111
+ * code, since the proxy is a separate bundle with its own module scope.
112
+ * 2. **A miss is uncacheable at the edge.** A path with no redirect answers
113
+ * 404 with `Cache-Control: no-store`, so a CDN bypasses it. Almost every
114
+ * request is a miss, so an uncached lookup would send every page view of
115
+ * the site to the API.
116
+ *
117
+ * The cache is per process, so a multi-instance deployment holds one copy per
118
+ * instance, and nothing invalidates it early: `TTL_MS` is the whole contract.
119
+ *
120
+ * Concurrent lookups of one path share one request, so a link posted somewhere
121
+ * busy costs one call rather than one per simultaneous visitor. A failed lookup
122
+ * answers `null` and is never cached: a transient API error would otherwise
123
+ * blind every redirect on the site for a full TTL, while answering "no
124
+ * redirect" for one request gives the visitor exactly the page they would have
125
+ * got without this module.
126
+ *
127
+ * `pathname` must carry no query string: the API compares the stored source
128
+ * against it verbatim, so `/old?page=2` matches nothing.
129
+ */
130
+ declare function createRedirectMatcher(client: () => Client): RedirectMatcher;
131
+
132
+ /**
133
+ * Options for the {@link markdownProxy} helper.
134
+ */
135
+ interface MarkdownProxyOptions {
136
+ /**
137
+ * URL prefix where the Markdown route handler is mounted. The helper
138
+ * rewrites `Accept: text/markdown` requests from `<path>` to
139
+ * `<mdPathPrefix><path>`. Defaults to `'/md'`.
140
+ *
141
+ * If your route handler lives at `app/markdown/[...path]/route.ts`,
142
+ * set this to `'/markdown'`.
143
+ */
144
+ mdPathPrefix?: string;
145
+ /**
146
+ * Whether to set `Vary: Accept` on the rewritten response. Defaults
147
+ * to `true`. Set to `false` only if your edge or CDN already sets the
148
+ * header and you want to avoid duplication.
149
+ */
150
+ setVary?: boolean;
151
+ /**
152
+ * Whether to set `X-Forwarded-Accept: text/markdown` on the rewritten
153
+ * request so the route handler can confirm the request originated
154
+ * from a Markdown negotiation. Defaults to `true`.
155
+ */
156
+ setForwardedAccept?: boolean;
157
+ }
158
+ /**
159
+ * Parses an `Accept` header per RFC 9110 §12.5.1 and returns the
160
+ * highest q-value for a target media type. Supports exact match
161
+ * (`text/markdown`), type wildcard (`text/*`), full wildcard (`*\/*`),
162
+ * and q-values.
163
+ *
164
+ * Returns `0` if the type is not present or has `q=0`. Returns `1` if
165
+ * no q value is given (per RFC, missing q means q=1). Out-of-range or
166
+ * non-numeric q values fall back to the default `1`.
167
+ *
168
+ * @param acceptHeader - The raw `Accept` header value, or `null` if absent
169
+ * @param targetType - The target media type, e.g. `'text/markdown'`
170
+ * @returns The highest q-value for the target type (0 to 1)
171
+ *
172
+ * @example
173
+ * ```typescript
174
+ * acceptQuality('text/html;q=0.9, text/markdown;q=0.8', 'text/markdown') // 0.8
175
+ * acceptQuality('text/markdown', 'text/markdown') // 1
176
+ * acceptQuality(null, 'text/markdown') // 0
177
+ * ```
178
+ */
179
+ declare function acceptQuality(acceptHeader: string | null, targetType: string): number;
180
+ /**
181
+ * Decides whether the request prefers Markdown over HTML based on its
182
+ * `Accept` header.
183
+ *
184
+ * Returns `true` when `Accept` contains `text/markdown` with q strictly
185
+ * higher than `text/html`. Ties break in favor of HTML (the default
186
+ * browsers expect).
187
+ *
188
+ * @param acceptHeader - The raw `Accept` header value, or `null` if absent
189
+ * @returns `true` when Markdown is the preferred representation
190
+ *
191
+ * @example
192
+ * ```typescript
193
+ * prefersMarkdown('text/markdown;q=0.9, text/html;q=0.8') // true
194
+ * prefersMarkdown('text/markdown, text/html') // false (tie -> HTML)
195
+ * prefersMarkdown('text/html') // false
196
+ * ```
197
+ */
198
+ declare function prefersMarkdown(acceptHeader: string | null): boolean;
199
+ /**
200
+ * Next.js proxy/middleware helper that translates
201
+ * `Accept: text/markdown` requests into rewrites to a dedicated
202
+ * Markdown route handler.
203
+ *
204
+ * Returns the rewritten `NextResponse` when the request preferred
205
+ * Markdown, or `null` when the request did not (so the caller can pass
206
+ * through to the next middleware step).
207
+ *
208
+ * Sets `Vary: Accept` on the rewritten response so intermediate caches
209
+ * keep separate variants for HTML and Markdown.
210
+ *
211
+ * The helper is Edge runtime compatible (no `node:` imports).
212
+ *
213
+ * @param request - The incoming `NextRequest`
214
+ * @param options - Optional {@link MarkdownProxyOptions} overrides
215
+ * @returns A `NextResponse.rewrite(...)` when Markdown was preferred, or `null` otherwise
216
+ *
217
+ * @example
218
+ * ```typescript
219
+ * // Next 16, proxy.ts
220
+ * import { markdownProxy } from '@lynkow/next/proxy'
221
+ * import { NextResponse, type NextRequest } from 'next/server'
222
+ *
223
+ * export function proxy(request: NextRequest) {
224
+ * const md = markdownProxy(request)
225
+ * if (md) return md
226
+ * return NextResponse.next()
227
+ * }
228
+ *
229
+ * export const config = {
230
+ * matcher: ['/((?!_next|favicon.ico|md|api).*)'],
231
+ * }
232
+ * ```
233
+ */
234
+ declare function markdownProxy(request: NextRequest, options?: MarkdownProxyOptions): NextResponse | null;
235
+
236
+ export { type LynkowProxyOptions, type MarkdownProxyOptions, type RedirectMatcher, acceptQuality, createLynkowProxy, createRedirectMatcher, lynkowPreviewMiddleware, markdownProxy, prefersMarkdown, withLynkowPreview };
@@ -0,0 +1,219 @@
1
+ import { createLocalePaths } from '../chunk-4QTGOTFV.js';
2
+ import { NextResponse } from 'next/server';
3
+ import { DEFAULT_CMS_ORIGIN, PREVIEW_PARAM } from 'lynkow/visual-editor';
4
+ export { DEFAULT_CMS_ORIGIN } from 'lynkow/visual-editor';
5
+
6
+ var CSP_HEADER = "Content-Security-Policy";
7
+ function warnIgnoredPreviewParam(options) {
8
+ const previewParam = options?.previewParam;
9
+ if (previewParam && previewParam !== PREVIEW_PARAM) {
10
+ console.warn(
11
+ `[lynkow] The previewParam option ("${previewParam}") is deprecated and ignored. The preview middleware always uses "${PREVIEW_PARAM}", the only parameter the Lynkow dashboard sends and the visual editor recognises. Remove the option.`
12
+ );
13
+ }
14
+ }
15
+ function lynkowPreviewMiddleware(options) {
16
+ return withLynkowPreview(() => void 0, options);
17
+ }
18
+ function isFrameAncestors(directive) {
19
+ const [name = ""] = directive.split(/\s+/, 1);
20
+ return name.toLowerCase() === "frame-ancestors";
21
+ }
22
+ function parseCspList(value) {
23
+ if (!value) return [];
24
+ return value.split(",").map(
25
+ (policy) => policy.split(";").map((directive) => directive.trim()).filter((directive) => directive.length > 0)
26
+ ).filter((policy) => policy.length > 0);
27
+ }
28
+ function appendToFirstPolicy(policies, directive) {
29
+ const [first = [], ...rest] = policies;
30
+ return [[...first, directive], ...rest];
31
+ }
32
+ function serializeCspList(policies) {
33
+ return policies.filter((policy) => policy.length > 0).map((policy) => policy.join("; ")).join(", ");
34
+ }
35
+ function addCspHeaders(response, request, cmsOrigin) {
36
+ const isPreview = request.nextUrl.searchParams.has(PREVIEW_PARAM);
37
+ const policies = parseCspList(response.headers.get(CSP_HEADER));
38
+ if (isPreview) {
39
+ const withoutFraming = policies.map(
40
+ (policy) => policy.filter((directive) => !isFrameAncestors(directive))
41
+ );
42
+ const merged = appendToFirstPolicy(withoutFraming, `frame-ancestors 'self' ${cmsOrigin}`);
43
+ response.headers.set(CSP_HEADER, serializeCspList(merged));
44
+ response.headers.delete("X-Frame-Options");
45
+ } else if (!policies.some((policy) => policy.some(isFrameAncestors))) {
46
+ const merged = appendToFirstPolicy(policies, "frame-ancestors 'self'");
47
+ response.headers.set(CSP_HEADER, serializeCspList(merged));
48
+ }
49
+ return response;
50
+ }
51
+ function withLynkowPreview(existingMiddleware, options) {
52
+ const cmsOrigin = options?.cmsOrigin || DEFAULT_CMS_ORIGIN;
53
+ warnIgnoredPreviewParam(options);
54
+ return function middleware(request, event) {
55
+ const response = existingMiddleware(request, event);
56
+ const finalResponse = response instanceof Promise ? response.then((res) => addCspHeaders(res || NextResponse.next(), request, cmsOrigin)) : addCspHeaders(response || NextResponse.next(), request, cmsOrigin);
57
+ return finalResponse;
58
+ };
59
+ }
60
+
61
+ // src/proxy/redirects.ts
62
+ var TTL_MS = 6e4;
63
+ var MAX_ENTRIES = 500;
64
+ function createRedirectMatcher(client) {
65
+ const cache = /* @__PURE__ */ new Map();
66
+ const inFlight = /* @__PURE__ */ new Map();
67
+ function remember(path, value) {
68
+ if (cache.size >= MAX_ENTRIES && !cache.has(path)) {
69
+ const oldest = cache.keys().next().value;
70
+ if (oldest !== void 0) cache.delete(oldest);
71
+ }
72
+ cache.set(path, { value, expiresAt: Date.now() + TTL_MS });
73
+ }
74
+ return async function matchRedirect(pathname) {
75
+ const entry = cache.get(pathname);
76
+ if (entry && entry.expiresAt > Date.now()) return entry.value;
77
+ if (entry) cache.delete(pathname);
78
+ const pending = inFlight.get(pathname);
79
+ if (pending) return pending;
80
+ const lookup = Promise.resolve().then(() => client().paths.matchRedirect(pathname)).then((value) => {
81
+ remember(pathname, value);
82
+ return value;
83
+ }).catch((error) => {
84
+ console.error(`[lynkow] redirect lookup failed for ${pathname}`, error);
85
+ return null;
86
+ }).finally(() => {
87
+ inFlight.delete(pathname);
88
+ });
89
+ inFlight.set(pathname, lookup);
90
+ return lookup;
91
+ };
92
+ }
93
+
94
+ // src/proxy/lynkow-proxy.ts
95
+ var LOCALE_COOKIE = "NEXT_LOCALE";
96
+ function createLynkowProxy({ config, client, cmsOrigin }) {
97
+ const paths = createLocalePaths(config);
98
+ const { defaultLocale, prefixDefaultLocale } = config;
99
+ const defaultSegment = `/${defaultLocale.toLowerCase()}`;
100
+ const matchRedirect = createRedirectMatcher(() => client(defaultLocale));
101
+ function prefixOf(pathname) {
102
+ const segment = pathname.split("/")[1] ?? "";
103
+ const locale = paths.toLocale(segment);
104
+ return locale ? { locale, rest: pathname.slice(segment.length + 1) || "/" } : null;
105
+ }
106
+ function resolveLocale(request) {
107
+ return paths.localeFromPath(request.nextUrl.pathname) ?? paths.toLocale(request.cookies.get(LOCALE_COOKIE)?.value) ?? paths.negotiateLocale(request.headers.get("accept-language"));
108
+ }
109
+ async function findRedirect(pathname) {
110
+ if (prefixDefaultLocale || prefixOf(pathname)) return matchRedirect(pathname);
111
+ const [typed, prefixed] = await Promise.all([
112
+ matchRedirect(pathname),
113
+ matchRedirect(pathname === "/" ? defaultSegment : defaultSegment + pathname)
114
+ ]);
115
+ return typed ?? prefixed;
116
+ }
117
+ function redirectTo(request, redirect, locale) {
118
+ const from = request.nextUrl;
119
+ let url;
120
+ try {
121
+ url = new URL(paths.localizeHref(redirect.target, locale), from.href);
122
+ } catch {
123
+ console.error(`[lynkow] unparseable redirect target ${redirect.target} for ${redirect.source}`);
124
+ return null;
125
+ }
126
+ if (url.origin === from.origin) url.pathname = url.pathname.replace(/^\/{2,}/, "/");
127
+ if (redirect.preserveQueryString) {
128
+ for (const [key, value] of from.searchParams) {
129
+ if (!url.searchParams.has(key)) url.searchParams.append(key, value);
130
+ }
131
+ }
132
+ if (url.origin === from.origin && url.pathname === from.pathname && url.search === from.search) {
133
+ console.error(`[lynkow] ignoring self-referential redirect on ${redirect.source}`);
134
+ return null;
135
+ }
136
+ return NextResponse.redirect(url, redirect.statusCode);
137
+ }
138
+ function redirectToPath(request, pathname, status) {
139
+ const url = request.nextUrl.clone();
140
+ url.pathname = pathname;
141
+ return NextResponse.redirect(url, status);
142
+ }
143
+ function route(request, locale) {
144
+ const { pathname } = request.nextUrl;
145
+ const prefix = prefixOf(pathname);
146
+ if (prefix) {
147
+ const canonical = paths.localePath(prefix.locale, prefix.rest);
148
+ return canonical === pathname ? NextResponse.next() : redirectToPath(request, canonical, 308);
149
+ }
150
+ if (!prefixDefaultLocale) {
151
+ const url = request.nextUrl.clone();
152
+ url.pathname = pathname === "/" ? defaultSegment : defaultSegment + pathname;
153
+ return NextResponse.rewrite(url);
154
+ }
155
+ const response = redirectToPath(request, paths.localePath(locale, pathname), 307);
156
+ response.cookies.set(LOCALE_COOKIE, locale, {
157
+ path: "/",
158
+ maxAge: 60 * 60 * 24 * 365,
159
+ sameSite: "lax"
160
+ });
161
+ return response;
162
+ }
163
+ async function site(request) {
164
+ const locale = resolveLocale(request);
165
+ const redirect = await findRedirect(request.nextUrl.pathname);
166
+ return redirect && redirectTo(request, redirect, locale) || route(request, locale);
167
+ }
168
+ return withLynkowPreview(site, { cmsOrigin });
169
+ }
170
+ var DEFAULT_MD_PREFIX = "/md";
171
+ function acceptQuality(acceptHeader, targetType) {
172
+ if (!acceptHeader) return 0;
173
+ const [targetMain, targetSub] = targetType.toLowerCase().split("/");
174
+ let best = 0;
175
+ for (const raw of acceptHeader.split(",")) {
176
+ const parts = raw.trim().split(";").map((p) => p.trim()).filter(Boolean);
177
+ const mediaType = parts[0]?.toLowerCase();
178
+ if (!mediaType || !mediaType.includes("/")) continue;
179
+ const [main, sub] = mediaType.split("/");
180
+ const matches = main === targetMain && sub === targetSub || main === targetMain && sub === "*" || main === "*" && sub === "*";
181
+ if (!matches) continue;
182
+ let q = 1;
183
+ for (const param of parts.slice(1)) {
184
+ if (param.startsWith("q=")) {
185
+ const parsed = Number.parseFloat(param.slice(2));
186
+ if (!Number.isNaN(parsed) && parsed >= 0 && parsed <= 1) q = parsed;
187
+ }
188
+ }
189
+ if (q > best) best = q;
190
+ }
191
+ return best;
192
+ }
193
+ function prefersMarkdown(acceptHeader) {
194
+ const md = acceptQuality(acceptHeader, "text/markdown");
195
+ if (md === 0) return false;
196
+ const html = acceptQuality(acceptHeader, "text/html");
197
+ return md > html;
198
+ }
199
+ function markdownProxy(request, options) {
200
+ const accept = request.headers.get("accept");
201
+ if (!prefersMarkdown(accept)) return null;
202
+ const prefix = options?.mdPathPrefix ?? DEFAULT_MD_PREFIX;
203
+ const setVary = options?.setVary ?? true;
204
+ const setForwardedAccept = options?.setForwardedAccept ?? true;
205
+ const url = request.nextUrl.clone();
206
+ if (url.pathname.startsWith(`${prefix}/`) || url.pathname === prefix) {
207
+ return null;
208
+ }
209
+ url.pathname = `${prefix}${url.pathname}`;
210
+ const headers = new Headers(request.headers);
211
+ if (setForwardedAccept) headers.set("x-forwarded-accept", "text/markdown");
212
+ const response = NextResponse.rewrite(url, { request: { headers } });
213
+ if (setVary) response.headers.set("vary", "accept");
214
+ return response;
215
+ }
216
+
217
+ export { acceptQuality, createLynkowProxy, createRedirectMatcher, lynkowPreviewMiddleware, markdownProxy, prefersMarkdown, withLynkowPreview };
218
+ //# sourceMappingURL=index.js.map
219
+ //# sourceMappingURL=index.js.map