includio-cms 0.13.2 → 0.13.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/ROADMAP.md +19 -2
- package/dist/admin/api/handler.js +2 -0
- package/dist/admin/api/media-gc.js +8 -3
- package/dist/admin/api/regenerate-posters.d.ts +2 -0
- package/dist/admin/api/regenerate-posters.js +32 -0
- package/dist/admin/api/replace.js +4 -0
- package/dist/admin/api/rest/middleware/apiKey.js +7 -1
- package/dist/admin/api/upload.js +4 -0
- package/dist/admin/client/collection/collection-entries.svelte +8 -4
- package/dist/admin/client/collection/grid-view.svelte +1 -1
- package/dist/admin/client/entry/entry-header.svelte +37 -44
- package/dist/admin/client/entry/entry-header.svelte.d.ts +1 -2
- package/dist/admin/client/entry/entry-version.svelte +9 -3
- package/dist/admin/client/entry/entry.svelte +20 -1
- package/dist/admin/client/maintenance/maintenance-page.svelte +153 -0
- package/dist/admin/components/fields/seo-field.svelte +30 -16
- package/dist/admin/remote/entry.remote.js +3 -4
- package/dist/admin/state/content-language.svelte.d.ts +0 -3
- package/dist/admin/state/content-language.svelte.js +7 -11
- package/dist/admin/utils/entryLabel.js +2 -3
- package/dist/cms/runtime/api.d.ts +5 -0
- package/dist/cms/runtime/types.d.ts +13 -8
- package/dist/core/cms.js +3 -0
- package/dist/core/fields/layoutUtils.d.ts +2 -2
- package/dist/core/fields/layoutUtils.js +3 -10
- package/dist/core/server/entries/operations/get.js +2 -2
- package/dist/core/server/media/mimeBlocklist.d.ts +1 -0
- package/dist/core/server/media/mimeBlocklist.js +31 -0
- package/dist/core/server/media/operations/batchRegenerateVideoPosters.d.ts +15 -0
- package/dist/core/server/media/operations/batchRegenerateVideoPosters.js +112 -0
- package/dist/files-local/index.d.ts +1 -0
- package/dist/files-local/index.js +3 -140
- package/dist/files-local/sanitizeFilename.js +2 -1
- package/dist/files-local/video.d.ts +9 -0
- package/dist/files-local/video.js +145 -0
- package/dist/paraglide/messages/_index.d.ts +3 -36
- package/dist/paraglide/messages/_index.js +3 -71
- package/dist/paraglide/messages/hello_world.d.ts +5 -0
- package/dist/paraglide/messages/hello_world.js +33 -0
- package/dist/paraglide/messages/login_hello.d.ts +16 -0
- package/dist/paraglide/messages/login_hello.js +34 -0
- package/dist/paraglide/messages/login_please_login.d.ts +16 -0
- package/dist/paraglide/messages/login_please_login.js +34 -0
- package/dist/sveltekit/server/handle.js +8 -0
- package/dist/updates/0.13.3/index.d.ts +2 -0
- package/dist/updates/0.13.3/index.js +21 -0
- package/dist/updates/0.13.4/index.d.ts +2 -0
- package/dist/updates/0.13.4/index.js +14 -0
- package/dist/updates/index.js +3 -1
- package/package.json +1 -1
- package/dist/admin/utils/translationStatus.d.ts +0 -17
- package/dist/admin/utils/translationStatus.js +0 -133
- package/dist/demo/reset.d.ts +0 -1
- package/dist/demo/reset.js +0 -26
- package/dist/paraglide/messages/en.d.ts +0 -5
- package/dist/paraglide/messages/en.js +0 -14
- package/dist/paraglide/messages/pl.d.ts +0 -5
- package/dist/paraglide/messages/pl.js +0 -14
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
import { getLocale, trackMessageCall, experimentalMiddlewareLocaleSplitting, isServer } from '../runtime.js';
|
|
3
|
+
|
|
4
|
+
const en_login_please_login = /** @type {(inputs: {}) => string} */ () => {
|
|
5
|
+
return `Login to your account`
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const pl_login_please_login = /** @type {(inputs: {}) => string} */ () => {
|
|
9
|
+
return `Zaloguj się na swoje konto`
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
|
|
14
|
+
*
|
|
15
|
+
* - Changing this function will be over-written by the next build.
|
|
16
|
+
*
|
|
17
|
+
* - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
|
|
18
|
+
* use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
|
|
19
|
+
*
|
|
20
|
+
* @param {{}} inputs
|
|
21
|
+
* @param {{ locale?: "en" | "pl" }} options
|
|
22
|
+
* @returns {string}
|
|
23
|
+
*/
|
|
24
|
+
/* @__NO_SIDE_EFFECTS__ */
|
|
25
|
+
const login_please_login = (inputs = {}, options = {}) => {
|
|
26
|
+
if (experimentalMiddlewareLocaleSplitting && isServer === false) {
|
|
27
|
+
return /** @type {any} */ (globalThis).__paraglide_ssr.login_please_login(inputs)
|
|
28
|
+
}
|
|
29
|
+
const locale = options.locale ?? getLocale()
|
|
30
|
+
trackMessageCall("login_please_login", locale)
|
|
31
|
+
if (locale === "en") return en_login_please_login(inputs)
|
|
32
|
+
return pl_login_please_login(inputs)
|
|
33
|
+
};
|
|
34
|
+
export { login_please_login as "login.please_login" }
|
|
@@ -65,5 +65,13 @@ export function includioCMS(cmsConfig) {
|
|
|
65
65
|
handles.push(handleAuth);
|
|
66
66
|
}
|
|
67
67
|
handles.push(adminGuard);
|
|
68
|
+
handles.push(securityHeaders);
|
|
68
69
|
return handles;
|
|
69
70
|
}
|
|
71
|
+
const securityHeaders = async ({ event, resolve }) => {
|
|
72
|
+
const response = await resolve(event);
|
|
73
|
+
response.headers.set('X-Content-Type-Options', 'nosniff');
|
|
74
|
+
response.headers.set('X-Frame-Options', 'DENY');
|
|
75
|
+
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
76
|
+
return response;
|
|
77
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const update = {
|
|
2
|
+
version: '0.13.3',
|
|
3
|
+
date: '2026-03-23',
|
|
4
|
+
description: 'Security hardening, per-language content fixes',
|
|
5
|
+
features: [
|
|
6
|
+
'Timing-safe API key comparison (prevents timing attacks)',
|
|
7
|
+
'MIME type blocklist on upload and file replace endpoints',
|
|
8
|
+
'Security headers: X-Content-Type-Options, X-Frame-Options, Referrer-Policy',
|
|
9
|
+
'CMS constructor validates that at least one language is configured'
|
|
10
|
+
],
|
|
11
|
+
fixes: [
|
|
12
|
+
'Form submission rate limiting applies to all requests (not just multipart uploads)',
|
|
13
|
+
'Form submission uses SvelteKit getClientAddress() instead of spoofable x-forwarded-for header',
|
|
14
|
+
'Removed demo reset endpoint with hardcoded fallback secret',
|
|
15
|
+
'Added .catch() handlers for unhandled promise rejections in admin components',
|
|
16
|
+
'Per-language content language state and entry label fixes'
|
|
17
|
+
],
|
|
18
|
+
breakingChanges: [
|
|
19
|
+
'CMS config with empty languages array now throws an error at initialization'
|
|
20
|
+
]
|
|
21
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const update = {
|
|
2
|
+
version: '0.13.4',
|
|
3
|
+
date: '2026-03-24',
|
|
4
|
+
description: 'Video poster regeneration, filename sanitization',
|
|
5
|
+
features: [
|
|
6
|
+
'Batch regenerate video posters & thumbnails from admin maintenance page with SSE progress',
|
|
7
|
+
'Video poster status reporting in media GC endpoint',
|
|
8
|
+
'Extracted video processing to reusable module'
|
|
9
|
+
],
|
|
10
|
+
fixes: [
|
|
11
|
+
'Filename sanitization normalizes Unicode to NFC (prevents NFD/NFC mismatch)'
|
|
12
|
+
],
|
|
13
|
+
breakingChanges: []
|
|
14
|
+
};
|
package/dist/updates/index.js
CHANGED
|
@@ -35,7 +35,9 @@ import { update as update0120 } from './0.12.0/index.js';
|
|
|
35
35
|
import { update as update0130 } from './0.13.0/index.js';
|
|
36
36
|
import { update as update0131 } from './0.13.1/index.js';
|
|
37
37
|
import { update as update0132 } from './0.13.2/index.js';
|
|
38
|
-
|
|
38
|
+
import { update as update0133 } from './0.13.3/index.js';
|
|
39
|
+
import { update as update0134 } from './0.13.4/index.js';
|
|
40
|
+
export const updates = [update0065, update0066, update0067, update0068, update0069, update010, update011, update012, update013, update014, update015, update020, update022, update050, update051, update052, update053, update054, update055, update056, update057, update058, update060, update061, update062, update070, update071, update072, update073, update080, update090, update0100, update0110, update0120, update0130, update0131, update0132, update0133, update0134];
|
|
39
41
|
export const getUpdatesFrom = (fromVersion) => {
|
|
40
42
|
const fromParts = fromVersion.split('.').map(Number);
|
|
41
43
|
return updates.filter((update) => {
|
package/package.json
CHANGED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import type { Field } from '../../types/fields.js';
|
|
2
|
-
export type TranslationCompleteness = 'complete' | 'partial' | 'empty';
|
|
3
|
-
export interface LangStatus {
|
|
4
|
-
filled: number;
|
|
5
|
-
total: number;
|
|
6
|
-
percentage: number;
|
|
7
|
-
status: TranslationCompleteness;
|
|
8
|
-
missingFields: {
|
|
9
|
-
slug: string;
|
|
10
|
-
label: string;
|
|
11
|
-
}[];
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Compute translation status for each language.
|
|
15
|
-
* Checks all localized fields (text, richtext, content) + seo/url sub-fields.
|
|
16
|
-
*/
|
|
17
|
-
export declare function computeTranslationStatus(data: Record<string, unknown>, fields: Field[], languages: string[]): Record<string, LangStatus>;
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import { getAtPath } from './objectPath.js';
|
|
2
|
-
/** Extract a label string from Localized, preferring pl then en then slug. */
|
|
3
|
-
function extractLabel(label, fallback) {
|
|
4
|
-
if (!label)
|
|
5
|
-
return fallback;
|
|
6
|
-
if (typeof label === 'string')
|
|
7
|
-
return label;
|
|
8
|
-
return label.pl || label.en || fallback;
|
|
9
|
-
}
|
|
10
|
-
/** Check if a value counts as "filled" for a given field type. */
|
|
11
|
-
function isFieldFilled(value, fieldType) {
|
|
12
|
-
if (value == null)
|
|
13
|
-
return false;
|
|
14
|
-
switch (fieldType) {
|
|
15
|
-
case 'text':
|
|
16
|
-
return typeof value === 'string' && value.length > 0;
|
|
17
|
-
case 'content': {
|
|
18
|
-
if (typeof value !== 'object')
|
|
19
|
-
return false;
|
|
20
|
-
const doc = value;
|
|
21
|
-
return doc.type === 'doc' && Array.isArray(doc.content) && doc.content.length > 0;
|
|
22
|
-
}
|
|
23
|
-
case 'seo': {
|
|
24
|
-
if (typeof value !== 'object')
|
|
25
|
-
return false;
|
|
26
|
-
const seo = value;
|
|
27
|
-
// At least title or description filled
|
|
28
|
-
return ((typeof seo.title === 'string' && seo.title.length > 0) ||
|
|
29
|
-
(typeof seo.description === 'string' && seo.description.length > 0));
|
|
30
|
-
}
|
|
31
|
-
case 'url': {
|
|
32
|
-
if (typeof value !== 'object')
|
|
33
|
-
return false;
|
|
34
|
-
const url = value;
|
|
35
|
-
return typeof url.url === 'string' && url.url.length > 0;
|
|
36
|
-
}
|
|
37
|
-
default:
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
/** Collect all localized fields from a field list, including nested seo/url sub-fields. */
|
|
42
|
-
function collectLocalizedFields(fields) {
|
|
43
|
-
const result = [];
|
|
44
|
-
for (const field of fields) {
|
|
45
|
-
if (field.localized === false)
|
|
46
|
-
continue;
|
|
47
|
-
const label = extractLabel(field.label, field.slug);
|
|
48
|
-
if (field.type === 'text' || field.type === 'content') {
|
|
49
|
-
result.push({ slug: field.slug, label, type: field.type, required: !!field.required });
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
// SEO and URL fields are always localized by nature (sub-fields are lang-keyed)
|
|
53
|
-
for (const field of fields) {
|
|
54
|
-
if (field.type === 'seo') {
|
|
55
|
-
const label = extractLabel(field.label, field.slug);
|
|
56
|
-
result.push({ slug: field.slug, label, type: 'seo', required: !!field.required });
|
|
57
|
-
}
|
|
58
|
-
if (field.type === 'url') {
|
|
59
|
-
const label = extractLabel(field.label, field.slug);
|
|
60
|
-
result.push({ slug: field.slug, label, type: 'url', required: !!field.required });
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return result;
|
|
64
|
-
}
|
|
65
|
-
/** Resolve the value of a field for a given language. */
|
|
66
|
-
function resolveFieldValue(data, field, lang) {
|
|
67
|
-
if (field.type === 'seo') {
|
|
68
|
-
const seoData = getAtPath(data, field.slug);
|
|
69
|
-
if (seoData && typeof seoData === 'object') {
|
|
70
|
-
const seo = seoData;
|
|
71
|
-
const title = extractLangValue(seo.title, lang);
|
|
72
|
-
const desc = extractLangValue(seo.description, lang);
|
|
73
|
-
return { title, description: desc };
|
|
74
|
-
}
|
|
75
|
-
return undefined;
|
|
76
|
-
}
|
|
77
|
-
else if (field.type === 'url') {
|
|
78
|
-
const urlData = getAtPath(data, field.slug);
|
|
79
|
-
if (urlData && typeof urlData === 'object') {
|
|
80
|
-
const url = urlData;
|
|
81
|
-
const urlVal = url.url && typeof url.url === 'object'
|
|
82
|
-
? url.url[lang]
|
|
83
|
-
: undefined;
|
|
84
|
-
return { url: urlVal || '' };
|
|
85
|
-
}
|
|
86
|
-
return undefined;
|
|
87
|
-
}
|
|
88
|
-
else {
|
|
89
|
-
const fieldData = getAtPath(data, field.slug);
|
|
90
|
-
if (fieldData && typeof fieldData === 'object' && !Array.isArray(fieldData)) {
|
|
91
|
-
return fieldData[lang];
|
|
92
|
-
}
|
|
93
|
-
return undefined;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* Compute translation status for each language.
|
|
98
|
-
* Checks all localized fields (text, richtext, content) + seo/url sub-fields.
|
|
99
|
-
*/
|
|
100
|
-
export function computeTranslationStatus(data, fields, languages) {
|
|
101
|
-
const allLocalizedFields = collectLocalizedFields(fields);
|
|
102
|
-
// Pre-filter: keep field if required OR has content in at least one language
|
|
103
|
-
const localizedFields = allLocalizedFields.filter((field) => field.required ||
|
|
104
|
-
languages.some((lang) => isFieldFilled(resolveFieldValue(data, field, lang), field.type)));
|
|
105
|
-
const result = {};
|
|
106
|
-
for (const lang of languages) {
|
|
107
|
-
let filled = 0;
|
|
108
|
-
const total = localizedFields.length;
|
|
109
|
-
const missingFields = [];
|
|
110
|
-
for (const field of localizedFields) {
|
|
111
|
-
const value = resolveFieldValue(data, field, lang);
|
|
112
|
-
if (isFieldFilled(value, field.type)) {
|
|
113
|
-
filled++;
|
|
114
|
-
}
|
|
115
|
-
else {
|
|
116
|
-
missingFields.push({ slug: field.slug, label: field.label });
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
const percentage = total === 0 ? 100 : Math.round((filled / total) * 100);
|
|
120
|
-
const status = percentage === 100 ? 'complete' : percentage === 0 ? 'empty' : 'partial';
|
|
121
|
-
result[lang] = { filled, total, percentage, status, missingFields };
|
|
122
|
-
}
|
|
123
|
-
return result;
|
|
124
|
-
}
|
|
125
|
-
/** Extract lang value from a possibly lang-keyed value. */
|
|
126
|
-
function extractLangValue(value, lang) {
|
|
127
|
-
if (typeof value === 'string')
|
|
128
|
-
return value;
|
|
129
|
-
if (value && typeof value === 'object') {
|
|
130
|
-
return (value[lang]) || '';
|
|
131
|
-
}
|
|
132
|
-
return '';
|
|
133
|
-
}
|
package/dist/demo/reset.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function resetDemoData(): Promise<void>;
|
package/dist/demo/reset.js
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { getCMS } from '../core/cms.js';
|
|
2
|
-
import { seedDemoData } from './seed.js';
|
|
3
|
-
export async function resetDemoData() {
|
|
4
|
-
const cms = getCMS();
|
|
5
|
-
const db = cms.databaseAdapter;
|
|
6
|
-
// Delete all entries (get all, then delete each)
|
|
7
|
-
const allCollectionEntries = await db.getEntries({ type: 'collection' });
|
|
8
|
-
const allSingletonEntries = await db.getEntries({ type: 'singleton' });
|
|
9
|
-
const allEntries = [...allCollectionEntries, ...allSingletonEntries];
|
|
10
|
-
for (const entry of allEntries) {
|
|
11
|
-
// Delete all versions for this entry
|
|
12
|
-
const versions = await db.getEntryVersions({ entryIds: [entry.id] });
|
|
13
|
-
for (const version of versions) {
|
|
14
|
-
await db.deleteEntryVersion({ id: version.id });
|
|
15
|
-
}
|
|
16
|
-
// Delete the entry itself
|
|
17
|
-
await db.deleteEntry({ id: entry.id });
|
|
18
|
-
}
|
|
19
|
-
// Delete all media files
|
|
20
|
-
const mediaFiles = await db.getMediaFiles({ data: {} });
|
|
21
|
-
for (const file of mediaFiles) {
|
|
22
|
-
await db.deleteMediaFile(file.id);
|
|
23
|
-
}
|
|
24
|
-
// Re-seed
|
|
25
|
-
await seedDemoData();
|
|
26
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/* eslint-disable */
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export const hello_world = /** @type {(inputs: { name: NonNullable<unknown> }) => string} */ (i) => {
|
|
5
|
-
return `Hello, ${i.name} from en!`
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
export const login_hello = /** @type {(inputs: {}) => string} */ () => {
|
|
9
|
-
return `Welcome back`
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export const login_please_login = /** @type {(inputs: {}) => string} */ () => {
|
|
13
|
-
return `Login to your account`
|
|
14
|
-
};
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/* eslint-disable */
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export const hello_world = /** @type {(inputs: { name: NonNullable<unknown> }) => string} */ (i) => {
|
|
5
|
-
return `Hello, ${i.name} from pl!`
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
export const login_hello = /** @type {(inputs: {}) => string} */ () => {
|
|
9
|
-
return `Witaj ponownie`
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export const login_please_login = /** @type {(inputs: {}) => string} */ () => {
|
|
13
|
-
return `Zaloguj się na swoje konto`
|
|
14
|
-
};
|