includio-cms 0.20.0 → 0.22.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.
Files changed (101) hide show
  1. package/API.md +22 -21
  2. package/CHANGELOG.md +147 -0
  3. package/DOCS.md +1 -1
  4. package/README.md +138 -32
  5. package/ROADMAP.md +11 -4
  6. package/dist/admin/api/rest/handler.d.ts +13 -1
  7. package/dist/admin/api/rest/handler.js +13 -1
  8. package/dist/admin/api/rest/middleware/apiKey.js +9 -1
  9. package/dist/admin/api/rest/middleware/generateApiKey.d.ts +16 -0
  10. package/dist/admin/api/rest/middleware/generateApiKey.js +19 -0
  11. package/dist/admin/client/collection/collection-entries.svelte +1 -1
  12. package/dist/admin/client/collection/empty-state.svelte +1 -1
  13. package/dist/admin/client/collection/row-actions.svelte +3 -3
  14. package/dist/admin/client/collection/table-toolbar.svelte +3 -1
  15. package/dist/admin/client/entry/entry-header.svelte +3 -1
  16. package/dist/admin/client/users/create-user-dialog.svelte +4 -4
  17. package/dist/admin/client/users/delete-user-dialog.svelte +4 -2
  18. package/dist/admin/client/users/lang.d.ts +10 -2
  19. package/dist/admin/client/users/lang.js +10 -4
  20. package/dist/admin/client/users/users-page.svelte +3 -2
  21. package/dist/admin/components/media/file-upload.svelte +2 -0
  22. package/dist/ai-claude/index.d.ts +9 -1
  23. package/dist/ai-claude/index.js +9 -1
  24. package/dist/ai-openai/index.d.ts +9 -1
  25. package/dist/ai-openai/index.js +9 -1
  26. package/dist/cli/index.js +115 -13
  27. package/dist/cms/runtime/schema.d.ts +2 -0
  28. package/dist/cms/runtime/schema.js +4 -0
  29. package/dist/cms/runtime/types.d.ts +1 -1
  30. package/dist/core/cms.d.ts +13 -1
  31. package/dist/core/cms.js +13 -1
  32. package/dist/core/errors.d.ts +71 -0
  33. package/dist/core/errors.js +179 -0
  34. package/dist/core/server/consentLogs/operations/create.d.ts +13 -1
  35. package/dist/core/server/consentLogs/operations/create.js +13 -1
  36. package/dist/core/server/entries/operations/create.js +6 -1
  37. package/dist/core/server/entries/operations/get.js +14 -3
  38. package/dist/core/server/entries/operations/resolveEntry.d.ts +32 -1
  39. package/dist/core/server/entries/operations/resolveEntry.js +36 -4
  40. package/dist/core/server/entries/operations/update.js +5 -1
  41. package/dist/core/server/fields/utils/resolveMedia.d.ts +18 -1
  42. package/dist/core/server/fields/utils/resolveMedia.js +13 -1
  43. package/dist/core/server/forms/submissions/operations/create.d.ts +21 -1
  44. package/dist/core/server/forms/submissions/operations/create.js +18 -2
  45. package/dist/core/server/forms/submissions/utils/parseMultipart.d.ts +15 -1
  46. package/dist/core/server/forms/submissions/utils/parseMultipart.js +15 -1
  47. package/dist/core/server/media/operations/uploadFile.js +4 -3
  48. package/dist/core/server/media/styles/sharp/generateImageStyle.js +3 -2
  49. package/dist/core/server/media/utils/generateAdminThumbnail.js +3 -2
  50. package/dist/core/server/media/utils/generateBlurDataUrl.js +2 -1
  51. package/dist/db-postgres/index.d.ts +10 -0
  52. package/dist/db-postgres/index.js +10 -0
  53. package/dist/email-nodemailer/index.d.ts +13 -1
  54. package/dist/email-nodemailer/index.js +13 -1
  55. package/dist/entity/index.d.ts +16 -1
  56. package/dist/entity/index.js +16 -1
  57. package/dist/files-local/index.d.ts +12 -1
  58. package/dist/files-local/index.js +12 -1
  59. package/dist/paraglide/messages/_index.d.ts +3 -36
  60. package/dist/paraglide/messages/_index.js +3 -71
  61. package/dist/paraglide/messages/hello_world.d.ts +5 -0
  62. package/dist/paraglide/messages/hello_world.js +33 -0
  63. package/dist/paraglide/messages/login_hello.d.ts +16 -0
  64. package/dist/paraglide/messages/login_hello.js +34 -0
  65. package/dist/paraglide/messages/login_please_login.d.ts +16 -0
  66. package/dist/paraglide/messages/login_please_login.js +34 -0
  67. package/dist/server/auth.d.ts +11 -0
  68. package/dist/server/auth.js +11 -0
  69. package/dist/server/security/csp.d.ts +16 -0
  70. package/dist/server/security/csp.js +33 -0
  71. package/dist/server/security/csrf.d.ts +13 -0
  72. package/dist/server/security/csrf.js +49 -0
  73. package/dist/server/security/index.d.ts +3 -0
  74. package/dist/server/security/index.js +3 -0
  75. package/dist/server/security/rate-limit.d.ts +44 -0
  76. package/dist/server/security/rate-limit.js +97 -0
  77. package/dist/server/utils/withTimeout.d.ts +21 -0
  78. package/dist/server/utils/withTimeout.js +37 -0
  79. package/dist/sveltekit/config.d.ts +67 -4
  80. package/dist/sveltekit/config.js +73 -4
  81. package/dist/sveltekit/server/handle.d.ts +15 -1
  82. package/dist/sveltekit/server/handle.js +22 -1
  83. package/dist/sveltekit/server/index.d.ts +1 -0
  84. package/dist/sveltekit/server/index.js +1 -0
  85. package/dist/sveltekit/server/layout.d.ts +12 -1
  86. package/dist/sveltekit/server/layout.js +12 -1
  87. package/dist/sveltekit/server/preview.d.ts +21 -1
  88. package/dist/sveltekit/server/preview.js +21 -1
  89. package/dist/types/cms.d.ts +4 -0
  90. package/dist/types/cms.schema.d.ts +452 -0
  91. package/dist/types/cms.schema.js +629 -0
  92. package/dist/updates/0.21.0/index.d.ts +2 -0
  93. package/dist/updates/0.21.0/index.js +55 -0
  94. package/dist/updates/0.22.0/index.d.ts +2 -0
  95. package/dist/updates/0.22.0/index.js +75 -0
  96. package/dist/updates/index.js +3 -1
  97. package/package.json +12 -2
  98. package/dist/paraglide/messages/en.d.ts +0 -5
  99. package/dist/paraglide/messages/en.js +0 -14
  100. package/dist/paraglide/messages/pl.d.ts +0 -5
  101. package/dist/paraglide/messages/pl.js +0 -14
@@ -0,0 +1,3 @@
1
+ export { csrfGuard, isCsrfSafe } from './csrf.js';
2
+ export { rateLimitGuard, MemoryRateLimitStore } from './rate-limit.js';
3
+ export { buildCspHeader } from './csp.js';
@@ -0,0 +1,44 @@
1
+ import type { Handle, RequestEvent } from '@sveltejs/kit';
2
+ export interface RateLimitResult {
3
+ allowed: boolean;
4
+ remaining: number;
5
+ resetAt: number;
6
+ }
7
+ /**
8
+ * Pluggable storage for {@link rateLimitGuard}. The default implementation is
9
+ * in-memory; multi-node deploys should provide a Redis-backed store.
10
+ * @internal
11
+ */
12
+ export interface RateLimitStore {
13
+ hit(key: string, windowMs: number, limit: number): Promise<RateLimitResult>;
14
+ }
15
+ /**
16
+ * In-memory {@link RateLimitStore}. Per-process; not safe across multiple nodes.
17
+ * @internal
18
+ */
19
+ export declare class MemoryRateLimitStore implements RateLimitStore {
20
+ private buckets;
21
+ private cleanupInterval;
22
+ constructor(opts?: {
23
+ cleanupMs?: number;
24
+ });
25
+ hit(key: string, windowMs: number, limit: number): Promise<RateLimitResult>;
26
+ private cleanup;
27
+ reset(key?: string): void;
28
+ dispose(): void;
29
+ }
30
+ export interface RateLimitGuardOptions {
31
+ store?: RateLimitStore;
32
+ limit?: number;
33
+ windowMs?: number;
34
+ pathPrefix?: string;
35
+ key?: (event: RequestEvent) => string;
36
+ }
37
+ /**
38
+ * SvelteKit handle that limits requests under `pathPrefix` (default `/admin/api/`)
39
+ * per key (default: authenticated user id, falling back to client IP). Returns
40
+ * 429 with `Retry-After` when exceeded. Defaults: 200 req / 60s, override via env
41
+ * `INCLUDIO_RATE_LIMIT_MAX` / `INCLUDIO_RATE_LIMIT_WINDOW_MS`.
42
+ * @internal
43
+ */
44
+ export declare function rateLimitGuard(opts?: RateLimitGuardOptions): Handle;
@@ -0,0 +1,97 @@
1
+ import { json } from '@sveltejs/kit';
2
+ /**
3
+ * In-memory {@link RateLimitStore}. Per-process; not safe across multiple nodes.
4
+ * @internal
5
+ */
6
+ export class MemoryRateLimitStore {
7
+ buckets = new Map();
8
+ cleanupInterval;
9
+ constructor(opts = {}) {
10
+ const cleanupMs = opts.cleanupMs ?? 60_000;
11
+ if (cleanupMs > 0 && typeof setInterval === 'function') {
12
+ this.cleanupInterval = setInterval(() => this.cleanup(), cleanupMs);
13
+ const i = this.cleanupInterval;
14
+ if (typeof i.unref === 'function')
15
+ i.unref();
16
+ }
17
+ }
18
+ async hit(key, windowMs, limit) {
19
+ const now = Date.now();
20
+ const bucket = this.buckets.get(key);
21
+ if (!bucket || bucket.resetAt <= now) {
22
+ const fresh = { count: 1, resetAt: now + windowMs };
23
+ this.buckets.set(key, fresh);
24
+ return { allowed: true, remaining: Math.max(0, limit - 1), resetAt: fresh.resetAt };
25
+ }
26
+ if (bucket.count >= limit) {
27
+ return { allowed: false, remaining: 0, resetAt: bucket.resetAt };
28
+ }
29
+ bucket.count += 1;
30
+ return { allowed: true, remaining: limit - bucket.count, resetAt: bucket.resetAt };
31
+ }
32
+ cleanup() {
33
+ const now = Date.now();
34
+ for (const [k, b] of this.buckets) {
35
+ if (b.resetAt <= now)
36
+ this.buckets.delete(k);
37
+ }
38
+ }
39
+ reset(key) {
40
+ if (key)
41
+ this.buckets.delete(key);
42
+ else
43
+ this.buckets.clear();
44
+ }
45
+ dispose() {
46
+ if (this.cleanupInterval)
47
+ clearInterval(this.cleanupInterval);
48
+ this.cleanupInterval = undefined;
49
+ }
50
+ }
51
+ function num(env, fallback) {
52
+ const n = env ? Number(env) : NaN;
53
+ return Number.isFinite(n) && n > 0 ? n : fallback;
54
+ }
55
+ /**
56
+ * SvelteKit handle that limits requests under `pathPrefix` (default `/admin/api/`)
57
+ * per key (default: authenticated user id, falling back to client IP). Returns
58
+ * 429 with `Retry-After` when exceeded. Defaults: 200 req / 60s, override via env
59
+ * `INCLUDIO_RATE_LIMIT_MAX` / `INCLUDIO_RATE_LIMIT_WINDOW_MS`.
60
+ * @internal
61
+ */
62
+ export function rateLimitGuard(opts = {}) {
63
+ const store = opts.store ?? new MemoryRateLimitStore();
64
+ const limit = opts.limit ?? num(process.env.INCLUDIO_RATE_LIMIT_MAX, 200);
65
+ const windowMs = opts.windowMs ?? num(process.env.INCLUDIO_RATE_LIMIT_WINDOW_MS, 60_000);
66
+ const pathPrefix = opts.pathPrefix ?? '/admin/api/';
67
+ const keyFn = opts.key ??
68
+ ((event) => {
69
+ const userId = event.locals?.user?.id;
70
+ if (userId)
71
+ return `u:${userId}`;
72
+ try {
73
+ return `ip:${event.getClientAddress()}`;
74
+ }
75
+ catch {
76
+ return 'ip:unknown';
77
+ }
78
+ });
79
+ return async ({ event, resolve }) => {
80
+ if (!event.url.pathname.startsWith(pathPrefix))
81
+ return resolve(event);
82
+ const result = await store.hit(keyFn(event), windowMs, limit);
83
+ if (!result.allowed) {
84
+ const retryAfter = Math.max(1, Math.ceil((result.resetAt - Date.now()) / 1000));
85
+ return json({ error: 'rate_limited', resetAt: result.resetAt }, {
86
+ status: 429,
87
+ headers: {
88
+ 'retry-after': String(retryAfter),
89
+ 'x-ratelimit-limit': String(limit),
90
+ 'x-ratelimit-remaining': '0',
91
+ 'x-ratelimit-reset': String(Math.ceil(result.resetAt / 1000))
92
+ }
93
+ });
94
+ }
95
+ return resolve(event);
96
+ };
97
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Race a promise against a deadline. Resolves with the original promise's value
3
+ * if it settles before `ms`; otherwise rejects with {@link TimeoutError}.
4
+ * Cleans up the timer in both success and failure paths so it never holds the
5
+ * event loop.
6
+ * @internal
7
+ */
8
+ export declare class TimeoutError extends Error {
9
+ readonly label: string;
10
+ readonly ms: number;
11
+ readonly name = "TimeoutError";
12
+ constructor(label: string, ms: number);
13
+ }
14
+ /** @internal */
15
+ export declare function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T>;
16
+ /**
17
+ * Resolved timeout (ms) for sharp operations. Override via `INCLUDIO_SHARP_TIMEOUT_MS`.
18
+ * Documented in `KNOWN-RISKS.md` §4.
19
+ * @internal
20
+ */
21
+ export declare function sharpTimeoutMs(): number;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Race a promise against a deadline. Resolves with the original promise's value
3
+ * if it settles before `ms`; otherwise rejects with {@link TimeoutError}.
4
+ * Cleans up the timer in both success and failure paths so it never holds the
5
+ * event loop.
6
+ * @internal
7
+ */
8
+ export class TimeoutError extends Error {
9
+ label;
10
+ ms;
11
+ name = 'TimeoutError';
12
+ constructor(label, ms) {
13
+ super(`${label} timed out after ${ms}ms`);
14
+ this.label = label;
15
+ this.ms = ms;
16
+ }
17
+ }
18
+ /** @internal */
19
+ export function withTimeout(promise, ms, label) {
20
+ let timer;
21
+ const timeout = new Promise((_, reject) => {
22
+ timer = setTimeout(() => reject(new TimeoutError(label, ms)), ms);
23
+ });
24
+ return Promise.race([promise, timeout]).finally(() => {
25
+ if (timer)
26
+ clearTimeout(timer);
27
+ });
28
+ }
29
+ /**
30
+ * Resolved timeout (ms) for sharp operations. Override via `INCLUDIO_SHARP_TIMEOUT_MS`.
31
+ * Documented in `KNOWN-RISKS.md` §4.
32
+ * @internal
33
+ */
34
+ export function sharpTimeoutMs() {
35
+ const n = Number(process.env.INCLUDIO_SHARP_TIMEOUT_MS);
36
+ return Number.isFinite(n) && n > 0 ? n : 30_000;
37
+ }
@@ -14,27 +14,90 @@ type SingleInput = Omit<SingleConfig, 'fields'> & {
14
14
  };
15
15
  /**
16
16
  * Defines the root CMS configuration. Pass to `includioCMS()` in `hooks.server.ts`.
17
+ *
18
+ * Runs strict runtime validation: invalid configs throw a `ConfigValidationError`
19
+ * (extends `CmsError`) listing every issue with a path and a fix hint, e.g.
20
+ * `languages[0].code: must be a 2-letter ISO code (e.g. 'en')`
21
+ *
22
+ * @param config - The CMSConfig (collections, singles, forms, adapters, languages, ...).
23
+ * @returns The same config, parsed and frozen against typos.
24
+ * @throws {ConfigValidationError} when validation fails — message aggregates all issues.
17
25
  * @public
26
+ * @example
27
+ * ```ts
28
+ * import { defineConfig } from 'includio-cms';
29
+ * export default defineConfig({
30
+ * languages: [{ code: 'en', default: true }],
31
+ * db, files,
32
+ * collections: [postsCollection]
33
+ * });
34
+ * ```
18
35
  */
19
36
  export declare function defineConfig(config: CMSConfig): CMSConfig;
20
37
  /**
21
- * Defines a collection (multi-entry content type).
38
+ * Defines a collection (multi-entry content type, e.g. blog posts, products).
39
+ *
40
+ * @param config - Collection config with slug, fields, and optional layout/labels/icon.
41
+ * @returns A typed `CollectionConfig` ready to drop into `defineConfig({ collections: [...] })`.
22
42
  * @public
43
+ * @example
44
+ * ```ts
45
+ * export const posts = defineCollection({
46
+ * slug: 'posts',
47
+ * labels: { singular: 'Post', plural: 'Posts' },
48
+ * fields: [{ slug: 'title', type: 'text', required: true }]
49
+ * });
50
+ * ```
23
51
  */
24
52
  export declare function defineCollection(config: CollectionInput): CollectionConfig;
25
53
  /**
26
- * Defines a singleton (single-entry content type, e.g. site settings).
54
+ * Defines a singleton (single-entry content type, e.g. site settings, homepage).
55
+ *
56
+ * @param config - Single config with slug, fields, and optional label/layout.
57
+ * @returns A typed `SingleConfig` ready to drop into `defineConfig({ singles: [...] })`.
27
58
  * @public
59
+ * @example
60
+ * ```ts
61
+ * export const settings = defineSingle({
62
+ * slug: 'settings',
63
+ * fields: [{ slug: 'siteName', type: 'text' }]
64
+ * });
65
+ * ```
28
66
  */
29
67
  export declare function defineSingle(config: SingleInput): SingleConfig;
30
68
  /**
31
- * Defines a public form (submitted via `/api/forms/[slug]/submit`).
69
+ * Defines a public form (submitted via `POST /api/forms/[slug]/submit`).
70
+ *
71
+ * @param config - Form config with slug, label, fields, and optional notification emails.
72
+ * @returns The `FormConfig` ready to drop into `defineConfig({ forms: [...] })`.
32
73
  * @public
74
+ * @example
75
+ * ```ts
76
+ * export const contact = defineForm({
77
+ * slug: 'contact',
78
+ * label: 'Contact us',
79
+ * fields: [{ slug: 'email', type: 'email', required: true }]
80
+ * });
81
+ * ```
33
82
  */
34
83
  export declare function defineForm(config: FormConfig): FormConfig;
35
84
  /**
36
- * Defines a reusable object field (nested record). Use inside `fields[]`.
85
+ * Defines a reusable object field (nested record). Use inline inside `fields[]`
86
+ * or as an entry in a `blocks` field.
87
+ *
88
+ * @param config - Object field shape sans the `type` discriminator.
89
+ * @returns The full `ObjectField` (with `type: 'object'` injected).
37
90
  * @public
91
+ * @example
92
+ * ```ts
93
+ * const cta = defineObject({
94
+ * slug: 'cta',
95
+ * fields: [
96
+ * { slug: 'label', type: 'text' },
97
+ * { slug: 'href', type: 'url' }
98
+ * ]
99
+ * });
100
+ * ```
38
101
  */
39
102
  export declare function defineObject(config: Omit<ObjectField, 'type'>): ObjectField;
40
103
  export {};
@@ -1,34 +1,103 @@
1
+ import { cmsConfigSchema } from '../types/cms.schema.js';
2
+ import { formatConfigError } from '../core/errors.js';
1
3
  /**
2
4
  * Defines the root CMS configuration. Pass to `includioCMS()` in `hooks.server.ts`.
5
+ *
6
+ * Runs strict runtime validation: invalid configs throw a `ConfigValidationError`
7
+ * (extends `CmsError`) listing every issue with a path and a fix hint, e.g.
8
+ * `languages[0].code: must be a 2-letter ISO code (e.g. 'en')`
9
+ *
10
+ * @param config - The CMSConfig (collections, singles, forms, adapters, languages, ...).
11
+ * @returns The same config, parsed and frozen against typos.
12
+ * @throws {ConfigValidationError} when validation fails — message aggregates all issues.
3
13
  * @public
14
+ * @example
15
+ * ```ts
16
+ * import { defineConfig } from 'includio-cms';
17
+ * export default defineConfig({
18
+ * languages: [{ code: 'en', default: true }],
19
+ * db, files,
20
+ * collections: [postsCollection]
21
+ * });
22
+ * ```
4
23
  */
5
24
  export function defineConfig(config) {
25
+ const result = cmsConfigSchema.safeParse(config);
26
+ if (!result.success) {
27
+ throw formatConfigError(result.error);
28
+ }
6
29
  return config;
7
30
  }
8
31
  /**
9
- * Defines a collection (multi-entry content type).
32
+ * Defines a collection (multi-entry content type, e.g. blog posts, products).
33
+ *
34
+ * @param config - Collection config with slug, fields, and optional layout/labels/icon.
35
+ * @returns A typed `CollectionConfig` ready to drop into `defineConfig({ collections: [...] })`.
10
36
  * @public
37
+ * @example
38
+ * ```ts
39
+ * export const posts = defineCollection({
40
+ * slug: 'posts',
41
+ * labels: { singular: 'Post', plural: 'Posts' },
42
+ * fields: [{ slug: 'title', type: 'text', required: true }]
43
+ * });
44
+ * ```
11
45
  */
12
46
  export function defineCollection(config) {
13
47
  return config;
14
48
  }
15
49
  /**
16
- * Defines a singleton (single-entry content type, e.g. site settings).
50
+ * Defines a singleton (single-entry content type, e.g. site settings, homepage).
51
+ *
52
+ * @param config - Single config with slug, fields, and optional label/layout.
53
+ * @returns A typed `SingleConfig` ready to drop into `defineConfig({ singles: [...] })`.
17
54
  * @public
55
+ * @example
56
+ * ```ts
57
+ * export const settings = defineSingle({
58
+ * slug: 'settings',
59
+ * fields: [{ slug: 'siteName', type: 'text' }]
60
+ * });
61
+ * ```
18
62
  */
19
63
  export function defineSingle(config) {
20
64
  return config;
21
65
  }
22
66
  /**
23
- * Defines a public form (submitted via `/api/forms/[slug]/submit`).
67
+ * Defines a public form (submitted via `POST /api/forms/[slug]/submit`).
68
+ *
69
+ * @param config - Form config with slug, label, fields, and optional notification emails.
70
+ * @returns The `FormConfig` ready to drop into `defineConfig({ forms: [...] })`.
24
71
  * @public
72
+ * @example
73
+ * ```ts
74
+ * export const contact = defineForm({
75
+ * slug: 'contact',
76
+ * label: 'Contact us',
77
+ * fields: [{ slug: 'email', type: 'email', required: true }]
78
+ * });
79
+ * ```
25
80
  */
26
81
  export function defineForm(config) {
27
82
  return config;
28
83
  }
29
84
  /**
30
- * Defines a reusable object field (nested record). Use inside `fields[]`.
85
+ * Defines a reusable object field (nested record). Use inline inside `fields[]`
86
+ * or as an entry in a `blocks` field.
87
+ *
88
+ * @param config - Object field shape sans the `type` discriminator.
89
+ * @returns The full `ObjectField` (with `type: 'object'` injected).
31
90
  * @public
91
+ * @example
92
+ * ```ts
93
+ * const cta = defineObject({
94
+ * slug: 'cta',
95
+ * fields: [
96
+ * { slug: 'label', type: 'text' },
97
+ * { slug: 'href', type: 'url' }
98
+ * ]
99
+ * });
100
+ * ```
32
101
  */
33
102
  export function defineObject(config) {
34
103
  return { ...config, type: 'object' };
@@ -1,7 +1,21 @@
1
1
  import { type Handle } from '@sveltejs/kit';
2
2
  import type { CMSConfig } from '../../types/cms.js';
3
3
  /**
4
- * SvelteKit `Handle[]` array that initializes the CMS, generates runtime artifacts, wires auth/admin guards, and exposes `event.locals.cmsContext`. Compose with `sequence()` in `hooks.server.ts`.
4
+ * SvelteKit `Handle[]` array that initializes the CMS, generates runtime
5
+ * artifacts, wires auth/admin guards, and exposes `event.locals.cmsContext`.
6
+ * Compose with `sequence()` in `hooks.server.ts`.
7
+ *
8
+ * @param cmsConfig - The output of `defineConfig({ ... })`.
9
+ * @returns An array of `Handle` middlewares. Pass to `sequence()`.
5
10
  * @public
11
+ * @example
12
+ * ```ts
13
+ * // src/hooks.server.ts
14
+ * import { sequence } from '@sveltejs/kit/hooks';
15
+ * import { includioCMS } from 'includio-cms/sveltekit/server';
16
+ * import cmsConfig from '../../cms/cms.config';
17
+ *
18
+ * export const handle = sequence(...includioCMS(cmsConfig));
19
+ * ```
6
20
  */
7
21
  export declare function includioCMS(cmsConfig: CMSConfig): Handle[];
@@ -4,6 +4,9 @@ import { getCMS, initCMS } from '../../core/cms.js';
4
4
  import { generateRuntime } from '../../core/server/generator/generator.js';
5
5
  import { svelteKitHandler } from 'better-auth/svelte-kit';
6
6
  import { building } from '$app/environment';
7
+ import { csrfGuard } from '../../server/security/csrf.js';
8
+ import { rateLimitGuard } from '../../server/security/rate-limit.js';
9
+ import { buildCspHeader } from '../../server/security/csp.js';
7
10
  const adminGuard = async ({ event, resolve }) => {
8
11
  const { user, session } = event.locals;
9
12
  // Secure the admin routes
@@ -66,25 +69,43 @@ const detectBrowserContext = async ({ event, resolve }) => {
66
69
  return resolve(event);
67
70
  };
68
71
  /**
69
- * SvelteKit `Handle[]` array that initializes the CMS, generates runtime artifacts, wires auth/admin guards, and exposes `event.locals.cmsContext`. Compose with `sequence()` in `hooks.server.ts`.
72
+ * SvelteKit `Handle[]` array that initializes the CMS, generates runtime
73
+ * artifacts, wires auth/admin guards, and exposes `event.locals.cmsContext`.
74
+ * Compose with `sequence()` in `hooks.server.ts`.
75
+ *
76
+ * @param cmsConfig - The output of `defineConfig({ ... })`.
77
+ * @returns An array of `Handle` middlewares. Pass to `sequence()`.
70
78
  * @public
79
+ * @example
80
+ * ```ts
81
+ * // src/hooks.server.ts
82
+ * import { sequence } from '@sveltejs/kit/hooks';
83
+ * import { includioCMS } from 'includio-cms/sveltekit/server';
84
+ * import cmsConfig from '../../cms/cms.config';
85
+ *
86
+ * export const handle = sequence(...includioCMS(cmsConfig));
87
+ * ```
71
88
  */
72
89
  export function includioCMS(cmsConfig) {
73
90
  generateRuntime(cmsConfig); // Generate runtime code based on the CMS config
74
91
  initCMS(cmsConfig);
75
92
  const handles = [];
76
93
  handles.push(detectBrowserContext);
94
+ handles.push(csrfGuard);
77
95
  if (cmsConfig.auth) {
78
96
  handles.push(handleAuth);
79
97
  }
98
+ handles.push(rateLimitGuard());
80
99
  handles.push(adminGuard);
81
100
  handles.push(securityHeaders);
82
101
  return handles;
83
102
  }
103
+ const CSP_VALUE = buildCspHeader();
84
104
  const securityHeaders = async ({ event, resolve }) => {
85
105
  const response = await resolve(event);
86
106
  response.headers.set('X-Content-Type-Options', 'nosniff');
87
107
  response.headers.set('X-Frame-Options', 'SAMEORIGIN');
88
108
  response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
109
+ response.headers.set('Content-Security-Policy', CSP_VALUE);
89
110
  return response;
90
111
  };
@@ -6,3 +6,4 @@ export { parseFormDataForSubmission } from '../../core/server/forms/submissions/
6
6
  export { createConsentLog } from '../../core/server/consentLogs/operations/create.js';
7
7
  export { getPreviewEntry } from './preview.js';
8
8
  export { createRestApiHandler } from '../../admin/api/rest/handler.js';
9
+ export { generateApiKey } from '../../admin/api/rest/middleware/generateApiKey.js';
@@ -7,3 +7,4 @@ export { createConsentLog } from '../../core/server/consentLogs/operations/creat
7
7
  export { getPreviewEntry } from './preview.js';
8
8
  // Folded from `./admin/api/rest/handler` (dropped as separate export in 0.20.0)
9
9
  export { createRestApiHandler } from '../../admin/api/rest/handler.js';
10
+ export { generateApiKey } from '../../admin/api/rest/middleware/generateApiKey.js';
@@ -1,7 +1,18 @@
1
1
  import type { RequestEvent } from '@sveltejs/kit';
2
2
  /**
3
- * Returns `cmsContext` from `event.locals` for use in a SvelteKit layout `load`. Drop into your `+layout.server.ts`.
3
+ * Returns `cmsContext` from `event.locals` for use in a SvelteKit layout
4
+ * `load`. Drop into your `+layout.server.ts`.
5
+ *
6
+ * @param event - The SvelteKit `RequestEvent`.
7
+ * @returns `{ cmsContext }` with browser/UA-derived hints (e.g. `preferMp4`).
4
8
  * @public
9
+ * @example
10
+ * ```ts
11
+ * // src/routes/+layout.server.ts
12
+ * import { cmsLayoutLoad } from 'includio-cms/sveltekit/server';
13
+ *
14
+ * export const load = (event) => cmsLayoutLoad(event);
15
+ * ```
5
16
  */
6
17
  export declare function cmsLayoutLoad(event: RequestEvent): {
7
18
  cmsContext: any;
@@ -1,6 +1,17 @@
1
1
  /**
2
- * Returns `cmsContext` from `event.locals` for use in a SvelteKit layout `load`. Drop into your `+layout.server.ts`.
2
+ * Returns `cmsContext` from `event.locals` for use in a SvelteKit layout
3
+ * `load`. Drop into your `+layout.server.ts`.
4
+ *
5
+ * @param event - The SvelteKit `RequestEvent`.
6
+ * @returns `{ cmsContext }` with browser/UA-derived hints (e.g. `preferMp4`).
3
7
  * @public
8
+ * @example
9
+ * ```ts
10
+ * // src/routes/+layout.server.ts
11
+ * import { cmsLayoutLoad } from 'includio-cms/sveltekit/server';
12
+ *
13
+ * export const load = (event) => cmsLayoutLoad(event);
14
+ * ```
4
15
  */
5
16
  export function cmsLayoutLoad(event) {
6
17
  return {
@@ -1,8 +1,28 @@
1
1
  import type { Entry } from '../../types/entries.js';
2
2
  import { type RequestEvent } from '@sveltejs/kit';
3
3
  /**
4
- * Resolves the preview entry from a `?preview=<versionId>` query param. Requires an authenticated session — throws `Unauthorized` otherwise.
4
+ * Resolves the preview entry from a `?preview=<versionId>` query param.
5
+ * Use it from a frontend page `load` to render unpublished/draft content for
6
+ * authenticated editors.
7
+ *
8
+ * Requires an authenticated session — throws `Unauthorized` otherwise.
9
+ *
10
+ * @param event - The SvelteKit `RequestEvent`.
11
+ * @param options - `language` is the locale to populate.
12
+ * @returns The populated preview `Entry`, or `null` when no `?preview=` param.
13
+ * @throws {Error} `'Unauthorized'` when the request lacks a valid session.
5
14
  * @public
15
+ * @example
16
+ * ```ts
17
+ * // src/routes/blog/[slug]/+page.server.ts
18
+ * import { getPreviewEntry } from 'includio-cms/sveltekit/server';
19
+ *
20
+ * export const load = async (event) => {
21
+ * const preview = await getPreviewEntry(event, { language: 'en' });
22
+ * if (preview) return { entry: preview };
23
+ * // ...fall through to published lookup
24
+ * };
25
+ * ```
6
26
  */
7
27
  export declare function getPreviewEntry(event: RequestEvent, options: {
8
28
  language: string;
@@ -1,8 +1,28 @@
1
1
  import { getEntryVersion } from '../../core/server/entries/operations/get.js';
2
2
  import {} from '@sveltejs/kit';
3
3
  /**
4
- * Resolves the preview entry from a `?preview=<versionId>` query param. Requires an authenticated session — throws `Unauthorized` otherwise.
4
+ * Resolves the preview entry from a `?preview=<versionId>` query param.
5
+ * Use it from a frontend page `load` to render unpublished/draft content for
6
+ * authenticated editors.
7
+ *
8
+ * Requires an authenticated session — throws `Unauthorized` otherwise.
9
+ *
10
+ * @param event - The SvelteKit `RequestEvent`.
11
+ * @param options - `language` is the locale to populate.
12
+ * @returns The populated preview `Entry`, or `null` when no `?preview=` param.
13
+ * @throws {Error} `'Unauthorized'` when the request lacks a valid session.
5
14
  * @public
15
+ * @example
16
+ * ```ts
17
+ * // src/routes/blog/[slug]/+page.server.ts
18
+ * import { getPreviewEntry } from 'includio-cms/sveltekit/server';
19
+ *
20
+ * export const load = async (event) => {
21
+ * const preview = await getPreviewEntry(event, { language: 'en' });
22
+ * if (preview) return { entry: preview };
23
+ * // ...fall through to published lookup
24
+ * };
25
+ * ```
6
26
  */
7
27
  export async function getPreviewEntry(event, options) {
8
28
  const preview = event.url.searchParams.get('preview');
@@ -60,6 +60,10 @@ export interface ApiKeyConfig {
60
60
  key: string;
61
61
  name?: string;
62
62
  role?: 'admin' | 'editor';
63
+ /** Opt-in expiry. ISO-8601 timestamp; past dates → 401 on use. */
64
+ expiresAt?: string;
65
+ /** Audit-trail only; not enforced. ISO-8601 timestamp of last manual rotation. */
66
+ rotatedAt?: string;
63
67
  }
64
68
  export interface TypographyConfig {
65
69
  fixOrphans?: boolean;