@cmssy/core 11.4.2 → 11.5.1
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/{csp-CRixC_wl.d.cts → csp-0D9pcuDp.d.cts} +1 -1
- package/dist/{csp-DpG1NcSd.d.ts → csp-CpiqfM03.d.ts} +1 -1
- package/dist/index.cjs +52 -0
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +52 -1
- package/dist/internal/locale.cjs +13 -3
- package/dist/internal/locale.d.cts +2 -19
- package/dist/internal/locale.d.ts +2 -19
- package/dist/internal/locale.js +13 -3
- package/dist/internal.cjs +13 -3
- package/dist/internal.d.cts +5 -5
- package/dist/internal.d.ts +5 -5
- package/dist/internal.js +13 -3
- package/dist/{locale-Ox_qVlNy.d.ts → locale-Clouq4s4.d.ts} +18 -2
- package/dist/{locale-C7sA46qu.d.cts → locale-XMylB_kz.d.cts} +18 -2
- package/dist/testing.cjs +99 -13
- package/dist/testing.d.cts +8 -5
- package/dist/testing.d.ts +8 -5
- package/dist/testing.js +99 -13
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CmssyClientConfig, BlockRect, BlockSchema, BlockMeta } from '@cmssy/types';
|
|
2
|
-
import { G as GraphqlRequestOptions } from './locale-
|
|
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-
|
|
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-
|
|
2
|
-
export { C as CMSSY_LOCALE_HEADER, G as GraphqlRequestOptions, g as graphqlRequest, l as localizeHref } from './locale-
|
|
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-
|
|
2
|
-
export { C as CMSSY_LOCALE_HEADER, G as GraphqlRequestOptions, g as graphqlRequest, l as localizeHref } from './locale-
|
|
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 };
|
package/dist/internal/locale.cjs
CHANGED
|
@@ -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
|
|
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 =
|
|
196
|
+
value = null;
|
|
196
197
|
}
|
|
197
198
|
if (cache.size >= MAX_ENTRIES) cache.clear();
|
|
198
|
-
cache.set(key, {
|
|
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
|
-
|
|
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 {
|
|
11
|
+
export { localizedPath };
|
|
@@ -1,28 +1,11 @@
|
|
|
1
|
-
|
|
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 {
|
|
11
|
+
export { localizedPath };
|
package/dist/internal/locale.js
CHANGED
|
@@ -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
|
|
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 =
|
|
194
|
+
value = null;
|
|
194
195
|
}
|
|
195
196
|
if (cache.size >= MAX_ENTRIES) cache.clear();
|
|
196
|
-
cache.set(key, {
|
|
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
|
|
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 =
|
|
743
|
+
value = null;
|
|
743
744
|
}
|
|
744
745
|
if (cache.size >= MAX_ENTRIES) cache.clear();
|
|
745
|
-
cache.set(key, {
|
|
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)) {
|
package/dist/internal.d.cts
CHANGED
|
@@ -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-
|
|
5
|
-
export { C as CMSSY_LOCALE_HEADER, b as buildLocaleSwitchHref, a as localeForPath, c as localeForPathname, l as localizeHref,
|
|
6
|
-
import { Q as QueryScopedOptions } from './csp-
|
|
7
|
-
export { o as cmssyCspHeaders, q as isDevelopment, s as resolveInitialTarget, t as toCspOrigin } from './csp-
|
|
8
|
-
export {
|
|
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}";
|
package/dist/internal.d.ts
CHANGED
|
@@ -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-
|
|
5
|
-
export { C as CMSSY_LOCALE_HEADER, b as buildLocaleSwitchHref, a as localeForPath, c as localeForPathname, l as localizeHref,
|
|
6
|
-
import { Q as QueryScopedOptions } from './csp-
|
|
7
|
-
export { o as cmssyCspHeaders, q as isDevelopment, s as resolveInitialTarget, t as toCspOrigin } from './csp-
|
|
8
|
-
export {
|
|
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
|
|
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 =
|
|
741
|
+
value = null;
|
|
741
742
|
}
|
|
742
743
|
if (cache.size >= MAX_ENTRIES) cache.clear();
|
|
743
|
-
cache.set(key, {
|
|
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,
|
|
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,
|
|
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,47 @@ async function graphqlRequest(config, query, variables, options = {}, label = "r
|
|
|
141
141
|
});
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
// src/preflight.ts
|
|
145
|
+
var ALLOWED_FRAME_HOSTS = ["cmssy.io", "www.cmssy.io"];
|
|
146
|
+
function frameAncestorSources(cspHeaderValue) {
|
|
147
|
+
const directive = cspHeaderValue.split(";").map((part) => part.trim()).find((part) => /^frame-ancestors\b/i.test(part));
|
|
148
|
+
if (!directive) return null;
|
|
149
|
+
return directive.split(/\s+/).slice(1).map((source) => source.toLowerCase().replace(/^'|'$/g, ""));
|
|
150
|
+
}
|
|
151
|
+
function allowsCmssyAdmin(source) {
|
|
152
|
+
if (source === "*" || source === "https:") return true;
|
|
153
|
+
const host = source.replace(/^[a-z][a-z0-9+.-]*:\/\//, "").split("/")[0];
|
|
154
|
+
if (!host) return false;
|
|
155
|
+
if (host === "*.cmssy.io") return true;
|
|
156
|
+
return ALLOWED_FRAME_HOSTS.includes(host);
|
|
157
|
+
}
|
|
158
|
+
function checkFrameAncestors(cspHeaderValue) {
|
|
159
|
+
if (!cspHeaderValue || cspHeaderValue.trim().length === 0) {
|
|
160
|
+
return {
|
|
161
|
+
status: "ok",
|
|
162
|
+
message: "no Content-Security-Policy restricts framing"
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
const sources = frameAncestorSources(cspHeaderValue);
|
|
166
|
+
if (sources === null) {
|
|
167
|
+
return {
|
|
168
|
+
status: "ok",
|
|
169
|
+
message: "the Content-Security-Policy has no frame-ancestors directive"
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
if (sources.some(allowsCmssyAdmin)) {
|
|
173
|
+
return {
|
|
174
|
+
status: "ok",
|
|
175
|
+
message: "frame-ancestors allows the cmssy editor"
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
status: "fail",
|
|
180
|
+
message: `frame-ancestors (${sources.join(" ") || "'none'"}) blocks the cmssy editor`,
|
|
181
|
+
fix: "add https://cmssy.io https://www.cmssy.io to the frame-ancestors directive (applyCmssyCsp does this for you)"
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
144
185
|
// src/testing/edit-smoke.ts
|
|
145
186
|
var EDITOR_MARKER = /data-cmssy-editor/;
|
|
146
187
|
var SERVER_LAYOUT_BLOCKS = /<header|<footer/;
|
|
@@ -151,7 +192,22 @@ function resolvedContentCounts(body) {
|
|
|
151
192
|
}
|
|
152
193
|
async function html(url) {
|
|
153
194
|
const response = await fetch(url, { redirect: "manual" });
|
|
154
|
-
return {
|
|
195
|
+
return {
|
|
196
|
+
status: response.status,
|
|
197
|
+
body: await response.text(),
|
|
198
|
+
headers: response.headers
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
var EDIT_PATH_PREFIX = "/cmssy-edit";
|
|
202
|
+
function withoutEditPrefix(path) {
|
|
203
|
+
if (path === EDIT_PATH_PREFIX) return "/";
|
|
204
|
+
return path.startsWith(`${EDIT_PATH_PREFIX}/`) ? path.slice(EDIT_PATH_PREFIX.length) : path;
|
|
205
|
+
}
|
|
206
|
+
function localeOf(path) {
|
|
207
|
+
return withoutEditPrefix(path).split("/").filter(Boolean)[0];
|
|
208
|
+
}
|
|
209
|
+
function langOf(body) {
|
|
210
|
+
return /<html[^>]*\slang=["']([^"']+)["']/i.exec(body)?.[1];
|
|
155
211
|
}
|
|
156
212
|
var LAYOUT_BLOCKS_QUERY = `query CmssySmokeLayouts($workspaceSlug: String!, $pageSlug: String!) {
|
|
157
213
|
public {
|
|
@@ -232,25 +288,55 @@ async function checkCmssyEditMode(options) {
|
|
|
232
288
|
);
|
|
233
289
|
}
|
|
234
290
|
}
|
|
291
|
+
const query = `cmssyEdit=1&cmssySecret=${encodeURIComponent(secret)}`;
|
|
235
292
|
const { localizedPath } = options;
|
|
236
293
|
if (localizedPath) {
|
|
237
|
-
const locale = options.localizedLocale ?? localizedPath
|
|
238
|
-
const localized = await html(
|
|
239
|
-
|
|
240
|
-
`${localizedPath}?cmssyEdit=1&cmssySecret=${encodeURIComponent(secret)}`
|
|
241
|
-
)
|
|
242
|
-
);
|
|
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) {
|
|
294
|
+
const locale = options.localizedLocale ?? localeOf(localizedPath);
|
|
295
|
+
const localized = await html(url(`${localizedPath}?${query}`));
|
|
296
|
+
if (localized.status !== 200) {
|
|
248
297
|
failures.push(
|
|
249
|
-
`edit ${localizedPath}:
|
|
298
|
+
`edit ${localizedPath}: expected 200, got ${localized.status}`
|
|
250
299
|
);
|
|
300
|
+
} else {
|
|
301
|
+
if (!EDITOR_MARKER.test(localized.body)) {
|
|
302
|
+
failures.push(`edit ${localizedPath}: no editor in the response`);
|
|
303
|
+
}
|
|
304
|
+
const served = langOf(localized.body);
|
|
305
|
+
if (locale && served !== locale) {
|
|
306
|
+
failures.push(
|
|
307
|
+
`edit ${localizedPath}: the page reports lang="${served ?? "?"}" but the URL asks for "${locale}" - the preview renders in the wrong language`
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
if (options.editRoute !== false) {
|
|
311
|
+
await checkDirectEditRoute(url, localizedPath, query, locale, failures);
|
|
312
|
+
}
|
|
251
313
|
}
|
|
252
314
|
}
|
|
315
|
+
if (options.editRoute !== false) {
|
|
316
|
+
await checkDirectEditRoute(url, path, query, void 0, failures);
|
|
317
|
+
}
|
|
253
318
|
return { ok: failures.length === 0, failures };
|
|
254
319
|
}
|
|
320
|
+
async function checkDirectEditRoute(url, routedPath, query, locale, failures) {
|
|
321
|
+
const withoutPrefix = withoutEditPrefix(routedPath);
|
|
322
|
+
const directPath = `${EDIT_PATH_PREFIX}${withoutPrefix === "/" ? "" : withoutPrefix}`;
|
|
323
|
+
if (directPath === routedPath) return;
|
|
324
|
+
const direct = await html(url(`${directPath}?${query}`));
|
|
325
|
+
if (direct.status !== 200 || !EDITOR_MARKER.test(direct.body)) return;
|
|
326
|
+
const directLang = langOf(direct.body);
|
|
327
|
+
if (locale && directLang !== locale) {
|
|
328
|
+
failures.push(
|
|
329
|
+
`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`
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
const framing = checkFrameAncestors(
|
|
333
|
+
direct.headers.get("content-security-policy")
|
|
334
|
+
);
|
|
335
|
+
if (framing.status === "fail") {
|
|
336
|
+
failures.push(
|
|
337
|
+
`edit ${directPath}: ${framing.message} - the editor shows a blank panel for a route that renders fine on its own. ${framing.fix ?? ""}`.trim()
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
255
341
|
|
|
256
342
|
exports.checkCmssyEditMode = checkCmssyEditMode;
|
package/dist/testing.d.cts
CHANGED
|
@@ -6,11 +6,13 @@ interface EditSmokeOptions {
|
|
|
6
6
|
/** A published page to exercise. Defaults to "/". */
|
|
7
7
|
path?: string;
|
|
8
8
|
/**
|
|
9
|
-
* The same page under a language prefix, e.g. "/no".
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* The same page under a language prefix, e.g. "/no". The caller states it
|
|
10
|
+
* because only the caller knows how its site spells a language; asking a
|
|
11
|
+
* workspace would tie this check to content that can change or be deleted.
|
|
12
|
+
*
|
|
13
|
+
* Given it, the check also proves the preview renders in THAT language rather
|
|
14
|
+
* than the default one - by reading `<html lang>`, which is a contract,
|
|
15
|
+
* unlike a word from the page's copy that an editor can change at any time.
|
|
14
16
|
*/
|
|
15
17
|
localizedPath?: string;
|
|
16
18
|
/**
|
|
@@ -33,6 +35,7 @@ interface EditSmokeOptions {
|
|
|
33
35
|
workspaceSlug: string;
|
|
34
36
|
apiUrl?: string;
|
|
35
37
|
};
|
|
38
|
+
editRoute?: boolean;
|
|
36
39
|
}
|
|
37
40
|
interface EditSmokeResult {
|
|
38
41
|
ok: boolean;
|
package/dist/testing.d.ts
CHANGED
|
@@ -6,11 +6,13 @@ interface EditSmokeOptions {
|
|
|
6
6
|
/** A published page to exercise. Defaults to "/". */
|
|
7
7
|
path?: string;
|
|
8
8
|
/**
|
|
9
|
-
* The same page under a language prefix, e.g. "/no".
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* The same page under a language prefix, e.g. "/no". The caller states it
|
|
10
|
+
* because only the caller knows how its site spells a language; asking a
|
|
11
|
+
* workspace would tie this check to content that can change or be deleted.
|
|
12
|
+
*
|
|
13
|
+
* Given it, the check also proves the preview renders in THAT language rather
|
|
14
|
+
* than the default one - by reading `<html lang>`, which is a contract,
|
|
15
|
+
* unlike a word from the page's copy that an editor can change at any time.
|
|
14
16
|
*/
|
|
15
17
|
localizedPath?: string;
|
|
16
18
|
/**
|
|
@@ -33,6 +35,7 @@ interface EditSmokeOptions {
|
|
|
33
35
|
workspaceSlug: string;
|
|
34
36
|
apiUrl?: string;
|
|
35
37
|
};
|
|
38
|
+
editRoute?: boolean;
|
|
36
39
|
}
|
|
37
40
|
interface EditSmokeResult {
|
|
38
41
|
ok: boolean;
|
package/dist/testing.js
CHANGED
|
@@ -139,6 +139,47 @@ async function graphqlRequest(config, query, variables, options = {}, label = "r
|
|
|
139
139
|
});
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
// src/preflight.ts
|
|
143
|
+
var ALLOWED_FRAME_HOSTS = ["cmssy.io", "www.cmssy.io"];
|
|
144
|
+
function frameAncestorSources(cspHeaderValue) {
|
|
145
|
+
const directive = cspHeaderValue.split(";").map((part) => part.trim()).find((part) => /^frame-ancestors\b/i.test(part));
|
|
146
|
+
if (!directive) return null;
|
|
147
|
+
return directive.split(/\s+/).slice(1).map((source) => source.toLowerCase().replace(/^'|'$/g, ""));
|
|
148
|
+
}
|
|
149
|
+
function allowsCmssyAdmin(source) {
|
|
150
|
+
if (source === "*" || source === "https:") return true;
|
|
151
|
+
const host = source.replace(/^[a-z][a-z0-9+.-]*:\/\//, "").split("/")[0];
|
|
152
|
+
if (!host) return false;
|
|
153
|
+
if (host === "*.cmssy.io") return true;
|
|
154
|
+
return ALLOWED_FRAME_HOSTS.includes(host);
|
|
155
|
+
}
|
|
156
|
+
function checkFrameAncestors(cspHeaderValue) {
|
|
157
|
+
if (!cspHeaderValue || cspHeaderValue.trim().length === 0) {
|
|
158
|
+
return {
|
|
159
|
+
status: "ok",
|
|
160
|
+
message: "no Content-Security-Policy restricts framing"
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
const sources = frameAncestorSources(cspHeaderValue);
|
|
164
|
+
if (sources === null) {
|
|
165
|
+
return {
|
|
166
|
+
status: "ok",
|
|
167
|
+
message: "the Content-Security-Policy has no frame-ancestors directive"
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
if (sources.some(allowsCmssyAdmin)) {
|
|
171
|
+
return {
|
|
172
|
+
status: "ok",
|
|
173
|
+
message: "frame-ancestors allows the cmssy editor"
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
status: "fail",
|
|
178
|
+
message: `frame-ancestors (${sources.join(" ") || "'none'"}) blocks the cmssy editor`,
|
|
179
|
+
fix: "add https://cmssy.io https://www.cmssy.io to the frame-ancestors directive (applyCmssyCsp does this for you)"
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
142
183
|
// src/testing/edit-smoke.ts
|
|
143
184
|
var EDITOR_MARKER = /data-cmssy-editor/;
|
|
144
185
|
var SERVER_LAYOUT_BLOCKS = /<header|<footer/;
|
|
@@ -149,7 +190,22 @@ function resolvedContentCounts(body) {
|
|
|
149
190
|
}
|
|
150
191
|
async function html(url) {
|
|
151
192
|
const response = await fetch(url, { redirect: "manual" });
|
|
152
|
-
return {
|
|
193
|
+
return {
|
|
194
|
+
status: response.status,
|
|
195
|
+
body: await response.text(),
|
|
196
|
+
headers: response.headers
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
var EDIT_PATH_PREFIX = "/cmssy-edit";
|
|
200
|
+
function withoutEditPrefix(path) {
|
|
201
|
+
if (path === EDIT_PATH_PREFIX) return "/";
|
|
202
|
+
return path.startsWith(`${EDIT_PATH_PREFIX}/`) ? path.slice(EDIT_PATH_PREFIX.length) : path;
|
|
203
|
+
}
|
|
204
|
+
function localeOf(path) {
|
|
205
|
+
return withoutEditPrefix(path).split("/").filter(Boolean)[0];
|
|
206
|
+
}
|
|
207
|
+
function langOf(body) {
|
|
208
|
+
return /<html[^>]*\slang=["']([^"']+)["']/i.exec(body)?.[1];
|
|
153
209
|
}
|
|
154
210
|
var LAYOUT_BLOCKS_QUERY = `query CmssySmokeLayouts($workspaceSlug: String!, $pageSlug: String!) {
|
|
155
211
|
public {
|
|
@@ -230,25 +286,55 @@ async function checkCmssyEditMode(options) {
|
|
|
230
286
|
);
|
|
231
287
|
}
|
|
232
288
|
}
|
|
289
|
+
const query = `cmssyEdit=1&cmssySecret=${encodeURIComponent(secret)}`;
|
|
233
290
|
const { localizedPath } = options;
|
|
234
291
|
if (localizedPath) {
|
|
235
|
-
const locale = options.localizedLocale ?? localizedPath
|
|
236
|
-
const localized = await html(
|
|
237
|
-
|
|
238
|
-
`${localizedPath}?cmssyEdit=1&cmssySecret=${encodeURIComponent(secret)}`
|
|
239
|
-
)
|
|
240
|
-
);
|
|
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) {
|
|
292
|
+
const locale = options.localizedLocale ?? localeOf(localizedPath);
|
|
293
|
+
const localized = await html(url(`${localizedPath}?${query}`));
|
|
294
|
+
if (localized.status !== 200) {
|
|
246
295
|
failures.push(
|
|
247
|
-
`edit ${localizedPath}:
|
|
296
|
+
`edit ${localizedPath}: expected 200, got ${localized.status}`
|
|
248
297
|
);
|
|
298
|
+
} else {
|
|
299
|
+
if (!EDITOR_MARKER.test(localized.body)) {
|
|
300
|
+
failures.push(`edit ${localizedPath}: no editor in the response`);
|
|
301
|
+
}
|
|
302
|
+
const served = langOf(localized.body);
|
|
303
|
+
if (locale && served !== locale) {
|
|
304
|
+
failures.push(
|
|
305
|
+
`edit ${localizedPath}: the page reports lang="${served ?? "?"}" but the URL asks for "${locale}" - the preview renders in the wrong language`
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
if (options.editRoute !== false) {
|
|
309
|
+
await checkDirectEditRoute(url, localizedPath, query, locale, failures);
|
|
310
|
+
}
|
|
249
311
|
}
|
|
250
312
|
}
|
|
313
|
+
if (options.editRoute !== false) {
|
|
314
|
+
await checkDirectEditRoute(url, path, query, void 0, failures);
|
|
315
|
+
}
|
|
251
316
|
return { ok: failures.length === 0, failures };
|
|
252
317
|
}
|
|
318
|
+
async function checkDirectEditRoute(url, routedPath, query, locale, failures) {
|
|
319
|
+
const withoutPrefix = withoutEditPrefix(routedPath);
|
|
320
|
+
const directPath = `${EDIT_PATH_PREFIX}${withoutPrefix === "/" ? "" : withoutPrefix}`;
|
|
321
|
+
if (directPath === routedPath) return;
|
|
322
|
+
const direct = await html(url(`${directPath}?${query}`));
|
|
323
|
+
if (direct.status !== 200 || !EDITOR_MARKER.test(direct.body)) return;
|
|
324
|
+
const directLang = langOf(direct.body);
|
|
325
|
+
if (locale && directLang !== locale) {
|
|
326
|
+
failures.push(
|
|
327
|
+
`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`
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
const framing = checkFrameAncestors(
|
|
331
|
+
direct.headers.get("content-security-policy")
|
|
332
|
+
);
|
|
333
|
+
if (framing.status === "fail") {
|
|
334
|
+
failures.push(
|
|
335
|
+
`edit ${directPath}: ${framing.message} - the editor shows a blank panel for a route that renders fine on its own. ${framing.fix ?? ""}`.trim()
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
253
339
|
|
|
254
340
|
export { checkCmssyEditMode };
|