includio-cms 0.19.0 → 0.21.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 (86) hide show
  1. package/API.md +410 -0
  2. package/CHANGELOG.md +112 -0
  3. package/DOCS.md +1 -1
  4. package/ROADMAP.md +8 -4
  5. package/dist/admin/api/rest/handler.d.ts +4 -0
  6. package/dist/admin/api/rest/handler.js +4 -0
  7. package/dist/admin/api/rest/middleware/apiKey.js +9 -1
  8. package/dist/admin/api/rest/middleware/generateApiKey.d.ts +7 -0
  9. package/dist/admin/api/rest/middleware/generateApiKey.js +10 -0
  10. package/dist/admin/client/index.d.ts +2 -0
  11. package/dist/admin/client/index.js +4 -0
  12. package/dist/admin/client/users/delete-user-dialog.svelte +2 -2
  13. package/dist/admin/client/users/lang.d.ts +10 -2
  14. package/dist/admin/client/users/lang.js +10 -4
  15. package/dist/components/ui/input-group/input-group-input.svelte.d.ts +1 -1
  16. package/dist/components/ui/sidebar/sidebar-input.svelte.d.ts +1 -1
  17. package/dist/core/cms.d.ts +4 -0
  18. package/dist/core/cms.js +4 -0
  19. package/dist/core/index.d.ts +2 -0
  20. package/dist/core/index.js +2 -0
  21. package/dist/core/server/consentLogs/operations/create.d.ts +4 -0
  22. package/dist/core/server/consentLogs/operations/create.js +4 -0
  23. package/dist/core/server/fields/utils/resolveMedia.d.ts +5 -2
  24. package/dist/core/server/fields/utils/resolveMedia.js +2 -2
  25. package/dist/core/server/forms/submissions/operations/create.d.ts +7 -0
  26. package/dist/core/server/forms/submissions/operations/create.js +4 -0
  27. package/dist/core/server/forms/submissions/utils/parseMultipart.d.ts +4 -0
  28. package/dist/core/server/forms/submissions/utils/parseMultipart.js +4 -0
  29. package/dist/core/server/media/operations/uploadFile.js +4 -3
  30. package/dist/core/server/media/styles/sharp/generateImageStyle.js +3 -2
  31. package/dist/core/server/media/utils/generateAdminThumbnail.js +3 -2
  32. package/dist/core/server/media/utils/generateBlurDataUrl.js +2 -1
  33. package/dist/db-postgres/index.d.ts +2 -0
  34. package/dist/db-postgres/index.js +3 -0
  35. package/dist/entity/index.d.ts +4 -0
  36. package/dist/entity/index.js +4 -0
  37. package/dist/files-local/index.d.ts +1 -0
  38. package/dist/files-local/index.js +1 -0
  39. package/dist/index.d.ts +0 -2
  40. package/dist/index.js +0 -1
  41. package/dist/paraglide/messages/_index.d.ts +36 -3
  42. package/dist/paraglide/messages/_index.js +71 -3
  43. package/dist/paraglide/messages/en.d.ts +5 -0
  44. package/dist/paraglide/messages/en.js +14 -0
  45. package/dist/paraglide/messages/pl.d.ts +5 -0
  46. package/dist/paraglide/messages/pl.js +14 -0
  47. package/dist/server/auth.d.ts +4 -0
  48. package/dist/server/auth.js +4 -0
  49. package/dist/server/security/csp.d.ts +16 -0
  50. package/dist/server/security/csp.js +33 -0
  51. package/dist/server/security/csrf.d.ts +13 -0
  52. package/dist/server/security/csrf.js +49 -0
  53. package/dist/server/security/index.d.ts +3 -0
  54. package/dist/server/security/index.js +3 -0
  55. package/dist/server/security/rate-limit.d.ts +44 -0
  56. package/dist/server/security/rate-limit.js +97 -0
  57. package/dist/server/utils/withTimeout.d.ts +21 -0
  58. package/dist/server/utils/withTimeout.js +37 -0
  59. package/dist/shop/client/index.d.ts +4 -0
  60. package/dist/shop/client/index.js +4 -0
  61. package/dist/sveltekit/config.d.ts +20 -0
  62. package/dist/sveltekit/config.js +20 -0
  63. package/dist/sveltekit/server/handle.d.ts +4 -0
  64. package/dist/sveltekit/server/handle.js +11 -0
  65. package/dist/sveltekit/server/index.d.ts +2 -0
  66. package/dist/sveltekit/server/index.js +3 -0
  67. package/dist/sveltekit/server/layout.d.ts +4 -0
  68. package/dist/sveltekit/server/layout.js +4 -0
  69. package/dist/sveltekit/server/preview.d.ts +4 -0
  70. package/dist/sveltekit/server/preview.js +4 -0
  71. package/dist/types/cms.d.ts +4 -0
  72. package/dist/types/index.d.ts +1 -0
  73. package/dist/types/index.js +1 -0
  74. package/dist/types/plugins.d.ts +8 -0
  75. package/dist/updates/0.20.0/index.d.ts +2 -0
  76. package/dist/updates/0.20.0/index.js +54 -0
  77. package/dist/updates/0.21.0/index.d.ts +2 -0
  78. package/dist/updates/0.21.0/index.js +55 -0
  79. package/dist/updates/index.js +3 -1
  80. package/package.json +15 -68
  81. package/dist/paraglide/messages/hello_world.d.ts +0 -5
  82. package/dist/paraglide/messages/hello_world.js +0 -33
  83. package/dist/paraglide/messages/login_hello.d.ts +0 -16
  84. package/dist/paraglide/messages/login_hello.js +0 -34
  85. package/dist/paraglide/messages/login_please_login.d.ts +0 -16
  86. package/dist/paraglide/messages/login_please_login.js +0 -34
@@ -0,0 +1,49 @@
1
+ const MUTATING_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
2
+ function getAllowedOrigins() {
3
+ const env = process.env.INCLUDIO_CSRF_ALLOWED_ORIGINS ?? '';
4
+ return new Set(env
5
+ .split(',')
6
+ .map((s) => s.trim())
7
+ .filter(Boolean));
8
+ }
9
+ /**
10
+ * Returns true when a request is CSRF-safe: a non-mutating method, or a mutating
11
+ * method whose Origin/Referer matches the request URL origin (or the env allowlist).
12
+ * @internal
13
+ */
14
+ export function isCsrfSafe(event) {
15
+ const method = event.request.method.toUpperCase();
16
+ if (!MUTATING_METHODS.has(method))
17
+ return true;
18
+ const expected = event.url.origin;
19
+ const allowed = getAllowedOrigins();
20
+ const origin = event.request.headers.get('origin');
21
+ if (origin)
22
+ return origin === expected || allowed.has(origin);
23
+ const referer = event.request.headers.get('referer');
24
+ if (referer) {
25
+ try {
26
+ const refOrigin = new URL(referer).origin;
27
+ return refOrigin === expected || allowed.has(refOrigin);
28
+ }
29
+ catch {
30
+ return false;
31
+ }
32
+ }
33
+ return false;
34
+ }
35
+ /**
36
+ * SvelteKit handle that rejects mutating requests under `/admin/api/*` lacking a
37
+ * matching Origin/Referer header. Other paths and safe methods pass through.
38
+ * @internal
39
+ */
40
+ export const csrfGuard = async ({ event, resolve }) => {
41
+ if (!event.url.pathname.startsWith('/admin/api/'))
42
+ return resolve(event);
43
+ if (isCsrfSafe(event))
44
+ return resolve(event);
45
+ return new Response(JSON.stringify({ error: 'csrf_rejected' }), {
46
+ status: 403,
47
+ headers: { 'content-type': 'application/json' }
48
+ });
49
+ };
@@ -0,0 +1,3 @@
1
+ export { csrfGuard, isCsrfSafe } from './csrf.js';
2
+ export { rateLimitGuard, MemoryRateLimitStore, type RateLimitStore, type RateLimitResult, type RateLimitGuardOptions } from './rate-limit.js';
3
+ export { buildCspHeader, type CspOptions } from './csp.js';
@@ -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
+ }
@@ -125,3 +125,7 @@ export declare function createShopClient(options?: ShopClientOptions): ShopClien
125
125
  export type { CartSnapshot, CartLine, CartItemRef } from '../cart/types.js';
126
126
  export { createOrderState } from './use-order.svelte.js';
127
127
  export type { OrderState, CreateOrderStateOptions, OrderPaymentPhase } from './use-order.svelte.js';
128
+ export { default as OrderStatus } from '../svelte/OrderStatus.svelte';
129
+ export { default as InpostPicker } from '../svelte/InpostPicker.svelte';
130
+ export { DEFAULT_LABELS_PL } from '../svelte/labels.js';
131
+ export type { OrderStatusLabels } from '../svelte/labels.js';
@@ -47,3 +47,7 @@ function tokenQuery(token) {
47
47
  return token ? `?token=${encodeURIComponent(token)}` : '';
48
48
  }
49
49
  export { createOrderState } from './use-order.svelte.js';
50
+ // Folded from `./shop/svelte` (dropped as separate export in 0.20.0)
51
+ export { default as OrderStatus } from '../svelte/OrderStatus.svelte';
52
+ export { default as InpostPicker } from '../svelte/InpostPicker.svelte';
53
+ export { DEFAULT_LABELS_PL } from '../svelte/labels.js';
@@ -12,9 +12,29 @@ type SingleInput = Omit<SingleConfig, 'fields'> & {
12
12
  fields: Field[];
13
13
  layout?: Layout;
14
14
  };
15
+ /**
16
+ * Defines the root CMS configuration. Pass to `includioCMS()` in `hooks.server.ts`.
17
+ * @public
18
+ */
15
19
  export declare function defineConfig(config: CMSConfig): CMSConfig;
20
+ /**
21
+ * Defines a collection (multi-entry content type).
22
+ * @public
23
+ */
16
24
  export declare function defineCollection(config: CollectionInput): CollectionConfig;
25
+ /**
26
+ * Defines a singleton (single-entry content type, e.g. site settings).
27
+ * @public
28
+ */
17
29
  export declare function defineSingle(config: SingleInput): SingleConfig;
30
+ /**
31
+ * Defines a public form (submitted via `/api/forms/[slug]/submit`).
32
+ * @public
33
+ */
18
34
  export declare function defineForm(config: FormConfig): FormConfig;
35
+ /**
36
+ * Defines a reusable object field (nested record). Use inside `fields[]`.
37
+ * @public
38
+ */
19
39
  export declare function defineObject(config: Omit<ObjectField, 'type'>): ObjectField;
20
40
  export {};
@@ -1,15 +1,35 @@
1
+ /**
2
+ * Defines the root CMS configuration. Pass to `includioCMS()` in `hooks.server.ts`.
3
+ * @public
4
+ */
1
5
  export function defineConfig(config) {
2
6
  return config;
3
7
  }
8
+ /**
9
+ * Defines a collection (multi-entry content type).
10
+ * @public
11
+ */
4
12
  export function defineCollection(config) {
5
13
  return config;
6
14
  }
15
+ /**
16
+ * Defines a singleton (single-entry content type, e.g. site settings).
17
+ * @public
18
+ */
7
19
  export function defineSingle(config) {
8
20
  return config;
9
21
  }
22
+ /**
23
+ * Defines a public form (submitted via `/api/forms/[slug]/submit`).
24
+ * @public
25
+ */
10
26
  export function defineForm(config) {
11
27
  return config;
12
28
  }
29
+ /**
30
+ * Defines a reusable object field (nested record). Use inside `fields[]`.
31
+ * @public
32
+ */
13
33
  export function defineObject(config) {
14
34
  return { ...config, type: 'object' };
15
35
  }
@@ -1,3 +1,7 @@
1
1
  import { type Handle } from '@sveltejs/kit';
2
2
  import type { CMSConfig } from '../../types/cms.js';
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`.
5
+ * @public
6
+ */
3
7
  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
@@ -65,22 +68,30 @@ const detectBrowserContext = async ({ event, resolve }) => {
65
68
  };
66
69
  return resolve(event);
67
70
  };
71
+ /**
72
+ * 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`.
73
+ * @public
74
+ */
68
75
  export function includioCMS(cmsConfig) {
69
76
  generateRuntime(cmsConfig); // Generate runtime code based on the CMS config
70
77
  initCMS(cmsConfig);
71
78
  const handles = [];
72
79
  handles.push(detectBrowserContext);
80
+ handles.push(csrfGuard);
73
81
  if (cmsConfig.auth) {
74
82
  handles.push(handleAuth);
75
83
  }
84
+ handles.push(rateLimitGuard());
76
85
  handles.push(adminGuard);
77
86
  handles.push(securityHeaders);
78
87
  return handles;
79
88
  }
89
+ const CSP_VALUE = buildCspHeader();
80
90
  const securityHeaders = async ({ event, resolve }) => {
81
91
  const response = await resolve(event);
82
92
  response.headers.set('X-Content-Type-Options', 'nosniff');
83
93
  response.headers.set('X-Frame-Options', 'SAMEORIGIN');
84
94
  response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
95
+ response.headers.set('Content-Security-Policy', CSP_VALUE);
85
96
  return response;
86
97
  };
@@ -5,3 +5,5 @@ export { createFormSubmission } from '../../core/server/forms/submissions/operat
5
5
  export { parseFormDataForSubmission } from '../../core/server/forms/submissions/utils/parseMultipart.js';
6
6
  export { createConsentLog } from '../../core/server/consentLogs/operations/create.js';
7
7
  export { getPreviewEntry } from './preview.js';
8
+ export { createRestApiHandler } from '../../admin/api/rest/handler.js';
9
+ export { generateApiKey } from '../../admin/api/rest/middleware/generateApiKey.js';
@@ -5,3 +5,6 @@ export { createFormSubmission } from '../../core/server/forms/submissions/operat
5
5
  export { parseFormDataForSubmission } from '../../core/server/forms/submissions/utils/parseMultipart.js';
6
6
  export { createConsentLog } from '../../core/server/consentLogs/operations/create.js';
7
7
  export { getPreviewEntry } from './preview.js';
8
+ // Folded from `./admin/api/rest/handler` (dropped as separate export in 0.20.0)
9
+ export { createRestApiHandler } from '../../admin/api/rest/handler.js';
10
+ export { generateApiKey } from '../../admin/api/rest/middleware/generateApiKey.js';
@@ -1,4 +1,8 @@
1
1
  import type { RequestEvent } from '@sveltejs/kit';
2
+ /**
3
+ * Returns `cmsContext` from `event.locals` for use in a SvelteKit layout `load`. Drop into your `+layout.server.ts`.
4
+ * @public
5
+ */
2
6
  export declare function cmsLayoutLoad(event: RequestEvent): {
3
7
  cmsContext: any;
4
8
  };
@@ -1,3 +1,7 @@
1
+ /**
2
+ * Returns `cmsContext` from `event.locals` for use in a SvelteKit layout `load`. Drop into your `+layout.server.ts`.
3
+ * @public
4
+ */
1
5
  export function cmsLayoutLoad(event) {
2
6
  return {
3
7
  cmsContext: event.locals.cmsContext ?? {}
@@ -1,5 +1,9 @@
1
1
  import type { Entry } from '../../types/entries.js';
2
2
  import { type RequestEvent } from '@sveltejs/kit';
3
+ /**
4
+ * Resolves the preview entry from a `?preview=<versionId>` query param. Requires an authenticated session — throws `Unauthorized` otherwise.
5
+ * @public
6
+ */
3
7
  export declare function getPreviewEntry(event: RequestEvent, options: {
4
8
  language: string;
5
9
  }): Promise<Entry | null>;
@@ -1,5 +1,9 @@
1
1
  import { getEntryVersion } from '../../core/server/entries/operations/get.js';
2
2
  import {} from '@sveltejs/kit';
3
+ /**
4
+ * Resolves the preview entry from a `?preview=<versionId>` query param. Requires an authenticated session — throws `Unauthorized` otherwise.
5
+ * @public
6
+ */
3
7
  export async function getPreviewEntry(event, options) {
4
8
  const preview = event.url.searchParams.get('preview');
5
9
  if (!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;
@@ -14,3 +14,4 @@ export { type PluginConfig, type CustomFieldDefinition } from './plugins.js';
14
14
  export { type Language, type Localized } from './languages.js';
15
15
  export { type Layout, type LayoutNode, type LayoutPreset, type LayoutNodeType, type ColumnRatio, type SectionNode, type ColumnsNode, type CardNode, type AccordionNode, type StackNode } from './layout.js';
16
16
  export { type CmsContext } from './cms-context.js';
17
+ export { type UserData, type Breadcrumb } from '../admin/types.js';
@@ -14,3 +14,4 @@ export {} from './plugins.js';
14
14
  export {} from './languages.js';
15
15
  export {} from './layout.js';
16
16
  export {} from './cms-context.js';
17
+ export {} from '../admin/types.js';
@@ -3,6 +3,10 @@ import type { z } from 'zod';
3
3
  import type { RawEntry } from './entries.js';
4
4
  import type { CustomField } from './fields.js';
5
5
  import type { PopulateCtx } from '../core/server/entries/operations/resolveEntry.js';
6
+ /**
7
+ * Defines a custom field type contributed by a plugin.
8
+ * @experimental Plugin system finalizes in 1.x — shape may change.
9
+ */
6
10
  export interface CustomFieldDefinition {
7
11
  /** Unique field type slug, e.g. 'photo-grid' */
8
12
  fieldType: string;
@@ -20,6 +24,10 @@ export interface CustomFieldDefinition {
20
24
  */
21
25
  populateResolver?: (value: unknown, field: CustomField, ctx: PopulateCtx) => Promise<unknown>;
22
26
  }
27
+ /**
28
+ * CMS plugin configuration — register custom field types and CRUD lifecycle hooks.
29
+ * @experimental Plugin hooks API not finalized — may change in 1.x without breaking semver.
30
+ */
23
31
  export interface PluginConfig {
24
32
  slug: string;
25
33
  fields?: CustomFieldDefinition[];
@@ -0,0 +1,2 @@
1
+ import type { CmsUpdate } from '../index.js';
2
+ export declare const update: CmsUpdate;
@@ -0,0 +1,54 @@
1
+ export const update = {
2
+ version: '0.20.0',
3
+ date: '2026-04-29',
4
+ description: 'API Surface Lock — exports trim 26→15, JSDoc tagi (`@public`/`@experimental`/`@internal`), autogenerowany `API.md`. Powierzchnia publiczna zamrożona w v1.0 — w 1.x tylko `@experimental` może się zmienić bez breaking semver.',
5
+ features: [
6
+ '`API.md` w root — autogenerowany przez `scripts/generate-api-md.ts` (ts-morph). 15 entry pointów, ~345 stable, 2 experimental. Skrypt wpięty w `prepublishOnly`.',
7
+ 'Wszystkie publiczne `define*` helpery (`defineConfig`, `defineCollection`, `defineSingle`, `defineForm`, `defineObject`) oznaczone `@public`.',
8
+ '`getCMS`, `getAuth`, `createEntityAPI`, `resolveMediaWithStyles`, `includioCMS`, `cmsLayoutLoad`, `getPreviewEntry`, `createFormSubmission`, `parseFormDataForSubmission`, `createConsentLog`, `createRestApiHandler` — `@public`.',
9
+ '`PluginConfig` + `CustomFieldDefinition` oznaczone `@experimental` — plugin hooks API finalizujemy w 1.x.'
10
+ ],
11
+ fixes: [],
12
+ breakingChanges: [
13
+ '**`package.json` exports trim 26→15.** Usunięte / scalone entry pointy poniżej. Każdy import wymaga jednorazowej migracji (znajdź-i-zamień).',
14
+ '`includio-cms/admin` → `includio-cms/types` (typy `UserData`, `Breadcrumb` przeniesione do publicznych typów).',
15
+ '`includio-cms/admin/helpers` → `includio-cms/admin/client` (re-eksport — zmień tylko ścieżkę importu).',
16
+ '`includio-cms/admin/ui` → `includio-cms/admin/client` (re-eksport — zmień tylko ścieżkę importu).',
17
+ '`includio-cms/admin/client/account` → `includio-cms/admin/client` (re-eksport — `AccountPage` dostępny pod nową ścieżką).',
18
+ '`includio-cms/admin/api/*` (wildcard) → **usunięte**. Były to internalsy (media-gc, upload, transcode, replace) — nie powinny być publiczne. Brak migracji.',
19
+ '`includio-cms/admin/api/rest/handler` → `includio-cms/sveltekit/server` (`createRestApiHandler` — zmień ścieżkę importu).',
20
+ '`includio-cms/admin/client/*.svelte` (wildcard) → użyj nazwanych eksportów z `includio-cms/admin/client`.',
21
+ '`includio-cms/sveltekit/components/*.svelte` (wildcard) → użyj nazwanych eksportów z `includio-cms/sveltekit`.',
22
+ '`includio-cms/db-postgres/schema-core` + `includio-cms/db-postgres/schema-shop` + `includio-cms/auth-schema` → `includio-cms/db-postgres` (jeden barrel — wszystkie tabele, w tym auth, dostępne pod jedną ścieżką).',
23
+ '`includio-cms/auth` → `includio-cms/core` (`getAuth` — zmień ścieżkę importu).',
24
+ '`includio-cms/entity` → `includio-cms/core` (`createEntityAPI` — zmień ścieżkę importu).',
25
+ '`includio-cms/shop/svelte` → `includio-cms/shop/client` (`OrderStatus`, `InpostPicker`, `DEFAULT_LABELS_PL`, `OrderStatusLabels` — zmień ścieżkę importu).',
26
+ '`includio-cms/updates` → **usunięte** (internal CLI tooling, nigdy nie powinno być w runtime API).'
27
+ ],
28
+ notes: `Migracja (znajdź-i-zamień):
29
+
30
+ \`\`\`bash
31
+ # Przykładowe sed'y (dostosuj do swojego edytora):
32
+ sed -i '' "s|'includio-cms/admin/helpers'|'includio-cms/admin/client'|g" src/**/*.ts
33
+ sed -i '' "s|'includio-cms/admin/ui'|'includio-cms/admin/client'|g" src/**/*.ts
34
+ sed -i '' "s|'includio-cms/auth'|'includio-cms/core'|g" src/**/*.ts
35
+ sed -i '' "s|'includio-cms/entity'|'includio-cms/core'|g" src/**/*.ts
36
+ sed -i '' "s|'includio-cms/shop/svelte'|'includio-cms/shop/client'|g" src/**/*.ts
37
+ sed -i '' "s|'includio-cms/auth-schema'|'includio-cms/db-postgres'|g" src/**/*.ts
38
+ sed -i '' "s|'includio-cms/db-postgres/schema-core'|'includio-cms/db-postgres'|g" src/**/*.ts
39
+ sed -i '' "s|'includio-cms/db-postgres/schema-shop'|'includio-cms/db-postgres'|g" src/**/*.ts
40
+ sed -i '' "s|'includio-cms/admin/api/rest/handler'|'includio-cms/sveltekit/server'|g" src/**/*.ts
41
+ \`\`\`
42
+
43
+ Po migracji uruchom \`pnpm check\` — TypeScript zweryfikuje czy wszystkie symbole są dostępne pod nową ścieżką.
44
+
45
+ API Surface freeze v1.0:
46
+
47
+ - **\`@public\`** — stabilne, semver-protected w v1.0. Breaking change wymaga major bump.
48
+ - **\`@experimental\`** — może się zmienić w 1.x bez breaking semver (np. \`PluginConfig\` — plugin hooks API).
49
+ - **\`@internal\`** — implementation detail, NIE importować. Nie listowane w \`API.md\`.
50
+
51
+ Pełna lista publicznych symboli per entry point: \`API.md\` w root paczki (regenerowany przy każdym \`npm publish\`).
52
+
53
+ Brak SQL migration. Zmiana czysto package-level.`
54
+ };
@@ -0,0 +1,2 @@
1
+ import type { CmsUpdate } from '../index.js';
2
+ export declare const update: CmsUpdate;
@@ -0,0 +1,55 @@
1
+ export const update = {
2
+ version: '0.21.0',
3
+ date: '2026-04-30',
4
+ description: 'Faza 5 część 2 — security finish: form rate-limit DRY refactor, API keys `expiresAt` / `rotatedAt`, sharp timeout 30s, `{@html}` audit close. `KNOWN-RISKS.md` w root jako single source of truth dla zaakceptowanych ryzyk v1.0. Plus: faza 6 setup — vitest coverage (info-only), docker test profile, integration test scaffolding (`tests/`).',
5
+ features: [
6
+ '`KNOWN-RISKS.md` w root paczki — 5 ryzyk udokumentowanych: CSP `\'unsafe-inline\'`, API key rotation opt-in, in-memory rate-limit, sharp 30s timeout, ffmpeg/sharp args audit. Każde z mitygacją i triggerem fix.',
7
+ '`ApiKeyConfig.expiresAt?: string` (ISO-8601) — opt-in expiry, enforced w `validateApiKey()`. Wygasły klucz → 401 generic (no leak). Brak `expiresAt` = klucz nigdy nie wygasa (backward compat).',
8
+ '`ApiKeyConfig.rotatedAt?: string` — info-only audit-trail, nie enforced.',
9
+ '`generateApiKey()` (`includio-cms/sveltekit/server`) — crypto-random 32B base64url. Pełna rotacja = update `cms.config.ts` + redeploy (statyczny model, patrz KNOWN-RISKS §2).',
10
+ '`withTimeout<T>(promise, ms, label)` + `TimeoutError` (`$lib/server/utils/withTimeout`). Wrap dookoła wszystkich sharp calls (metadata, toBuffer, blur, admin thumbnail, downscale resize). Default 30s, env override `INCLUDIO_SHARP_TIMEOUT_MS`.',
11
+ 'Form submit rate-limit: refaktor `src/routes/api/forms/[slug]/submit/+server.ts` — używa shared `MemoryRateLimitStore` z `$lib/server/security/rate-limit.js` (DRY z `/admin/api/*` rate-limit). Limity 5/h per IP zachowane. Env: `INCLUDIO_FORM_RATE_LIMIT_MAX`, `INCLUDIO_FORM_RATE_LIMIT_WINDOW_MS`.',
12
+ 'Faza 6 setup: docker `test` profile (`db_test` na porcie 5434, tmpfs, `fsync=off`), `tests/helpers/{db,api,auth}.ts`, `tests/setup.ts` z TRUNCATE strategy, vitest project `integration` (sequential, `singleFork`), sanity test `tests/integration/db-sanity.spec.ts`.',
13
+ 'Vitest coverage config (info-only, bez threshold) — `pnpm test:coverage` generuje `coverage/`. Reporter: text/json/html. Devdep: `@vitest/coverage-v8`.'
14
+ ],
15
+ fixes: [
16
+ '`{@html}` w `src/lib/admin/client/users/delete-user-dialog.svelte` (linie 85, 93) zastąpione safe Svelte template `{...}<strong>{...}</strong>{...}`. Defense-in-depth — content był admin-controlled, ale eliminuje wektor regresji XSS gdyby ktoś kiedyś dodał user-supplied input do tych komunikatów.'
17
+ ],
18
+ breakingChanges: [
19
+ 'Brak hard breakages. `ApiKeyConfig.expiresAt` / `rotatedAt` są opcjonalne — istniejące configi działają bez zmian.',
20
+ '`usersLang.deleteWarningDesc` zmienia sygnaturę z `(name: string) => string` na statyczny obiekt `{ before: string; after: string }`. `usersLang.deleteConfirmType` analogicznie ze stringa na `{ before: string; after: string }`. Wpływ tylko na fork\'i admin UI z customowym lang — szybki `pnpm check` zwróci błąd typu, fix = przepisz wartości w lang.'
21
+ ],
22
+ notes: `## Setup integration tests (opt-in)
23
+
24
+ \`\`\`bash
25
+ docker compose --profile test up -d db_test
26
+ pnpm prepack # buduje dist/ wymagany przez drizzle-kit push schema
27
+ pnpm db:test:migrate # drizzle-kit push do test DB
28
+ pnpm test:integration # uruchamia tests/integration/**
29
+ docker compose --profile test down
30
+ \`\`\`
31
+
32
+ ## API key expiry (opt-in)
33
+
34
+ \`\`\`ts
35
+ // cms.config.ts
36
+ apiKeys: [
37
+ { key: 'sk-...', name: 'ci', role: 'admin', expiresAt: '2027-01-01T00:00:00Z' }
38
+ ]
39
+ \`\`\`
40
+
41
+ Generowanie nowego klucza:
42
+
43
+ \`\`\`ts
44
+ import { generateApiKey } from 'includio-cms/sveltekit/server';
45
+ console.log(generateApiKey()); // → 43-char base64url, 32B entropii
46
+ \`\`\`
47
+
48
+ Pełna rotacja wymaga update'u \`cms.config.ts\` + redeploy. Pole \`rotatedAt\` jest info-only (audit trail) — admin ustawia ręcznie przy rotacji.
49
+
50
+ ## Known risks
51
+
52
+ Lista zaakceptowanych ryzyk v1.0 — \`KNOWN-RISKS.md\` w root paczki. 5 sekcji: CSP \`unsafe-inline\`, API key rotation opt-in, in-memory rate-limit (multi-node = wymaga Redis adapter), sharp 30s timeout, ffmpeg/sharp shell audit (SAFE).
53
+
54
+ Brak SQL migration.`
55
+ };
@@ -53,7 +53,9 @@ import { update as update0155 } from './0.15.5/index.js';
53
53
  import { update as update0160 } from './0.16.0/index.js';
54
54
  import { update as update0180 } from './0.18.0/index.js';
55
55
  import { update as update0190 } from './0.19.0/index.js';
56
- 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, update0140, update0141, update0142, update0143, update0144, update0145, update0146, update0150, update0151, update0152, update0153, update0154, update0155, update0160, update0180, update0190];
56
+ import { update as update0200 } from './0.20.0/index.js';
57
+ import { update as update0210 } from './0.21.0/index.js';
58
+ 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, update0140, update0141, update0142, update0143, update0144, update0145, update0146, update0150, update0151, update0152, update0153, update0154, update0155, update0160, update0180, update0190, update0200, update0210];
57
59
  export const getUpdatesFrom = (fromVersion) => {
58
60
  const fromParts = fromVersion.split('.').map(Number);
59
61
  return updates.filter((update) => {