@cmssy/core 11.4.1 → 11.5.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.
@@ -1,5 +1,5 @@
1
1
  import { CmssyClientConfig, BlockRect, BlockSchema, BlockMeta } from '@cmssy/types';
2
- import { G as GraphqlRequestOptions } from './locale-C7sA46qu.cjs';
2
+ import { G as GraphqlRequestOptions } from './locale-XMylB_kz.cjs';
3
3
 
4
4
  declare const DEFAULT_CMSSY_EDITOR_ORIGINS: string[];
5
5
  declare function isDevelopment(): boolean;
@@ -1,5 +1,5 @@
1
1
  import { CmssyClientConfig, BlockRect, BlockSchema, BlockMeta } from '@cmssy/types';
2
- import { G as GraphqlRequestOptions } from './locale-Ox_qVlNy.js';
2
+ import { G as GraphqlRequestOptions } from './locale-Clouq4s4.js';
3
3
 
4
4
  declare const DEFAULT_CMSSY_EDITOR_ORIGINS: string[];
5
5
  declare function isDevelopment(): boolean;
package/dist/index.cjs CHANGED
@@ -540,6 +540,57 @@ function localizeHref(href, locale) {
540
540
  return `${addLocalePrefix(bare, locale.current, locale)}${suffix}`;
541
541
  }
542
542
 
543
+ // src/data/site-locales.ts
544
+ var TTL_MS = 6e4;
545
+ var FAILURE_TTL_MS = 5e3;
546
+ var MAX_ENTRIES = 64;
547
+ var cache = /* @__PURE__ */ new Map();
548
+ function localesFromSiteConfig(siteConfig) {
549
+ const defaultLocale = siteConfig?.defaultLanguage || "en";
550
+ const enabled = siteConfig?.enabledLanguages ?? [];
551
+ return {
552
+ defaultLocale,
553
+ locales: enabled.length > 0 ? enabled : [defaultLocale]
554
+ };
555
+ }
556
+ async function loadSiteLocales(config, options) {
557
+ const key = `${resolveApiUrl(config.apiUrl)}::${config.org}::${config.workspaceSlug}`;
558
+ const cached = cache.get(key);
559
+ if (cached && cached.expires > Date.now()) return cached.value;
560
+ cache.delete(key);
561
+ let value;
562
+ try {
563
+ const data = await graphqlRequest(
564
+ config,
565
+ SITE_CONFIG_QUERY,
566
+ { workspaceSlug: config.workspaceSlug },
567
+ { ...options, public: true, retry: options?.retry ?? {} },
568
+ "site config"
569
+ );
570
+ value = localesFromSiteConfig(data.public?.siteConfig ?? null);
571
+ } catch {
572
+ value = null;
573
+ }
574
+ if (cache.size >= MAX_ENTRIES) cache.clear();
575
+ cache.set(key, {
576
+ value,
577
+ expires: Date.now() + (value ? TTL_MS : FAILURE_TTL_MS)
578
+ });
579
+ return value;
580
+ }
581
+ async function resolveCmssyLocale(config, path, options) {
582
+ const siteLocales = await loadSiteLocales(config, options);
583
+ if (!siteLocales) return void 0;
584
+ return splitLocaleFromPath(path, siteLocales).locale;
585
+ }
586
+ function splitLocaleFromPath(path, siteLocales) {
587
+ const first = path?.[0];
588
+ if (first && first !== siteLocales.defaultLocale && siteLocales.locales.includes(first)) {
589
+ return { locale: first, path: path.slice(1) };
590
+ }
591
+ return { locale: siteLocales.defaultLocale, path };
592
+ }
593
+
543
594
  // src/locale.ts
544
595
  var CMSSY_LOCALE_HEADER = "x-cmssy-locale";
545
596
 
@@ -823,5 +874,6 @@ exports.localizeHref = localizeHref;
823
874
  exports.normalizeOrigin = normalizeOrigin;
824
875
  exports.parseEditorMessage = parseEditorMessage;
825
876
  exports.postToEditor = postToEditor;
877
+ exports.resolveCmssyLocale = resolveCmssyLocale;
826
878
  exports.resolveEditorOrigin = resolveEditorOrigin;
827
879
  exports.verifyCmssyWebhook = verifyCmssyWebhook;
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- export { A as AppToEditorMessage, B as BoundsMessage, C as ClickMessage, a as CmssyClient, b as CmssyConfig, c as CmssyCspOptions, d as CmssyEnvConfig, e as CmssyTypedDocument, D as DEFAULT_CMSSY_EDITOR_ORIGINS, E as EditorToAppMessage, P as PROTOCOL_VERSION, f as ParentReadyMessage, g as PatchMessage, h as PostTarget, Q as QueryScopedOptions, R as ReadyMessage, S as SelectMessage, i as applyCmssyCsp, j as createCmssyClient, k as defineCmssyConfig, l as isProtocolCompatible, n as normalizeOrigin, p as parseEditorMessage, m as postToEditor, r as resolveEditorOrigin } from './csp-CRixC_wl.cjs';
2
- export { C as CMSSY_LOCALE_HEADER, G as GraphqlRequestOptions, g as graphqlRequest, l as localizeHref } from './locale-C7sA46qu.cjs';
1
+ export { A as AppToEditorMessage, B as BoundsMessage, C as ClickMessage, a as CmssyClient, b as CmssyConfig, c as CmssyCspOptions, d as CmssyEnvConfig, e as CmssyTypedDocument, D as DEFAULT_CMSSY_EDITOR_ORIGINS, E as EditorToAppMessage, P as PROTOCOL_VERSION, f as ParentReadyMessage, g as PatchMessage, h as PostTarget, Q as QueryScopedOptions, R as ReadyMessage, S as SelectMessage, i as applyCmssyCsp, j as createCmssyClient, k as defineCmssyConfig, l as isProtocolCompatible, n as normalizeOrigin, p as parseEditorMessage, m as postToEditor, r as resolveEditorOrigin } from './csp-0D9pcuDp.cjs';
2
+ export { C as CMSSY_LOCALE_HEADER, G as GraphqlRequestOptions, g as graphqlRequest, l as localizeHref, r as resolveCmssyLocale } from './locale-XMylB_kz.cjs';
3
3
  export { C as CmssyRequestError, D as DEFAULT_CMSSY_API_URL, F as FetchLike, a as FetchLikeResponse, b as FetchPageOptions, R as RetryPolicy } from './content-client-CJQ-nafm.cjs';
4
4
  import * as _cmssy_types from '@cmssy/types';
5
5
  import { FieldOptions, TypedField, BlockPropsSchema, InferBlockContent, RelationMode, CmssyModelRecord, VerifyCmssyWebhookOptions, CmssyWebhookEvent } from '@cmssy/types';
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export { A as AppToEditorMessage, B as BoundsMessage, C as ClickMessage, a as CmssyClient, b as CmssyConfig, c as CmssyCspOptions, d as CmssyEnvConfig, e as CmssyTypedDocument, D as DEFAULT_CMSSY_EDITOR_ORIGINS, E as EditorToAppMessage, P as PROTOCOL_VERSION, f as ParentReadyMessage, g as PatchMessage, h as PostTarget, Q as QueryScopedOptions, R as ReadyMessage, S as SelectMessage, i as applyCmssyCsp, j as createCmssyClient, k as defineCmssyConfig, l as isProtocolCompatible, n as normalizeOrigin, p as parseEditorMessage, m as postToEditor, r as resolveEditorOrigin } from './csp-DpG1NcSd.js';
2
- export { C as CMSSY_LOCALE_HEADER, G as GraphqlRequestOptions, g as graphqlRequest, l as localizeHref } from './locale-Ox_qVlNy.js';
1
+ export { A as AppToEditorMessage, B as BoundsMessage, C as ClickMessage, a as CmssyClient, b as CmssyConfig, c as CmssyCspOptions, d as CmssyEnvConfig, e as CmssyTypedDocument, D as DEFAULT_CMSSY_EDITOR_ORIGINS, E as EditorToAppMessage, P as PROTOCOL_VERSION, f as ParentReadyMessage, g as PatchMessage, h as PostTarget, Q as QueryScopedOptions, R as ReadyMessage, S as SelectMessage, i as applyCmssyCsp, j as createCmssyClient, k as defineCmssyConfig, l as isProtocolCompatible, n as normalizeOrigin, p as parseEditorMessage, m as postToEditor, r as resolveEditorOrigin } from './csp-CpiqfM03.js';
2
+ export { C as CMSSY_LOCALE_HEADER, G as GraphqlRequestOptions, g as graphqlRequest, l as localizeHref, r as resolveCmssyLocale } from './locale-Clouq4s4.js';
3
3
  export { C as CmssyRequestError, D as DEFAULT_CMSSY_API_URL, F as FetchLike, a as FetchLikeResponse, b as FetchPageOptions, R as RetryPolicy } from './content-client-CJQ-nafm.js';
4
4
  import * as _cmssy_types from '@cmssy/types';
5
5
  import { FieldOptions, TypedField, BlockPropsSchema, InferBlockContent, RelationMode, CmssyModelRecord, VerifyCmssyWebhookOptions, CmssyWebhookEvent } from '@cmssy/types';
package/dist/index.js CHANGED
@@ -538,6 +538,57 @@ function localizeHref(href, locale) {
538
538
  return `${addLocalePrefix(bare, locale.current, locale)}${suffix}`;
539
539
  }
540
540
 
541
+ // src/data/site-locales.ts
542
+ var TTL_MS = 6e4;
543
+ var FAILURE_TTL_MS = 5e3;
544
+ var MAX_ENTRIES = 64;
545
+ var cache = /* @__PURE__ */ new Map();
546
+ function localesFromSiteConfig(siteConfig) {
547
+ const defaultLocale = siteConfig?.defaultLanguage || "en";
548
+ const enabled = siteConfig?.enabledLanguages ?? [];
549
+ return {
550
+ defaultLocale,
551
+ locales: enabled.length > 0 ? enabled : [defaultLocale]
552
+ };
553
+ }
554
+ async function loadSiteLocales(config, options) {
555
+ const key = `${resolveApiUrl(config.apiUrl)}::${config.org}::${config.workspaceSlug}`;
556
+ const cached = cache.get(key);
557
+ if (cached && cached.expires > Date.now()) return cached.value;
558
+ cache.delete(key);
559
+ let value;
560
+ try {
561
+ const data = await graphqlRequest(
562
+ config,
563
+ SITE_CONFIG_QUERY,
564
+ { workspaceSlug: config.workspaceSlug },
565
+ { ...options, public: true, retry: options?.retry ?? {} },
566
+ "site config"
567
+ );
568
+ value = localesFromSiteConfig(data.public?.siteConfig ?? null);
569
+ } catch {
570
+ value = null;
571
+ }
572
+ if (cache.size >= MAX_ENTRIES) cache.clear();
573
+ cache.set(key, {
574
+ value,
575
+ expires: Date.now() + (value ? TTL_MS : FAILURE_TTL_MS)
576
+ });
577
+ return value;
578
+ }
579
+ async function resolveCmssyLocale(config, path, options) {
580
+ const siteLocales = await loadSiteLocales(config, options);
581
+ if (!siteLocales) return void 0;
582
+ return splitLocaleFromPath(path, siteLocales).locale;
583
+ }
584
+ function splitLocaleFromPath(path, siteLocales) {
585
+ const first = path?.[0];
586
+ if (first && first !== siteLocales.defaultLocale && siteLocales.locales.includes(first)) {
587
+ return { locale: first, path: path.slice(1) };
588
+ }
589
+ return { locale: siteLocales.defaultLocale, path };
590
+ }
591
+
541
592
  // src/locale.ts
542
593
  var CMSSY_LOCALE_HEADER = "x-cmssy-locale";
543
594
 
@@ -793,4 +844,4 @@ async function verifyCmssyWebhook(options) {
793
844
  return parsed;
794
845
  }
795
846
 
796
- export { CMSSY_EDIT_HEADER, CMSSY_EDIT_QUERY_PARAM, CMSSY_LOCALE_HEADER, CMSSY_SECRET_QUERY_PARAM, CmssyRequestError, CmssyWebhookError, DEFAULT_CMSSY_API_URL, DEFAULT_CMSSY_EDITOR_ORIGINS, PROTOCOL_VERSION, applyCmssyCsp, createCmssyClient, defineCmssyConfig, fields, graphqlRequest, isProtocolCompatible, isVerifiedEditUrl, localizeHref, normalizeOrigin, parseEditorMessage, postToEditor, resolveEditorOrigin, verifyCmssyWebhook };
847
+ export { CMSSY_EDIT_HEADER, CMSSY_EDIT_QUERY_PARAM, CMSSY_LOCALE_HEADER, CMSSY_SECRET_QUERY_PARAM, CmssyRequestError, CmssyWebhookError, DEFAULT_CMSSY_API_URL, DEFAULT_CMSSY_EDITOR_ORIGINS, PROTOCOL_VERSION, applyCmssyCsp, createCmssyClient, defineCmssyConfig, fields, graphqlRequest, isProtocolCompatible, isVerifiedEditUrl, localizeHref, normalizeOrigin, parseEditorMessage, postToEditor, resolveCmssyLocale, resolveEditorOrigin, verifyCmssyWebhook };
@@ -166,6 +166,7 @@ var SITE_CONFIG_QUERY = `query PublicSiteConfig($workspaceSlug: String!) {
166
166
 
167
167
  // src/data/site-locales.ts
168
168
  var TTL_MS = 6e4;
169
+ var FAILURE_TTL_MS = 5e3;
169
170
  var MAX_ENTRIES = 64;
170
171
  var cache = /* @__PURE__ */ new Map();
171
172
  function localesFromSiteConfig(siteConfig) {
@@ -176,7 +177,7 @@ function localesFromSiteConfig(siteConfig) {
176
177
  locales: enabled.length > 0 ? enabled : [defaultLocale]
177
178
  };
178
179
  }
179
- async function resolveSiteLocales(config, options) {
180
+ async function loadSiteLocales(config, options) {
180
181
  const key = `${resolveApiUrl(config.apiUrl)}::${config.org}::${config.workspaceSlug}`;
181
182
  const cached = cache.get(key);
182
183
  if (cached && cached.expires > Date.now()) return cached.value;
@@ -192,12 +193,21 @@ async function resolveSiteLocales(config, options) {
192
193
  );
193
194
  value = localesFromSiteConfig(data.public?.siteConfig ?? null);
194
195
  } catch {
195
- value = { defaultLocale: "en", locales: ["en"] };
196
+ value = null;
196
197
  }
197
198
  if (cache.size >= MAX_ENTRIES) cache.clear();
198
- cache.set(key, { value, expires: Date.now() + TTL_MS });
199
+ cache.set(key, {
200
+ value,
201
+ expires: Date.now() + (value ? TTL_MS : FAILURE_TTL_MS)
202
+ });
199
203
  return value;
200
204
  }
205
+ async function resolveSiteLocales(config, options) {
206
+ return await loadSiteLocales(config, options) ?? {
207
+ defaultLocale: "en",
208
+ locales: ["en"]
209
+ };
210
+ }
201
211
  function splitLocaleFromPath(path, siteLocales) {
202
212
  const first = path?.[0];
203
213
  if (first && first !== siteLocales.defaultLocale && siteLocales.locales.includes(first)) {
@@ -1,28 +1,11 @@
1
- import { G as GraphqlRequestOptions } from '../locale-C7sA46qu.cjs';
2
- export { C as CMSSY_LOCALE_HEADER, b as buildLocaleSwitchHref, a as localeForPath, c as localeForPathname, l as localizeHref, d as localizeHtmlLinks, s as splitCmssyLocale } from '../locale-C7sA46qu.cjs';
3
- import { CmssySiteLocales, CmssyClientConfig } from '@cmssy/types';
1
+ export { C as CMSSY_LOCALE_HEADER, b as buildLocaleSwitchHref, a as localeForPath, c as localeForPathname, d as localesFromSiteConfig, l as localizeHref, e as localizeHtmlLinks, f as resolveSiteLocales, s as splitCmssyLocale, h as splitLocaleFromPath } from '../locale-XMylB_kz.cjs';
4
2
  export { CmssySiteLocales } from '@cmssy/types';
5
3
  import '../content-client-CJQ-nafm.cjs';
6
4
 
7
- /**
8
- * Maps a workspace site config to its locale set. The single place that
9
- * decides the default and enabled languages - the router and the SEO helpers
10
- * must agree, so both go through here.
11
- */
12
- declare function localesFromSiteConfig(siteConfig: {
13
- defaultLanguage?: string | null;
14
- enabledLanguages?: string[];
15
- } | null): CmssySiteLocales;
16
- declare function resolveSiteLocales(config: CmssyClientConfig, options?: GraphqlRequestOptions): Promise<CmssySiteLocales>;
17
- declare function splitLocaleFromPath(path: string[] | undefined, siteLocales: CmssySiteLocales): {
18
- locale: string;
19
- path: string[] | undefined;
20
- };
21
-
22
5
  /**
23
6
  * Maps a page slug to its path for a locale: the default locale gets no
24
7
  * prefix, others get `/${locale}`. The homepage ("/") stays "/" (or "/${locale}").
25
8
  */
26
9
  declare function localizedPath(slug: string, locale: string, defaultLocale: string): string;
27
10
 
28
- export { localesFromSiteConfig, localizedPath, resolveSiteLocales, splitLocaleFromPath };
11
+ export { localizedPath };
@@ -1,28 +1,11 @@
1
- import { G as GraphqlRequestOptions } from '../locale-Ox_qVlNy.js';
2
- export { C as CMSSY_LOCALE_HEADER, b as buildLocaleSwitchHref, a as localeForPath, c as localeForPathname, l as localizeHref, d as localizeHtmlLinks, s as splitCmssyLocale } from '../locale-Ox_qVlNy.js';
3
- import { CmssySiteLocales, CmssyClientConfig } from '@cmssy/types';
1
+ export { C as CMSSY_LOCALE_HEADER, b as buildLocaleSwitchHref, a as localeForPath, c as localeForPathname, d as localesFromSiteConfig, l as localizeHref, e as localizeHtmlLinks, f as resolveSiteLocales, s as splitCmssyLocale, h as splitLocaleFromPath } from '../locale-Clouq4s4.js';
4
2
  export { CmssySiteLocales } from '@cmssy/types';
5
3
  import '../content-client-CJQ-nafm.js';
6
4
 
7
- /**
8
- * Maps a workspace site config to its locale set. The single place that
9
- * decides the default and enabled languages - the router and the SEO helpers
10
- * must agree, so both go through here.
11
- */
12
- declare function localesFromSiteConfig(siteConfig: {
13
- defaultLanguage?: string | null;
14
- enabledLanguages?: string[];
15
- } | null): CmssySiteLocales;
16
- declare function resolveSiteLocales(config: CmssyClientConfig, options?: GraphqlRequestOptions): Promise<CmssySiteLocales>;
17
- declare function splitLocaleFromPath(path: string[] | undefined, siteLocales: CmssySiteLocales): {
18
- locale: string;
19
- path: string[] | undefined;
20
- };
21
-
22
5
  /**
23
6
  * Maps a page slug to its path for a locale: the default locale gets no
24
7
  * prefix, others get `/${locale}`. The homepage ("/") stays "/" (or "/${locale}").
25
8
  */
26
9
  declare function localizedPath(slug: string, locale: string, defaultLocale: string): string;
27
10
 
28
- export { localesFromSiteConfig, localizedPath, resolveSiteLocales, splitLocaleFromPath };
11
+ export { localizedPath };
@@ -164,6 +164,7 @@ var SITE_CONFIG_QUERY = `query PublicSiteConfig($workspaceSlug: String!) {
164
164
 
165
165
  // src/data/site-locales.ts
166
166
  var TTL_MS = 6e4;
167
+ var FAILURE_TTL_MS = 5e3;
167
168
  var MAX_ENTRIES = 64;
168
169
  var cache = /* @__PURE__ */ new Map();
169
170
  function localesFromSiteConfig(siteConfig) {
@@ -174,7 +175,7 @@ function localesFromSiteConfig(siteConfig) {
174
175
  locales: enabled.length > 0 ? enabled : [defaultLocale]
175
176
  };
176
177
  }
177
- async function resolveSiteLocales(config, options) {
178
+ async function loadSiteLocales(config, options) {
178
179
  const key = `${resolveApiUrl(config.apiUrl)}::${config.org}::${config.workspaceSlug}`;
179
180
  const cached = cache.get(key);
180
181
  if (cached && cached.expires > Date.now()) return cached.value;
@@ -190,12 +191,21 @@ async function resolveSiteLocales(config, options) {
190
191
  );
191
192
  value = localesFromSiteConfig(data.public?.siteConfig ?? null);
192
193
  } catch {
193
- value = { defaultLocale: "en", locales: ["en"] };
194
+ value = null;
194
195
  }
195
196
  if (cache.size >= MAX_ENTRIES) cache.clear();
196
- cache.set(key, { value, expires: Date.now() + TTL_MS });
197
+ cache.set(key, {
198
+ value,
199
+ expires: Date.now() + (value ? TTL_MS : FAILURE_TTL_MS)
200
+ });
197
201
  return value;
198
202
  }
203
+ async function resolveSiteLocales(config, options) {
204
+ return await loadSiteLocales(config, options) ?? {
205
+ defaultLocale: "en",
206
+ locales: ["en"]
207
+ };
208
+ }
199
209
  function splitLocaleFromPath(path, siteLocales) {
200
210
  const first = path?.[0];
201
211
  if (first && first !== siteLocales.defaultLocale && siteLocales.locales.includes(first)) {
package/dist/internal.cjs CHANGED
@@ -713,6 +713,7 @@ async function resolveForms(config, blocks, locale, defaultLocale, options) {
713
713
 
714
714
  // src/data/site-locales.ts
715
715
  var TTL_MS = 6e4;
716
+ var FAILURE_TTL_MS = 5e3;
716
717
  var MAX_ENTRIES = 64;
717
718
  var cache = /* @__PURE__ */ new Map();
718
719
  function localesFromSiteConfig(siteConfig) {
@@ -723,7 +724,7 @@ function localesFromSiteConfig(siteConfig) {
723
724
  locales: enabled.length > 0 ? enabled : [defaultLocale]
724
725
  };
725
726
  }
726
- async function resolveSiteLocales(config, options) {
727
+ async function loadSiteLocales(config, options) {
727
728
  const key = `${resolveApiUrl(config.apiUrl)}::${config.org}::${config.workspaceSlug}`;
728
729
  const cached = cache.get(key);
729
730
  if (cached && cached.expires > Date.now()) return cached.value;
@@ -739,12 +740,21 @@ async function resolveSiteLocales(config, options) {
739
740
  );
740
741
  value = localesFromSiteConfig(data.public?.siteConfig ?? null);
741
742
  } catch {
742
- value = { defaultLocale: "en", locales: ["en"] };
743
+ value = null;
743
744
  }
744
745
  if (cache.size >= MAX_ENTRIES) cache.clear();
745
- cache.set(key, { value, expires: Date.now() + TTL_MS });
746
+ cache.set(key, {
747
+ value,
748
+ expires: Date.now() + (value ? TTL_MS : FAILURE_TTL_MS)
749
+ });
746
750
  return value;
747
751
  }
752
+ async function resolveSiteLocales(config, options) {
753
+ return await loadSiteLocales(config, options) ?? {
754
+ defaultLocale: "en",
755
+ locales: ["en"]
756
+ };
757
+ }
748
758
  function splitLocaleFromPath(path, siteLocales) {
749
759
  const first = path?.[0];
750
760
  if (first && first !== siteLocales.defaultLocale && siteLocales.locales.includes(first)) {
@@ -1,11 +1,11 @@
1
1
  export { f as fetchLayouts, c as fetchPage, d as fetchPageById, e as fetchPageMeta, g as fetchPages, n as normalizeSlug, r as resolveApiUrl, h as resolvePublicUrl } from './content-client-CJQ-nafm.cjs';
2
2
  import { CmssyFormDefinition, BuildBlockContextExtra, CmssyBlockContext, CmssyClientConfig, CmssySiteConfig, RawBlock, FieldDefinition } from '@cmssy/types';
3
3
  export { CmssySiteLocales } from '@cmssy/types';
4
- import { G as GraphqlRequestOptions } from './locale-C7sA46qu.cjs';
5
- export { C as CMSSY_LOCALE_HEADER, b as buildLocaleSwitchHref, a as localeForPath, c as localeForPathname, l as localizeHref, d as localizeHtmlLinks, s as splitCmssyLocale } from './locale-C7sA46qu.cjs';
6
- import { Q as QueryScopedOptions } from './csp-CRixC_wl.cjs';
7
- export { o as cmssyCspHeaders, q as isDevelopment, s as resolveInitialTarget, t as toCspOrigin } from './csp-CRixC_wl.cjs';
8
- export { localesFromSiteConfig, localizedPath, resolveSiteLocales, splitLocaleFromPath } from './internal/locale.cjs';
4
+ import { G as GraphqlRequestOptions } from './locale-XMylB_kz.cjs';
5
+ export { C as CMSSY_LOCALE_HEADER, b as buildLocaleSwitchHref, a as localeForPath, c as localeForPathname, d as localesFromSiteConfig, l as localizeHref, e as localizeHtmlLinks, f as resolveSiteLocales, s as splitCmssyLocale, h as splitLocaleFromPath } from './locale-XMylB_kz.cjs';
6
+ import { Q as QueryScopedOptions } from './csp-0D9pcuDp.cjs';
7
+ export { o as cmssyCspHeaders, q as isDevelopment, s as resolveInitialTarget, t as toCspOrigin } from './csp-0D9pcuDp.cjs';
8
+ export { localizedPath } from './internal/locale.cjs';
9
9
 
10
10
  declare const SITE_CONFIG_QUERY = "query PublicSiteConfig($workspaceSlug: String!) {\n public {\n siteConfig(workspaceSlug: $workspaceSlug) {\n id\n workspaceId\n siteName\n defaultLanguage\n enabledLanguages\n enabledFeatures\n notFoundPageId\n previewUrl\n branding {\n brandName\n logoUrl\n logoDarkUrl\n faviconUrl\n ogImageUrl\n }\n }\n }\n}";
11
11
  declare const MODEL_DEFINITIONS_QUERY = "query PublicModelDefinitions($workspaceId: String!) {\n public {\n model {\n definitions(workspaceId: $workspaceId) {\n id name slug description icon color displayField recordCount\n }\n }\n }\n}";
@@ -1,11 +1,11 @@
1
1
  export { f as fetchLayouts, c as fetchPage, d as fetchPageById, e as fetchPageMeta, g as fetchPages, n as normalizeSlug, r as resolveApiUrl, h as resolvePublicUrl } from './content-client-CJQ-nafm.js';
2
2
  import { CmssyFormDefinition, BuildBlockContextExtra, CmssyBlockContext, CmssyClientConfig, CmssySiteConfig, RawBlock, FieldDefinition } from '@cmssy/types';
3
3
  export { CmssySiteLocales } from '@cmssy/types';
4
- import { G as GraphqlRequestOptions } from './locale-Ox_qVlNy.js';
5
- export { C as CMSSY_LOCALE_HEADER, b as buildLocaleSwitchHref, a as localeForPath, c as localeForPathname, l as localizeHref, d as localizeHtmlLinks, s as splitCmssyLocale } from './locale-Ox_qVlNy.js';
6
- import { Q as QueryScopedOptions } from './csp-DpG1NcSd.js';
7
- export { o as cmssyCspHeaders, q as isDevelopment, s as resolveInitialTarget, t as toCspOrigin } from './csp-DpG1NcSd.js';
8
- export { localesFromSiteConfig, localizedPath, resolveSiteLocales, splitLocaleFromPath } from './internal/locale.js';
4
+ import { G as GraphqlRequestOptions } from './locale-Clouq4s4.js';
5
+ export { C as CMSSY_LOCALE_HEADER, b as buildLocaleSwitchHref, a as localeForPath, c as localeForPathname, d as localesFromSiteConfig, l as localizeHref, e as localizeHtmlLinks, f as resolveSiteLocales, s as splitCmssyLocale, h as splitLocaleFromPath } from './locale-Clouq4s4.js';
6
+ import { Q as QueryScopedOptions } from './csp-CpiqfM03.js';
7
+ export { o as cmssyCspHeaders, q as isDevelopment, s as resolveInitialTarget, t as toCspOrigin } from './csp-CpiqfM03.js';
8
+ export { localizedPath } from './internal/locale.js';
9
9
 
10
10
  declare const SITE_CONFIG_QUERY = "query PublicSiteConfig($workspaceSlug: String!) {\n public {\n siteConfig(workspaceSlug: $workspaceSlug) {\n id\n workspaceId\n siteName\n defaultLanguage\n enabledLanguages\n enabledFeatures\n notFoundPageId\n previewUrl\n branding {\n brandName\n logoUrl\n logoDarkUrl\n faviconUrl\n ogImageUrl\n }\n }\n }\n}";
11
11
  declare const MODEL_DEFINITIONS_QUERY = "query PublicModelDefinitions($workspaceId: String!) {\n public {\n model {\n definitions(workspaceId: $workspaceId) {\n id name slug description icon color displayField recordCount\n }\n }\n }\n}";
package/dist/internal.js CHANGED
@@ -711,6 +711,7 @@ async function resolveForms(config, blocks, locale, defaultLocale, options) {
711
711
 
712
712
  // src/data/site-locales.ts
713
713
  var TTL_MS = 6e4;
714
+ var FAILURE_TTL_MS = 5e3;
714
715
  var MAX_ENTRIES = 64;
715
716
  var cache = /* @__PURE__ */ new Map();
716
717
  function localesFromSiteConfig(siteConfig) {
@@ -721,7 +722,7 @@ function localesFromSiteConfig(siteConfig) {
721
722
  locales: enabled.length > 0 ? enabled : [defaultLocale]
722
723
  };
723
724
  }
724
- async function resolveSiteLocales(config, options) {
725
+ async function loadSiteLocales(config, options) {
725
726
  const key = `${resolveApiUrl(config.apiUrl)}::${config.org}::${config.workspaceSlug}`;
726
727
  const cached = cache.get(key);
727
728
  if (cached && cached.expires > Date.now()) return cached.value;
@@ -737,12 +738,21 @@ async function resolveSiteLocales(config, options) {
737
738
  );
738
739
  value = localesFromSiteConfig(data.public?.siteConfig ?? null);
739
740
  } catch {
740
- value = { defaultLocale: "en", locales: ["en"] };
741
+ value = null;
741
742
  }
742
743
  if (cache.size >= MAX_ENTRIES) cache.clear();
743
- cache.set(key, { value, expires: Date.now() + TTL_MS });
744
+ cache.set(key, {
745
+ value,
746
+ expires: Date.now() + (value ? TTL_MS : FAILURE_TTL_MS)
747
+ });
744
748
  return value;
745
749
  }
750
+ async function resolveSiteLocales(config, options) {
751
+ return await loadSiteLocales(config, options) ?? {
752
+ defaultLocale: "en",
753
+ locales: ["en"]
754
+ };
755
+ }
746
756
  function splitLocaleFromPath(path, siteLocales) {
747
757
  const first = path?.[0];
748
758
  if (first && first !== siteLocales.defaultLocale && siteLocales.locales.includes(first)) {
@@ -1,5 +1,5 @@
1
+ import { CmssyClientConfig, CmssyLocaleContext, CmssySiteLocales } from '@cmssy/types';
1
2
  import { F as FetchLike, R as RetryPolicy } from './content-client-CJQ-nafm.js';
2
- import { CmssyClientConfig, CmssyLocaleContext } from '@cmssy/types';
3
3
 
4
4
  interface GraphqlRequestOptions {
5
5
  fetch?: FetchLike;
@@ -37,6 +37,22 @@ declare function buildLocaleSwitchHref(target: string, pathname: string, locale:
37
37
  */
38
38
  declare function localizeHtmlLinks(html: string, locale: CmssyLocaleContext): string;
39
39
 
40
+ /**
41
+ * Maps a workspace site config to its locale set. The single place that
42
+ * decides the default and enabled languages - the router and the SEO helpers
43
+ * must agree, so both go through here.
44
+ */
45
+ declare function localesFromSiteConfig(siteConfig: {
46
+ defaultLanguage?: string | null;
47
+ enabledLanguages?: string[];
48
+ } | null): CmssySiteLocales;
49
+ declare function resolveSiteLocales(config: CmssyClientConfig, options?: GraphqlRequestOptions): Promise<CmssySiteLocales>;
50
+ declare function resolveCmssyLocale(config: CmssyClientConfig, path: string[] | undefined, options?: GraphqlRequestOptions): Promise<string | undefined>;
51
+ declare function splitLocaleFromPath(path: string[] | undefined, siteLocales: CmssySiteLocales): {
52
+ locale: string;
53
+ path: string[] | undefined;
54
+ };
55
+
40
56
  declare const CMSSY_LOCALE_HEADER = "x-cmssy-locale";
41
57
  declare function localeForPathname(config: CmssyClientConfig, pathname: string): Promise<string>;
42
58
  declare function splitCmssyLocale(config: CmssyClientConfig, path: string[] | undefined): Promise<{
@@ -49,4 +65,4 @@ declare function splitCmssyLocale(config: CmssyClientConfig, path: string[] | un
49
65
  */
50
66
  declare function localeForPath(config: CmssyClientConfig, path: string | string[]): Promise<string>;
51
67
 
52
- export { CMSSY_LOCALE_HEADER as C, type GraphqlRequestOptions as G, localeForPath as a, buildLocaleSwitchHref as b, localeForPathname as c, localizeHtmlLinks as d, graphqlRequest as g, localizeHref as l, splitCmssyLocale as s };
68
+ export { CMSSY_LOCALE_HEADER as C, type GraphqlRequestOptions as G, localeForPath as a, buildLocaleSwitchHref as b, localeForPathname as c, localesFromSiteConfig as d, localizeHtmlLinks as e, resolveSiteLocales as f, graphqlRequest as g, splitLocaleFromPath as h, localizeHref as l, resolveCmssyLocale as r, splitCmssyLocale as s };
@@ -1,5 +1,5 @@
1
+ import { CmssyClientConfig, CmssyLocaleContext, CmssySiteLocales } from '@cmssy/types';
1
2
  import { F as FetchLike, R as RetryPolicy } from './content-client-CJQ-nafm.cjs';
2
- import { CmssyClientConfig, CmssyLocaleContext } from '@cmssy/types';
3
3
 
4
4
  interface GraphqlRequestOptions {
5
5
  fetch?: FetchLike;
@@ -37,6 +37,22 @@ declare function buildLocaleSwitchHref(target: string, pathname: string, locale:
37
37
  */
38
38
  declare function localizeHtmlLinks(html: string, locale: CmssyLocaleContext): string;
39
39
 
40
+ /**
41
+ * Maps a workspace site config to its locale set. The single place that
42
+ * decides the default and enabled languages - the router and the SEO helpers
43
+ * must agree, so both go through here.
44
+ */
45
+ declare function localesFromSiteConfig(siteConfig: {
46
+ defaultLanguage?: string | null;
47
+ enabledLanguages?: string[];
48
+ } | null): CmssySiteLocales;
49
+ declare function resolveSiteLocales(config: CmssyClientConfig, options?: GraphqlRequestOptions): Promise<CmssySiteLocales>;
50
+ declare function resolveCmssyLocale(config: CmssyClientConfig, path: string[] | undefined, options?: GraphqlRequestOptions): Promise<string | undefined>;
51
+ declare function splitLocaleFromPath(path: string[] | undefined, siteLocales: CmssySiteLocales): {
52
+ locale: string;
53
+ path: string[] | undefined;
54
+ };
55
+
40
56
  declare const CMSSY_LOCALE_HEADER = "x-cmssy-locale";
41
57
  declare function localeForPathname(config: CmssyClientConfig, pathname: string): Promise<string>;
42
58
  declare function splitCmssyLocale(config: CmssyClientConfig, path: string[] | undefined): Promise<{
@@ -49,4 +65,4 @@ declare function splitCmssyLocale(config: CmssyClientConfig, path: string[] | un
49
65
  */
50
66
  declare function localeForPath(config: CmssyClientConfig, path: string | string[]): Promise<string>;
51
67
 
52
- export { CMSSY_LOCALE_HEADER as C, type GraphqlRequestOptions as G, localeForPath as a, buildLocaleSwitchHref as b, localeForPathname as c, localizeHtmlLinks as d, graphqlRequest as g, localizeHref as l, splitCmssyLocale as s };
68
+ export { CMSSY_LOCALE_HEADER as C, type GraphqlRequestOptions as G, localeForPath as a, buildLocaleSwitchHref as b, localeForPathname as c, localesFromSiteConfig as d, localizeHtmlLinks as e, resolveSiteLocales as f, graphqlRequest as g, splitLocaleFromPath as h, localizeHref as l, resolveCmssyLocale as r, splitCmssyLocale as s };
package/dist/testing.cjs CHANGED
@@ -141,6 +141,80 @@ async function graphqlRequest(config, query, variables, options = {}, label = "r
141
141
  });
142
142
  }
143
143
 
144
+ // src/data/queries.ts
145
+ var SITE_CONFIG_QUERY = `query PublicSiteConfig($workspaceSlug: String!) {
146
+ public {
147
+ siteConfig(workspaceSlug: $workspaceSlug) {
148
+ id
149
+ workspaceId
150
+ siteName
151
+ defaultLanguage
152
+ enabledLanguages
153
+ enabledFeatures
154
+ notFoundPageId
155
+ previewUrl
156
+ branding {
157
+ brandName
158
+ logoUrl
159
+ logoDarkUrl
160
+ faviconUrl
161
+ ogImageUrl
162
+ }
163
+ }
164
+ }
165
+ }`;
166
+
167
+ // src/data/site-locales.ts
168
+ function localesFromSiteConfig(siteConfig) {
169
+ const defaultLocale = siteConfig?.defaultLanguage || "en";
170
+ const enabled = siteConfig?.enabledLanguages ?? [];
171
+ return {
172
+ defaultLocale,
173
+ locales: enabled.length > 0 ? enabled : [defaultLocale]
174
+ };
175
+ }
176
+
177
+ // src/preflight.ts
178
+ var ALLOWED_FRAME_HOSTS = ["cmssy.io", "www.cmssy.io"];
179
+ function frameAncestorSources(cspHeaderValue) {
180
+ const directive = cspHeaderValue.split(";").map((part) => part.trim()).find((part) => /^frame-ancestors\b/i.test(part));
181
+ if (!directive) return null;
182
+ return directive.split(/\s+/).slice(1).map((source) => source.toLowerCase().replace(/^'|'$/g, ""));
183
+ }
184
+ function allowsCmssyAdmin(source) {
185
+ if (source === "*" || source === "https:") return true;
186
+ const host = source.replace(/^[a-z][a-z0-9+.-]*:\/\//, "").split("/")[0];
187
+ if (!host) return false;
188
+ if (host === "*.cmssy.io") return true;
189
+ return ALLOWED_FRAME_HOSTS.includes(host);
190
+ }
191
+ function checkFrameAncestors(cspHeaderValue) {
192
+ if (!cspHeaderValue || cspHeaderValue.trim().length === 0) {
193
+ return {
194
+ status: "ok",
195
+ message: "no Content-Security-Policy restricts framing"
196
+ };
197
+ }
198
+ const sources = frameAncestorSources(cspHeaderValue);
199
+ if (sources === null) {
200
+ return {
201
+ status: "ok",
202
+ message: "the Content-Security-Policy has no frame-ancestors directive"
203
+ };
204
+ }
205
+ if (sources.some(allowsCmssyAdmin)) {
206
+ return {
207
+ status: "ok",
208
+ message: "frame-ancestors allows the cmssy editor"
209
+ };
210
+ }
211
+ return {
212
+ status: "fail",
213
+ message: `frame-ancestors (${sources.join(" ") || "'none'"}) blocks the cmssy editor`,
214
+ fix: "add https://cmssy.io https://www.cmssy.io to the frame-ancestors directive (applyCmssyCsp does this for you)"
215
+ };
216
+ }
217
+
144
218
  // src/testing/edit-smoke.ts
145
219
  var EDITOR_MARKER = /data-cmssy-editor/;
146
220
  var SERVER_LAYOUT_BLOCKS = /<header|<footer/;
@@ -151,7 +225,22 @@ function resolvedContentCounts(body) {
151
225
  }
152
226
  async function html(url) {
153
227
  const response = await fetch(url, { redirect: "manual" });
154
- return { status: response.status, body: await response.text() };
228
+ return {
229
+ status: response.status,
230
+ body: await response.text(),
231
+ headers: response.headers
232
+ };
233
+ }
234
+ var EDIT_PATH_PREFIX = "/cmssy-edit";
235
+ function withoutEditPrefix(path) {
236
+ if (path === EDIT_PATH_PREFIX) return "/";
237
+ return path.startsWith(`${EDIT_PATH_PREFIX}/`) ? path.slice(EDIT_PATH_PREFIX.length) : path;
238
+ }
239
+ function localeOf(path) {
240
+ return withoutEditPrefix(path).split("/").filter(Boolean)[0];
241
+ }
242
+ function langOf(body) {
243
+ return /<html[^>]*\slang=["']([^"']+)["']/i.exec(body)?.[1];
155
244
  }
156
245
  var LAYOUT_BLOCKS_QUERY = `query CmssySmokeLayouts($workspaceSlug: String!, $pageSlug: String!) {
157
246
  public {
@@ -180,6 +269,29 @@ async function workspaceHasLayoutBlocks(workspace, pageSlug) {
180
269
  return null;
181
270
  }
182
271
  }
272
+ async function derivedLocalizedPath(workspace, path) {
273
+ try {
274
+ const data = await graphqlRequest(
275
+ workspace,
276
+ SITE_CONFIG_QUERY,
277
+ { workspaceSlug: workspace.workspaceSlug },
278
+ { public: true, retry: {} },
279
+ "edit smoke: site locales"
280
+ );
281
+ const { defaultLocale, locales } = localesFromSiteConfig(
282
+ data.public?.siteConfig ?? null
283
+ );
284
+ const locale = locales.find((candidate) => candidate !== defaultLocale);
285
+ if (!locale) return { kind: "none" };
286
+ const routed = withoutEditPrefix(path);
287
+ const [first] = routed.split("/").filter(Boolean);
288
+ if (first && locales.includes(first)) return { kind: "none" };
289
+ const suffix = routed === "/" || routed === "" ? "" : routed.replace(/\/+$/, "");
290
+ return { kind: "derived", locale, localizedPath: `/${locale}${suffix}` };
291
+ } catch {
292
+ return { kind: "unavailable" };
293
+ }
294
+ }
183
295
  async function checkCmssyEditMode(options) {
184
296
  const { baseUrl, secret, path = "/" } = options;
185
297
  const failures = [];
@@ -232,25 +344,62 @@ async function checkCmssyEditMode(options) {
232
344
  );
233
345
  }
234
346
  }
235
- const { localizedPath } = options;
236
- if (localizedPath) {
237
- const locale = options.localizedLocale ?? localizedPath.split("/").filter(Boolean)[0];
238
- const localized = await html(
239
- url(
240
- `${localizedPath}?cmssyEdit=1&cmssySecret=${encodeURIComponent(secret)}`
241
- )
347
+ const query = `cmssyEdit=1&cmssySecret=${encodeURIComponent(secret)}`;
348
+ const asked = options.localizedPath;
349
+ const derived = !asked && options.workspace ? await derivedLocalizedPath(options.workspace, path) : { kind: "none" };
350
+ if (derived.kind === "unavailable") {
351
+ failures.push(
352
+ `the delivery API would not say which languages ${options.workspace?.workspaceSlug} enables, so the preview's language went unchecked. That is the check three releases shipped a wrong-language preview past - a green run here does not mean it works.`
242
353
  );
243
- if (!EDITOR_MARKER.test(localized.body)) {
244
- failures.push(`edit ${localizedPath}: no editor in the response`);
245
- }
246
- const served = /<html[^>]*\slang=["']([^"']+)["']/i.exec(localized.body)?.[1];
247
- if (locale && served !== locale) {
354
+ }
355
+ const localizedPath = asked ?? (derived.kind === "derived" ? derived.localizedPath : void 0);
356
+ if (localizedPath) {
357
+ const locale = options.localizedLocale ?? (derived.kind === "derived" ? derived.locale : localeOf(localizedPath));
358
+ const localized = await html(url(`${localizedPath}?${query}`));
359
+ if (localized.status !== 200) {
248
360
  failures.push(
249
- `edit ${localizedPath}: the page reports lang="${served ?? "?"}" but the URL asks for "${locale}" - the preview renders in the wrong language`
361
+ derived.kind === "derived" ? `edit ${localizedPath}: expected 200, got ${localized.status}. The workspace enables "${locale}", so the language went unchecked - pass localizedPath if this site spells the language some other way.` : `edit ${localizedPath}: expected 200, got ${localized.status}`
250
362
  );
363
+ } else {
364
+ if (!EDITOR_MARKER.test(localized.body)) {
365
+ failures.push(`edit ${localizedPath}: no editor in the response`);
366
+ }
367
+ const served = langOf(localized.body);
368
+ if (locale && served !== locale) {
369
+ failures.push(
370
+ `edit ${localizedPath}: the page reports lang="${served ?? "?"}" but the URL asks for "${locale}" - the preview renders in the wrong language`
371
+ );
372
+ }
373
+ if (options.editRoute !== false) {
374
+ await checkDirectEditRoute(url, localizedPath, query, locale, failures);
375
+ }
251
376
  }
252
377
  }
378
+ if (options.editRoute !== false) {
379
+ await checkDirectEditRoute(url, path, query, void 0, failures);
380
+ }
253
381
  return { ok: failures.length === 0, failures };
254
382
  }
383
+ async function checkDirectEditRoute(url, routedPath, query, locale, failures) {
384
+ const withoutPrefix = withoutEditPrefix(routedPath);
385
+ const directPath = `${EDIT_PATH_PREFIX}${withoutPrefix === "/" ? "" : withoutPrefix}`;
386
+ if (directPath === routedPath) return;
387
+ const direct = await html(url(`${directPath}?${query}`));
388
+ if (direct.status !== 200 || !EDITOR_MARKER.test(direct.body)) return;
389
+ const directLang = langOf(direct.body);
390
+ if (locale && directLang !== locale) {
391
+ failures.push(
392
+ `edit ${directPath}: reached directly the page reports lang="${directLang ?? "?"}", through the rewrite it reports "${locale}" - the same page renders in two languages depending on how the editor arrives`
393
+ );
394
+ }
395
+ const framing = checkFrameAncestors(
396
+ direct.headers.get("content-security-policy")
397
+ );
398
+ if (framing.status === "fail") {
399
+ failures.push(
400
+ `edit ${directPath}: ${framing.message} - the editor shows a blank panel for a route that renders fine on its own. ${framing.fix ?? ""}`.trim()
401
+ );
402
+ }
403
+ }
255
404
 
256
405
  exports.checkCmssyEditMode = checkCmssyEditMode;
@@ -33,6 +33,7 @@ interface EditSmokeOptions {
33
33
  workspaceSlug: string;
34
34
  apiUrl?: string;
35
35
  };
36
+ editRoute?: boolean;
36
37
  }
37
38
  interface EditSmokeResult {
38
39
  ok: boolean;
package/dist/testing.d.ts CHANGED
@@ -33,6 +33,7 @@ interface EditSmokeOptions {
33
33
  workspaceSlug: string;
34
34
  apiUrl?: string;
35
35
  };
36
+ editRoute?: boolean;
36
37
  }
37
38
  interface EditSmokeResult {
38
39
  ok: boolean;
package/dist/testing.js CHANGED
@@ -139,6 +139,80 @@ async function graphqlRequest(config, query, variables, options = {}, label = "r
139
139
  });
140
140
  }
141
141
 
142
+ // src/data/queries.ts
143
+ var SITE_CONFIG_QUERY = `query PublicSiteConfig($workspaceSlug: String!) {
144
+ public {
145
+ siteConfig(workspaceSlug: $workspaceSlug) {
146
+ id
147
+ workspaceId
148
+ siteName
149
+ defaultLanguage
150
+ enabledLanguages
151
+ enabledFeatures
152
+ notFoundPageId
153
+ previewUrl
154
+ branding {
155
+ brandName
156
+ logoUrl
157
+ logoDarkUrl
158
+ faviconUrl
159
+ ogImageUrl
160
+ }
161
+ }
162
+ }
163
+ }`;
164
+
165
+ // src/data/site-locales.ts
166
+ function localesFromSiteConfig(siteConfig) {
167
+ const defaultLocale = siteConfig?.defaultLanguage || "en";
168
+ const enabled = siteConfig?.enabledLanguages ?? [];
169
+ return {
170
+ defaultLocale,
171
+ locales: enabled.length > 0 ? enabled : [defaultLocale]
172
+ };
173
+ }
174
+
175
+ // src/preflight.ts
176
+ var ALLOWED_FRAME_HOSTS = ["cmssy.io", "www.cmssy.io"];
177
+ function frameAncestorSources(cspHeaderValue) {
178
+ const directive = cspHeaderValue.split(";").map((part) => part.trim()).find((part) => /^frame-ancestors\b/i.test(part));
179
+ if (!directive) return null;
180
+ return directive.split(/\s+/).slice(1).map((source) => source.toLowerCase().replace(/^'|'$/g, ""));
181
+ }
182
+ function allowsCmssyAdmin(source) {
183
+ if (source === "*" || source === "https:") return true;
184
+ const host = source.replace(/^[a-z][a-z0-9+.-]*:\/\//, "").split("/")[0];
185
+ if (!host) return false;
186
+ if (host === "*.cmssy.io") return true;
187
+ return ALLOWED_FRAME_HOSTS.includes(host);
188
+ }
189
+ function checkFrameAncestors(cspHeaderValue) {
190
+ if (!cspHeaderValue || cspHeaderValue.trim().length === 0) {
191
+ return {
192
+ status: "ok",
193
+ message: "no Content-Security-Policy restricts framing"
194
+ };
195
+ }
196
+ const sources = frameAncestorSources(cspHeaderValue);
197
+ if (sources === null) {
198
+ return {
199
+ status: "ok",
200
+ message: "the Content-Security-Policy has no frame-ancestors directive"
201
+ };
202
+ }
203
+ if (sources.some(allowsCmssyAdmin)) {
204
+ return {
205
+ status: "ok",
206
+ message: "frame-ancestors allows the cmssy editor"
207
+ };
208
+ }
209
+ return {
210
+ status: "fail",
211
+ message: `frame-ancestors (${sources.join(" ") || "'none'"}) blocks the cmssy editor`,
212
+ fix: "add https://cmssy.io https://www.cmssy.io to the frame-ancestors directive (applyCmssyCsp does this for you)"
213
+ };
214
+ }
215
+
142
216
  // src/testing/edit-smoke.ts
143
217
  var EDITOR_MARKER = /data-cmssy-editor/;
144
218
  var SERVER_LAYOUT_BLOCKS = /<header|<footer/;
@@ -149,7 +223,22 @@ function resolvedContentCounts(body) {
149
223
  }
150
224
  async function html(url) {
151
225
  const response = await fetch(url, { redirect: "manual" });
152
- return { status: response.status, body: await response.text() };
226
+ return {
227
+ status: response.status,
228
+ body: await response.text(),
229
+ headers: response.headers
230
+ };
231
+ }
232
+ var EDIT_PATH_PREFIX = "/cmssy-edit";
233
+ function withoutEditPrefix(path) {
234
+ if (path === EDIT_PATH_PREFIX) return "/";
235
+ return path.startsWith(`${EDIT_PATH_PREFIX}/`) ? path.slice(EDIT_PATH_PREFIX.length) : path;
236
+ }
237
+ function localeOf(path) {
238
+ return withoutEditPrefix(path).split("/").filter(Boolean)[0];
239
+ }
240
+ function langOf(body) {
241
+ return /<html[^>]*\slang=["']([^"']+)["']/i.exec(body)?.[1];
153
242
  }
154
243
  var LAYOUT_BLOCKS_QUERY = `query CmssySmokeLayouts($workspaceSlug: String!, $pageSlug: String!) {
155
244
  public {
@@ -178,6 +267,29 @@ async function workspaceHasLayoutBlocks(workspace, pageSlug) {
178
267
  return null;
179
268
  }
180
269
  }
270
+ async function derivedLocalizedPath(workspace, path) {
271
+ try {
272
+ const data = await graphqlRequest(
273
+ workspace,
274
+ SITE_CONFIG_QUERY,
275
+ { workspaceSlug: workspace.workspaceSlug },
276
+ { public: true, retry: {} },
277
+ "edit smoke: site locales"
278
+ );
279
+ const { defaultLocale, locales } = localesFromSiteConfig(
280
+ data.public?.siteConfig ?? null
281
+ );
282
+ const locale = locales.find((candidate) => candidate !== defaultLocale);
283
+ if (!locale) return { kind: "none" };
284
+ const routed = withoutEditPrefix(path);
285
+ const [first] = routed.split("/").filter(Boolean);
286
+ if (first && locales.includes(first)) return { kind: "none" };
287
+ const suffix = routed === "/" || routed === "" ? "" : routed.replace(/\/+$/, "");
288
+ return { kind: "derived", locale, localizedPath: `/${locale}${suffix}` };
289
+ } catch {
290
+ return { kind: "unavailable" };
291
+ }
292
+ }
181
293
  async function checkCmssyEditMode(options) {
182
294
  const { baseUrl, secret, path = "/" } = options;
183
295
  const failures = [];
@@ -230,25 +342,62 @@ async function checkCmssyEditMode(options) {
230
342
  );
231
343
  }
232
344
  }
233
- const { localizedPath } = options;
234
- if (localizedPath) {
235
- const locale = options.localizedLocale ?? localizedPath.split("/").filter(Boolean)[0];
236
- const localized = await html(
237
- url(
238
- `${localizedPath}?cmssyEdit=1&cmssySecret=${encodeURIComponent(secret)}`
239
- )
345
+ const query = `cmssyEdit=1&cmssySecret=${encodeURIComponent(secret)}`;
346
+ const asked = options.localizedPath;
347
+ const derived = !asked && options.workspace ? await derivedLocalizedPath(options.workspace, path) : { kind: "none" };
348
+ if (derived.kind === "unavailable") {
349
+ failures.push(
350
+ `the delivery API would not say which languages ${options.workspace?.workspaceSlug} enables, so the preview's language went unchecked. That is the check three releases shipped a wrong-language preview past - a green run here does not mean it works.`
240
351
  );
241
- if (!EDITOR_MARKER.test(localized.body)) {
242
- failures.push(`edit ${localizedPath}: no editor in the response`);
243
- }
244
- const served = /<html[^>]*\slang=["']([^"']+)["']/i.exec(localized.body)?.[1];
245
- if (locale && served !== locale) {
352
+ }
353
+ const localizedPath = asked ?? (derived.kind === "derived" ? derived.localizedPath : void 0);
354
+ if (localizedPath) {
355
+ const locale = options.localizedLocale ?? (derived.kind === "derived" ? derived.locale : localeOf(localizedPath));
356
+ const localized = await html(url(`${localizedPath}?${query}`));
357
+ if (localized.status !== 200) {
246
358
  failures.push(
247
- `edit ${localizedPath}: the page reports lang="${served ?? "?"}" but the URL asks for "${locale}" - the preview renders in the wrong language`
359
+ derived.kind === "derived" ? `edit ${localizedPath}: expected 200, got ${localized.status}. The workspace enables "${locale}", so the language went unchecked - pass localizedPath if this site spells the language some other way.` : `edit ${localizedPath}: expected 200, got ${localized.status}`
248
360
  );
361
+ } else {
362
+ if (!EDITOR_MARKER.test(localized.body)) {
363
+ failures.push(`edit ${localizedPath}: no editor in the response`);
364
+ }
365
+ const served = langOf(localized.body);
366
+ if (locale && served !== locale) {
367
+ failures.push(
368
+ `edit ${localizedPath}: the page reports lang="${served ?? "?"}" but the URL asks for "${locale}" - the preview renders in the wrong language`
369
+ );
370
+ }
371
+ if (options.editRoute !== false) {
372
+ await checkDirectEditRoute(url, localizedPath, query, locale, failures);
373
+ }
249
374
  }
250
375
  }
376
+ if (options.editRoute !== false) {
377
+ await checkDirectEditRoute(url, path, query, void 0, failures);
378
+ }
251
379
  return { ok: failures.length === 0, failures };
252
380
  }
381
+ async function checkDirectEditRoute(url, routedPath, query, locale, failures) {
382
+ const withoutPrefix = withoutEditPrefix(routedPath);
383
+ const directPath = `${EDIT_PATH_PREFIX}${withoutPrefix === "/" ? "" : withoutPrefix}`;
384
+ if (directPath === routedPath) return;
385
+ const direct = await html(url(`${directPath}?${query}`));
386
+ if (direct.status !== 200 || !EDITOR_MARKER.test(direct.body)) return;
387
+ const directLang = langOf(direct.body);
388
+ if (locale && directLang !== locale) {
389
+ failures.push(
390
+ `edit ${directPath}: reached directly the page reports lang="${directLang ?? "?"}", through the rewrite it reports "${locale}" - the same page renders in two languages depending on how the editor arrives`
391
+ );
392
+ }
393
+ const framing = checkFrameAncestors(
394
+ direct.headers.get("content-security-policy")
395
+ );
396
+ if (framing.status === "fail") {
397
+ failures.push(
398
+ `edit ${directPath}: ${framing.message} - the editor shows a blank panel for a route that renders fine on its own. ${framing.fix ?? ""}`.trim()
399
+ );
400
+ }
401
+ }
253
402
 
254
403
  export { checkCmssyEditMode };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmssy/core",
3
- "version": "11.4.1",
3
+ "version": "11.5.0",
4
4
  "description": "Framework-agnostic cmssy gateway: GraphQL client, block/field system, editor protocol. No React, no Next.",
5
5
  "keywords": [
6
6
  "cmssy",