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
package/API.md CHANGED
@@ -1,8 +1,8 @@
1
- # includio-cms — Public API v0.20.0
1
+ # includio-cms — Public API v0.22.0
2
2
 
3
3
  > Auto-generated by `scripts/generate-api-md.ts`. Do not edit by hand.
4
4
 
5
- Entry points: **15** · Stable: **345** · Experimental: **2**
5
+ Entry points: **15** · Stable: **346** · Experimental: **2**
6
6
 
7
7
  Tags:
8
8
  - `@public` — frozen for v1.0; semver-protected.
@@ -13,19 +13,19 @@ Tags:
13
13
 
14
14
  ### `includio-cms`
15
15
 
16
- - `createEntityAPI(cms: CMS, opts?: EntityAPIOptions): <inferred>` — Creates a high-level Entity API (CRUD + publish/archive) bound to a CMS instance and a user.
16
+ - `createEntityAPI(cms: CMS, opts?: EntityAPIOptions): <inferred>` — Creates a high-level Entity API (CRUD + publish/archive) bound to a CMS
17
17
  - `getAuth(): <inferred>` — Returns the underlying `better-auth` instance from the initialized CMS.
18
- - `getCMS(): CMS` — Returns the singleton CMS instance. Must be called after `includioCMS()` initializes the CMS in `hooks.server.ts`.
19
- - `interface ResolvedMedia`
20
- - `resolveMediaWithStyles(mediaIds: string[], styles?: ImageFieldStyle[]): Promise<Record<string, ResolvedMedia>>` — Resolve media files by IDs and generate image styles. Useful for plugins resolving media references (e.g. photo-grid).
18
+ - `getCMS(): CMS` — Returns the singleton CMS instance. Must be called after `includioCMS()`
19
+ - `interface ResolvedMedia` — Resolved media file plus its image styles + blur placeholder. Returned by
20
+ - `resolveMediaWithStyles(mediaIds: string[], styles?: ImageFieldStyle[]): Promise<Record<string, ResolvedMedia>>` — Resolve media files by IDs and generate image styles. Useful for plugins or
21
21
 
22
22
  ### `includio-cms/core`
23
23
 
24
- - `createEntityAPI(cms: CMS, opts?: EntityAPIOptions): <inferred>` — Creates a high-level Entity API (CRUD + publish/archive) bound to a CMS instance and a user.
24
+ - `createEntityAPI(cms: CMS, opts?: EntityAPIOptions): <inferred>` — Creates a high-level Entity API (CRUD + publish/archive) bound to a CMS
25
25
  - `getAuth(): <inferred>` — Returns the underlying `better-auth` instance from the initialized CMS.
26
- - `getCMS(): CMS` — Returns the singleton CMS instance. Must be called after `includioCMS()` initializes the CMS in `hooks.server.ts`.
27
- - `interface ResolvedMedia`
28
- - `resolveMediaWithStyles(mediaIds: string[], styles?: ImageFieldStyle[]): Promise<Record<string, ResolvedMedia>>` — Resolve media files by IDs and generate image styles. Useful for plugins resolving media references (e.g. photo-grid).
26
+ - `getCMS(): CMS` — Returns the singleton CMS instance. Must be called after `includioCMS()`
27
+ - `interface ResolvedMedia` — Resolved media file plus its image styles + blur placeholder. Returned by
28
+ - `resolveMediaWithStyles(mediaIds: string[], styles?: ImageFieldStyle[]): Promise<Record<string, ResolvedMedia>>` — Resolve media files by IDs and generate image styles. Useful for plugins or
29
29
 
30
30
  ### `includio-cms/types`
31
31
 
@@ -239,11 +239,11 @@ Tags:
239
239
  ### `includio-cms/sveltekit`
240
240
 
241
241
  - `const CmsProvider: LegacyComponentType`
242
- - `defineCollection(config: CollectionInput): CollectionConfig` — Defines a collection (multi-entry content type).
242
+ - `defineCollection(config: CollectionInput): CollectionConfig` — Defines a collection (multi-entry content type, e.g. blog posts, products).
243
243
  - `defineConfig(config: CMSConfig): CMSConfig` — Defines the root CMS configuration. Pass to `includioCMS()` in `hooks.server.ts`.
244
- - `defineForm(config: FormConfig): FormConfig` — Defines a public form (submitted via `/api/forms/[slug]/submit`).
245
- - `defineObject(config: Omit<ObjectField, 'type'>): ObjectField` — Defines a reusable object field (nested record). Use inside `fields[]`.
246
- - `defineSingle(config: SingleInput): SingleConfig` — Defines a singleton (single-entry content type, e.g. site settings).
244
+ - `defineForm(config: FormConfig): FormConfig` — Defines a public form (submitted via `POST /api/forms/[slug]/submit`).
245
+ - `defineObject(config: Omit<ObjectField, 'type'>): ObjectField` — Defines a reusable object field (nested record). Use inline inside `fields[]`
246
+ - `defineSingle(config: SingleInput): SingleConfig` — Defines a singleton (single-entry content type, e.g. site settings, homepage).
247
247
  - `enableHybridEditing(): <inferred>` — Call in a layout/component script to enable data-hybrid-path rendering for all descendant HybridTarget/Image/Video.
248
248
  - `extractBlocks(doc: StructuredContentDoc, type?: string): SCNode[]` — Extract block-level nodes, optionally filtered by type.
249
249
  - `extractInlineBlocks(doc: StructuredContentDoc, blockType?: string): SCInlineBlockAttrs[]` — Extract inline block nodes, optionally filtered by blockType.
@@ -267,15 +267,16 @@ Tags:
267
267
 
268
268
  ### `includio-cms/sveltekit/server`
269
269
 
270
- - `cmsLayoutLoad(event: RequestEvent): <inferred>` — Returns `cmsContext` from `event.locals` for use in a SvelteKit layout `load`. Drop into your `+layout.server.ts`.
271
- - `countEntries(opts: CountEntriesOptions): Promise<number>` — Count entries matching the same filters as `resolveEntries`, without populating.
270
+ - `cmsLayoutLoad(event: RequestEvent): <inferred>` — Returns `cmsContext` from `event.locals` for use in a SvelteKit layout
271
+ - `countEntries(opts: CountEntriesOptions): Promise<number>` — Count entries matching the same filters as `resolveEntries`, without
272
272
  - `type CountEntriesOptions = Omit< ResolveEntriesOptions, 'limit' | 'offset' | 'populate' | 'orderBy' | 'dataOrderBy' >`
273
273
  - `const createConsentLog: <inferred>`
274
274
  - `const createFormSubmission: <inferred>`
275
- - `createRestApiHandler(): <inferred>` — REST API handler factory. Returns `{ GET, POST, PUT, DELETE }` `RequestHandler`s authenticated via `x-api-key` header. Mount in `src/routes/admin/api/rest/[...restPath]/+server.ts`.
276
- - `getPreviewEntry(event: RequestEvent, options: { language: string }): Promise<Entry | null>` Resolves the preview entry from a `?preview=<versionId>` query param. Requires an authenticated session — throws `Unauthorized` otherwise.
277
- - `includioCMS(cmsConfig: CMSConfig): Handle[]` 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`.
278
- - `parseFormDataForSubmission(formData: FormData, fields: FormField[]): Promise<Record<string, unknown>>` Parses multipart `FormData` into a typed record of field values, handling file uploads via the configured files adapter.
275
+ - `createRestApiHandler(): <inferred>` — REST API handler factory. Returns `{ GET, POST, PUT, DELETE }`
276
+ - `generateApiKey(): string`Generate a cryptographically random API key (32 bytes, base64url-encoded).
277
+ - `getPreviewEntry(event: RequestEvent, options: { language: string }): Promise<Entry | null>` Resolves the preview entry from a `?preview=<versionId>` query param.
278
+ - `includioCMS(cmsConfig: CMSConfig): Handle[]`SvelteKit `Handle[]` array that initializes the CMS, generates runtime
279
+ - `parseFormDataForSubmission(formData: FormData, fields: FormField[]): Promise<Record<string, unknown>>` — Parses multipart `FormData` into a typed record of field values, handling
279
280
  - `type PopulateConfig = { /** Hard cap on relation depth. Default 5. Use `0` to keep all relations as raw IDs. */ maxDept...` — Recursion + per-field opt-out config for relation population.
280
281
  - `resolveEntries(opts: ResolveEntriesOptions): Promise<Entry[]>` — Fetch a list of populated Entries from a collection (or singleton with multiple instances).
281
282
  - `interface ResolveEntriesOptions`
@@ -386,7 +387,7 @@ Tags:
386
387
  ### `includio-cms/files-local`
387
388
 
388
389
  - `const fullDir: <inferred>`
389
- - `local(config?: LocalFilesConfig): FilesAdapter` — Local-disk files adapter. Stores uploads under `./static/uploads` (dev) or `/data/uploads` (prod).
390
+ - `local(config?: LocalFilesConfig): FilesAdapter` — Local-disk files adapter. Stores uploads under `./static/uploads` (dev) or
390
391
  - `interface LocalFilesConfig`
391
392
 
392
393
  ### `includio-cms/email-nodemailer`
package/CHANGELOG.md CHANGED
@@ -3,6 +3,153 @@
3
3
  All notable changes to includio-cms are documented here.
4
4
  Generated from `src/lib/updates/` — do not edit manually.
5
5
 
6
+ ## 0.22.0 — 2026-04-30
7
+
8
+ Faza 9 — DX & config validation pass. `defineConfig()` waliduje config strict Zodem z czytelnymi błędami (path + hint), resolvery / operacje throwują typowane `CmsError` z `code` + `context`, CLI ma `--help` per subcommand i `--version`, README przepisany pod nowych userów (system requirements + 5-min quickstart), `.env.example` rozszerzony o wszystkie `INCLUDIO_*` envy, JSDoc na każdym `@public` symbolu (opis + `@param` + `@returns` + `@example`).
9
+
10
+ ### Added
11
+ - `defineConfig()` (`includio-cms/sveltekit`) — runtime Zod validation z agregacją wszystkich błędów. Invalid config → `ConfigValidationError` (extends `CmsError`) z `issues[]` (path + message + hint), bullet-list message ready-to-paste do PR. `superRefine`: unique slugs (collections/singles/forms), default locale ≤ 1, relation targets w deklarowanych kolekcjach, `apiKeys[].permissions` referuje istniejące collection slugs.
12
+ - `CmsError` + `ConfigValidationError` (`includio-cms` → `core/errors`) — bazowa klasa z `code` (SCREAMING_SNAKE_CASE), `context: Record<string, unknown>`, `cause?`. Stabilne kody: `ENTRY_NOT_FOUND`, `ENTRY_VERSION_NOT_FOUND`, `INVALID_DATA`, `MISSING_REQUIRED_PARAM`, `CONFIG_VALIDATION_FAILED`. `toString()` = `[CODE] message (k=v, k=v)`.
13
+ - `formatZodDataIssues(error)` helper — render Zod errora z entry/form data jako lista `path: message`. Używany w `createEntryVersion`, `updateEntryVersion`, `createFormSubmission`.
14
+ - CLI: `includio --help`, `includio scaffold admin --help`, `includio install-peers --help`, `includio create-user --help` — exit 0 + Usage/Options/Example block. `includio --version` / `-v` z `package.json`. Unknown command → exit 1 + top-level help.
15
+ - `.env.example` rozszerzony — 11 envów z komentarzami i sekcjami (Required / Media & Uploads / Security & Rate Limits). Demo vars usunięte (były tylko dla `src/lib/demo/` skasowanego w Fazie 1).
16
+ - README rebuild: TOC, sekcja **System requirements** (Node 18+, PG 14+, ffmpeg optional), 5-step **Quick start** (install → .env → config → scaffold → create-user → dev), poprawiony link do `DOCS.md`, dodana tabelka adapterów, sekcja CLI z subcommandami.
17
+ - JSDoc bodies (opis + `@param` + `@returns` + `@example`) na każdym `@public` symbolu — `defineConfig`, `defineCollection`, `defineSingle`, `defineForm`, `defineObject`, `getCMS`, `resolveEntry`, `resolveEntries`, `countEntries`, `createFormSubmission`, `parseFormDataForSubmission`, `createConsentLog`, `resolveMediaWithStyles`, `createEntityAPI`, `createRestApiHandler`, `generateApiKey`, `getAuth`, `includioCMS`, `getPreviewEntry`, `cmsLayoutLoad`, factory adapterów (`pg`, `local`, `nodemailerAdapter`, `openAIAdapter`, `claudeAdapter`).
18
+
19
+ ### Fixed
20
+ - `MISSING_REQUIRED_PARAM` zamiast `throw new Error('... is required')` w `resolveEntry` / `resolveEntries` / `countEntries`. Context zawiera `op` + `missing`.
21
+ - `INVALID_DATA` zamiast `throw Error('Invalid data: ' + JSON.stringify(parsedData.error.flatten()))` w `createEntryVersion` / `updateEntryVersion` / `createFormSubmission`. Context zawiera `collection`/`entryId`/`lang` (entries) lub `formSlug` (forms). Body errora to czytelna lista `field.path: message`.
22
+ - `ENTRY_NOT_FOUND` z `{ collection, id }` w `_getDbEntryOrThrow` / `_getRawEntryOrThrow`; `ENTRY_VERSION_NOT_FOUND` z `{ versionId, entryId, lang }` w `_getDbEntryVersionOrThrow`.
23
+
24
+ ### Breaking
25
+ - **Strict config validation w `defineConfig`** — invalid configs które wcześniej "działały" w runtime (np. duplicate slug, brak default locale, relation do nieistniejącej kolekcji, brak metody w adapterze) **teraz throwują `ConfigValidationError` na starcie**. Najczęstsze przypadki + fix:
26
+ - **Duplicate slug**: dwie kolekcje/singles/forms z tym samym `slug` → unique slug per kategoria.
27
+ - **Invalid language code**: `code: 'ENGLISH'` → `code: 'en'` (ISO-639-1, opcjonalnie z regionem `'pl-PL'`).
28
+ - **Multiple default locales**: dwa `{ default: true }` → tylko jeden.
29
+ - **Missing adapter method**: stub adaptera bez `getEntries` etc. → zaimportuj pełen `pg()` / `local()` lub doimplementuj brakujące metody.
30
+ - **Invalid relation target**: `{ type: 'relation', collection: 'authors' }` przy braku kolekcji `authors` → dodaj kolekcję lub popraw target.
31
+ - **Resolver / operation error format** — błędy z `resolveEntry` / `resolveEntries` / `countEntries` / `createEntryVersion` / `updateEntryVersion` / `createFormSubmission` rzucają teraz `CmsError` (extends `Error`). `error.message` zachowuje stary opis (przy `toString()` poprzedzony `[CODE]`) — string-matching `error.message === 'Entry not found'` może się rozjechać. **Migracja:**
32
+ ```ts
33
+ import { CmsError } from 'includio-cms';
34
+
35
+ try { await resolveEntry({ id }); }
36
+ catch (e) {
37
+ if (e instanceof CmsError && e.code === 'ENTRY_NOT_FOUND') { ... }
38
+ }
39
+ ```
40
+ - **`.env.example` reformatted** — `DEMO_USER_EMAIL`, `DEMO_USER_PASSWORD`, `DEMO_RESET_KEY` usunięte (były tylko dla wewnętrznego demo skasowanego w Fazie 1). Jeśli polegałeś na nich → trzymaj w lokalnym `.env`, nie w `.env.example`.
41
+
42
+ ### Notes
43
+
44
+ ## Czytelność błędów config
45
+
46
+ Po update — invalid config wyrzuci na starcie:
47
+
48
+ ```
49
+ ConfigValidationError: CMSConfig validation failed (2 issues):
50
+ - languages[0]: must be a 2-letter ISO code (e.g. 'en' or 'pl-PL')
51
+ - collections[1].slug: duplicate collection slug 'posts' — Hint: each collection/single/form must have a unique `slug`
52
+ ```
53
+
54
+ Każdy issue ma `path` (JS-notation: `collections[1].slug`, `apiKeys[0].permissions[2]`) i `message`. Tam gdzie da się rozsądnie zgadnąć, dochodzi `Hint:` z fix-suggestion.
55
+
56
+ ## Pattern: branchowanie po error code
57
+
58
+ Stary kod string-matchujący po `error.message` musi się przepisać:
59
+
60
+ ```ts
61
+ // PRZED
62
+ catch (e) {
63
+ if ((e as Error).message === 'Entry not found') { /* 404 */ }
64
+ }
65
+
66
+ // PO
67
+ import { CmsError } from 'includio-cms';
68
+ catch (e) {
69
+ if (e instanceof CmsError && e.code === 'ENTRY_NOT_FOUND') { /* 404 */ }
70
+ }
71
+ ```
72
+
73
+ Dostępne kody w v0.22.0: `ENTRY_NOT_FOUND`, `ENTRY_VERSION_NOT_FOUND`, `INVALID_DATA`, `MISSING_REQUIRED_PARAM`, `CONFIG_VALIDATION_FAILED`.
74
+
75
+ ## Nowe envy (opt-in)
76
+
77
+ Skopiuj `node_modules/includio-cms/.env.example` do swojego `.env` — domyślne wartości działają OOTB:
78
+
79
+ - `INCLUDIO_SHARP_TIMEOUT_MS` (default 30000)
80
+ - `INCLUDIO_RATE_LIMIT_MAX` / `INCLUDIO_RATE_LIMIT_WINDOW_MS` (default 200/60000)
81
+ - `INCLUDIO_FORM_RATE_LIMIT_MAX` / `INCLUDIO_FORM_RATE_LIMIT_WINDOW_MS` (default 5/3600000)
82
+ - `INCLUDIO_CSRF_ALLOWED_ORIGINS` (default: same-origin only)
83
+
84
+ ## CLI helpy
85
+
86
+ ```bash
87
+ includio --help
88
+ includio scaffold admin --help
89
+ includio install-peers --help
90
+ includio create-user --help
91
+ includio --version
92
+ ```
93
+
94
+ Brak SQL migration.
95
+
96
+ ## 0.21.0 — 2026-04-30
97
+
98
+ 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/`).
99
+
100
+ ### Added
101
+ - `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.
102
+ - `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).
103
+ - `ApiKeyConfig.rotatedAt?: string` — info-only audit-trail, nie enforced.
104
+ - `generateApiKey()` (`includio-cms/sveltekit/server`) — crypto-random 32B base64url. Pełna rotacja = update `cms.config.ts` + redeploy (statyczny model, patrz KNOWN-RISKS §2).
105
+ - `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`.
106
+ - 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`.
107
+ - 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`.
108
+ - Vitest coverage config (info-only, bez threshold) — `pnpm test:coverage` generuje `coverage/`. Reporter: text/json/html. Devdep: `@vitest/coverage-v8`.
109
+
110
+ ### Fixed
111
+ - `{@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.
112
+
113
+ ### Breaking
114
+ - Brak hard breakages. `ApiKeyConfig.expiresAt` / `rotatedAt` są opcjonalne — istniejące configi działają bez zmian.
115
+ - `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.
116
+
117
+ ### Notes
118
+
119
+ ## Setup integration tests (opt-in)
120
+
121
+ ```bash
122
+ docker compose --profile test up -d db_test
123
+ pnpm prepack # buduje dist/ wymagany przez drizzle-kit push schema
124
+ pnpm db:test:migrate # drizzle-kit push do test DB
125
+ pnpm test:integration # uruchamia tests/integration/**
126
+ docker compose --profile test down
127
+ ```
128
+
129
+ ## API key expiry (opt-in)
130
+
131
+ ```ts
132
+ // cms.config.ts
133
+ apiKeys: [
134
+ { key: 'sk-...', name: 'ci', role: 'admin', expiresAt: '2027-01-01T00:00:00Z' }
135
+ ]
136
+ ```
137
+
138
+ Generowanie nowego klucza:
139
+
140
+ ```ts
141
+ import { generateApiKey } from 'includio-cms/sveltekit/server';
142
+ console.log(generateApiKey()); // → 43-char base64url, 32B entropii
143
+ ```
144
+
145
+ Pełna rotacja wymaga update'u `cms.config.ts` + redeploy. Pole `rotatedAt` jest info-only (audit trail) — admin ustawia ręcznie przy rotacji.
146
+
147
+ ## Known risks
148
+
149
+ 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).
150
+
151
+ Brak SQL migration.
152
+
6
153
  ## 0.20.0 — 2026-04-29
7
154
 
8
155
  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.
package/DOCS.md CHANGED
@@ -1,4 +1,4 @@
1
- # Includio CMS Documentation (v0.20.0)
1
+ # Includio CMS Documentation (v0.22.0)
2
2
 
3
3
  > This file is auto-generated from the docs site. For the latest version, update the package.
4
4
 
package/README.md CHANGED
@@ -1,57 +1,149 @@
1
- # Includio CMS
1
+ # includio-cms
2
2
 
3
- A headless CMS built for SvelteKit. Type-safe, extensible, with a modern admin interface.
3
+ [![CI](https://github.com/includio/includio-cms/actions/workflows/ci.yml/badge.svg)](https://github.com/includio/includio-cms/actions/workflows/ci.yml)
4
+
5
+ A headless CMS built for SvelteKit. Type-safe, extensible, with a modern admin interface and pluggable adapters for database, files, email, and AI.
6
+
7
+ ## Contents
8
+
9
+ - [Features](#features)
10
+ - [System requirements](#system-requirements)
11
+ - [Quick start (5 minutes)](#quick-start-5-minutes)
12
+ - [Configuration](#configuration)
13
+ - [Adapters](#adapters)
14
+ - [Writing your own adapter](#writing-your-own-adapter)
15
+ - [Optional peer dependencies](#optional-peer-dependencies)
16
+ - [CLI](#cli)
17
+ - [Links](#links)
18
+ - [For AI assistants](#for-ai-assistants)
19
+ - [License](#license)
4
20
 
5
21
  ## Features
6
22
 
7
- - **19 field types** — text, structured content, media, relations, blocks, SEO, and more
23
+ - **20 field types** — text, structured content, media, relations, blocks, SEO, URL, custom plugins, and more
8
24
  - **Structured content** — ProseMirror-based rich text with inline blocks, tables, media embeds
9
25
  - **Media management** — image styles (Sharp), video transcoding (ffmpeg), focal points, blur placeholders
10
- - **Per-language versioning** — independent draft/published/scheduled per language
26
+ - **Per-language versioning** — independent draft / published / scheduled per language
11
27
  - **Layout DSL** — organize admin editor with sections, columns, cards, accordions
12
28
  - **Frontend components** — `<Image>`, `<Video>`, `<StructuredContent>`, `<Media>` for SvelteKit
13
29
  - **REST API** — API key auth, CRUD endpoints, schema introspection
14
30
  - **Entity API** — server-side programmatic CRUD for scripts, migrations, webhooks
15
31
  - **Code generation** — TypeScript types and Zod schemas from your content schema
16
- - **Pluggable adapters** — swap database, file storage, email, and AI providers
32
+ - **Pluggable adapters** — swap database, file storage, email, AI provider
17
33
  - **Plugin system** — lifecycle hooks and custom field types
18
- - **Video transcoding** — auto-transcode to mp4/webm with background processing
34
+ - **Strict config validation** — invalid configs throw a `ConfigValidationError` listing every issue with a path and a fix hint
35
+
36
+ ## System requirements
37
+
38
+ - **Node.js** 18+ (20 LTS recommended)
39
+ - **PostgreSQL** 14+ — required by the bundled `db-postgres` adapter; or write your own [`DatabaseAdapter`](#writing-your-own-adapter)
40
+ - **ffmpeg + ffprobe** — optional, only required for video transcoding (set `FFMPEG_PATH` / `FFPROBE_PATH` if not on `PATH`)
41
+ - A SvelteKit project (Kit ≥ 2.48, Svelte ≥ 5.43, Vite ≥ 7)
42
+
43
+ ## Quick start (5 minutes)
19
44
 
20
- ## Quick Start
45
+ The five steps below take you from `pnpm add` to a running admin at `/admin`.
46
+
47
+ **1. Install:**
21
48
 
22
49
  ```bash
23
50
  pnpm add includio-cms
51
+ pnpm dlx includio install-peers
52
+ ```
53
+
54
+ **2. Configure environment:**
55
+
56
+ ```bash
57
+ cp node_modules/includio-cms/.env.example .env
58
+ # edit .env — set DATABASE_URL and BETTER_AUTH_SECRET
24
59
  ```
25
60
 
26
- ```typescript
61
+ Generate a secret:
62
+
63
+ ```bash
64
+ openssl rand -hex 32
65
+ ```
66
+
67
+ **3. Create `src/lib/cms/cms.config.ts`:**
68
+
69
+ ```ts
27
70
  import { defineConfig } from 'includio-cms/sveltekit';
28
71
  import { pg } from 'includio-cms/db-postgres';
29
72
  import { local } from 'includio-cms/files-local';
30
73
 
31
74
  export default defineConfig({
32
75
  languages: ['en'],
33
- db: pg({ databaseUrl: process.env.DATABASE_URL }),
76
+ db: pg({ databaseUrl: process.env.DATABASE_URL! }),
34
77
  files: local(),
35
- collections: [/* ... */],
78
+ auth: { secret: process.env.BETTER_AUTH_SECRET! },
79
+ collections: []
36
80
  });
37
81
  ```
38
82
 
39
- See the [full documentation](/docs) for installation, configuration, and usage.
83
+ **4. Scaffold admin routes + create a user:**
40
84
 
41
- ## Writing your own adapter
85
+ ```bash
86
+ pnpm dlx includio scaffold admin
87
+ pnpm dlx includio create-user
88
+ ```
42
89
 
43
- `includio-cms` ships with default adapters (`db-postgres`, `files-local`, `email-nodemailer`, `ai-openai`, `ai-claude`). Each is a factory returning an object that implements a `@public` contract from `includio-cms/types`. Build your own by implementing the same interface — TypeScript will enforce the surface.
90
+ **5. Run the dev server:**
44
91
 
45
- Four adapter contracts:
92
+ ```bash
93
+ pnpm dev
94
+ ```
46
95
 
47
- - `DatabaseAdapter` entries, versions, media, tags, form submissions, consent logs.
48
- - `FilesAdapter` — file upload/download/list, optional private files (e.g. shop receipts).
49
- - `EmailAdapter` `sendMail`, default from address/name.
50
- - `AIAdapter` — currently `generateAltText(fileId)`.
96
+ Open `http://localhost:5173/admin` and sign in with the user you just created.
97
+
98
+ > Need more? Full reference in [`DOCS.md`](./DOCS.md). API surface in [`API.md`](./API.md).
99
+
100
+ ## Configuration
101
+
102
+ `defineConfig({ ... })` is your single source of truth. The shape (abridged):
103
+
104
+ ```ts
105
+ defineConfig({
106
+ languages: ['en', 'pl'], // first entry is the default locale
107
+ db, files, // required adapters
108
+ email, ai, // optional adapters
109
+ auth: { secret }, // required for /admin
110
+ collections: [postsCollection],
111
+ singles: [siteSettings],
112
+ forms: [contactForm],
113
+ apiKeys: [{ key: '...', role: 'admin' }],
114
+ media: { /* sharp / video / maintenance options */ },
115
+ typography: { fixOrphans: true },
116
+ shop, cmp // optional modules
117
+ });
118
+ ```
119
+
120
+ Invalid configs throw immediately at startup with every issue listed:
121
+
122
+ ```
123
+ ConfigValidationError: CMSConfig validation failed (2 issues):
124
+ - languages[0]: must be a 2-letter ISO code (e.g. 'en' or 'pl-PL')
125
+ - collections[1].slug: duplicate collection slug 'posts'
126
+ — Hint: each collection/single/form must have a unique `slug`
127
+ ```
128
+
129
+ See [`DOCS.md`](./DOCS.md) for every field type and option.
130
+
131
+ ## Adapters
132
+
133
+ `includio-cms` ships four pluggable contracts. Each has a default implementation; bring your own when you need to.
134
+
135
+ | Contract | Default | Lazy peer dep |
136
+ |--------------------|------------------------|------------------------------|
137
+ | `DatabaseAdapter` | `includio-cms/db-postgres` | `postgres`, `drizzle-orm` |
138
+ | `FilesAdapter` | `includio-cms/files-local` | — |
139
+ | `EmailAdapter` | `includio-cms/email-nodemailer` | `nodemailer` |
140
+ | `AIAdapter` | `includio-cms/ai-openai`, `includio-cms/ai-claude` | `openai`, `@anthropic-ai/sdk` |
51
141
 
52
142
  Source of truth: [`src/lib/types/adapters/`](src/lib/types/adapters/).
53
143
 
54
- ### Minimal custom DB adapter (sketch)
144
+ ### Writing your own adapter
145
+
146
+ Each contract is a TypeScript interface — implement every required method (optionals are marked `?`). TypeScript will enforce the surface.
55
147
 
56
148
  ```ts
57
149
  import type { DatabaseAdapter } from 'includio-cms/types';
@@ -68,7 +160,7 @@ export function myDbAdapter(config: { connection: string }): DatabaseAdapter {
68
160
  createEntryVersion: async (data) => { /* ... */ },
69
161
  updateEntryVersion: async (data) => { /* ... */ },
70
162
  getEntryVersions: async (data) => { /* ... */ },
71
- deleteEntryVersion: async (data) => { /* ... */ },
163
+ deleteEntryVersion: async (data) => { /* ... */ }
72
164
  // ...form submissions, media files, image/video styles, tags, consent logs...
73
165
  };
74
166
  }
@@ -90,25 +182,39 @@ Reference implementation: [`src/lib/db-postgres/index.ts`](src/lib/db-postgres/i
90
182
 
91
183
  ### Optional peer dependencies
92
184
 
93
- The default email + AI adapters depend on third-party SDKs that **are not installed by default**. If you use them, install the peer:
185
+ The default email + AI adapters depend on third-party SDKs that **are not installed by default**. Install the peer once you use the adapter:
186
+
187
+ | Adapter | Required peer | Install |
188
+ |--------------------|---------------------|-------------------------------|
189
+ | `email-nodemailer` | `nodemailer` | `pnpm add nodemailer` |
190
+ | `ai-openai` | `openai` | `pnpm add openai` |
191
+ | `ai-claude` | `@anthropic-ai/sdk` | `pnpm add @anthropic-ai/sdk` |
192
+
193
+ Each SDK is loaded lazily on first call. A missing peer throws a clear error at runtime — install it and the adapter wakes up. `pnpm dlx includio install-peers` automates this for the parts of the CMS you have configured.
194
+
195
+ ## CLI
196
+
197
+ The `includio` binary ships with three subcommands. Run any of them with `--help` for details.
94
198
 
95
- | Adapter | Required peer | Install |
96
- | ------------------------- | ------------------------ | -------------------------------------- |
97
- | `email-nodemailer` | `nodemailer` | `pnpm add nodemailer` |
98
- | `ai-openai` | `openai` | `pnpm add openai` |
99
- | `ai-claude` | `@anthropic-ai/sdk` | `pnpm add @anthropic-ai/sdk` |
199
+ | Command | What it does |
200
+ |--------------------------|-----------------------------------------------------------|
201
+ | `includio scaffold admin`| Generates `/admin` and `/api/admin` route files |
202
+ | `includio install-peers` | Installs missing optional peer SDKs based on your config |
203
+ | `includio create-user` | Creates an admin/user account interactively |
100
204
 
101
- Each SDK is loaded lazily on first call. Missing peer throws a clear error at runtime — install it and the adapter wakes up.
205
+ Global flags: `--help, -h` and `--version, -v`.
102
206
 
103
207
  ## Links
104
208
 
105
- - [Full Documentation](DOCS.md)
106
- - [Changelog](CHANGELOG.md)
107
- - [Roadmap](ROADMAP.md)
209
+ - [Full documentation](./DOCS.md)
210
+ - [Public API surface](./API.md)
211
+ - [Changelog](./CHANGELOG.md)
212
+ - [Roadmap](./ROADMAP.md)
213
+ - [Known risks](./KNOWN-RISKS.md)
108
214
 
109
- ## For AI Assistants
215
+ ## For AI assistants
110
216
 
111
- Full documentation is in [`DOCS.md`](DOCS.md) (shipped with the npm package). When working on a project using includio-cms, read `node_modules/includio-cms/DOCS.md` for the complete API reference, available components, and migration guides.
217
+ Full documentation is in [`DOCS.md`](./DOCS.md) (shipped with the npm package). When working on a project using includio-cms, read `node_modules/includio-cms/DOCS.md` for the complete API reference, available components, and migration guides.
112
218
 
113
219
  ## License
114
220
 
package/ROADMAP.md CHANGED
@@ -339,6 +339,10 @@
339
339
  > 13 faz w ~16 sesjach, droga 0.16 → 1.0.0. Lean scope: stabilizacja, security, testy, docs, audit + polish shopa. Bez nowych feature'ów.
340
340
 
341
341
  - [x] `[breaking]` `[P0]` API surface lock — exports trim 26→15, JSDoc tagi (@public/@internal/@experimental), `API.md` autogenerated (0.20.0) <!-- files: package.json, scripts/generate-api-md.ts -->
342
+ - [x] `[chore]` `[P1]` Faza 6 — Test foundation: docker postgres + integration project + 43 cases (30 REST + 6 shop checkout + 7 webhook signature/idempotency dla PayU/InPost), coverage report <!-- files: tests/integration/rest/, tests/helpers/{rest,shop}.ts, tests/integration/fixtures/shopConfig.ts, tests/setup.ts -->
343
+ - [x] `[chore]` `[P1]` Faza 7 — E2E foundation: Playwright config (storageState reuse, setup project), `db_test_e2e` na 5435, `e2e/setup/global-setup.ts` (drizzle push), `e2e/admin/auth.setup.ts` (sign-up + admin role + login), 4 specy zielone w ~23s: login, collection-crud (blog-post create→edit→archive→permanent-delete), media-upload, user-mgmt (create→delete); data-testid na critical buttons admin <!-- files: playwright.config.ts, e2e/admin/*, src/lib/admin/client/{collection,entry,users}/, src/lib/admin/components/media/file-upload.svelte -->
344
+ - [x] `[chore]` `[P1]` Faza 8 — CI na PR: `.github/workflows/ci.yml` z jobs lint/typecheck/unit/integration (postgres :5434)/e2e (postgres :5435 + playwright cache)/build, pnpm cache, trace artifacts on fail, README CI badge <!-- files: .github/workflows/ci.yml, README.md -->
345
+ - [x] `[breaking]` `[P0]` Faza 9 — DX & config validation (0.22.0): `defineConfig` Zod strict z field path + hint, `CmsError` + `ConfigValidationError` z code/context, resolver throw sites zmigrowane, CLI `--help` per subcommand + `--version`, `.env.example` rozszerzony o `INCLUDIO_*`, README rebuild (TOC + system req + 5-min quickstart), JSDoc body (opis + @param + @returns + @example) na każdym `@public` <!-- files: src/lib/core/errors.ts, src/lib/types/cms.schema.ts, src/lib/sveltekit/config.ts, src/lib/cli/index.ts, README.md, .env.example, src/lib/updates/0.22.0/ -->
342
346
  - [ ] `[fix]` `[P1]` Select field — `defaultValue` propagacja do zod schema (full repro: `ideas/post-v1/select-field-defaultvalue-bug.md`); fix planowany w Fazie 12 (RC)
343
347
 
344
348
  ## v1.x — Post-v1.0 deferred
@@ -354,10 +358,13 @@
354
358
  ## Security hardening
355
359
 
356
360
  - [ ] `[feature]` `[P1]` `sanitizeHTML` utility — general HTML sanitization outside richtext (text fields, SEO fields)
357
- - [ ] `[feature]` `[P1]` CSP headers — Content-Security-Policy middleware
358
- - [ ] `[feature]` `[P1]` CSRF protection — tokens for mutating operations
359
- - [x] `[feature]` `[P1]` Rate limiting — form submit endpoints (done in 0.13.3; admin/auth endpoints remaining)
360
- - [x] `[chore]` `[P1]` Security audit — timing attacks, MIME validation, demo endpoint, rate limiting (done in 0.13.3; `{@html}` review remaining)
361
+ - [x] `[feature]` `[P1]` CSP headers — Content-Security-Policy middleware (0.20.0/0.21.0; `unsafe-inline` zaakceptowany — KNOWN-RISKS §1)
362
+ - [x] `[feature]` `[P1]` CSRF protection — origin/referer guard na `/admin/api/*` (0.20.0/0.21.0)
363
+ - [x] `[feature]` `[P1]` Rate limiting — form submit + `/admin/api/*` (form 0.13.3, admin 0.20.0, DRY refactor 0.21.0)
364
+ - [x] `[chore]` `[P1]` Security audit — timing attacks, MIME, demo endpoint, rate limiting, `{@html}` w admin UI, ffmpeg/sharp shell args (0.13.3 + 0.21.0)
365
+ - [x] `[feature]` `[P1]` API key expiry (opt-in `expiresAt`) + `generateApiKey()` helper (0.21.0; pełna rotacja = manual config + redeploy, KNOWN-RISKS §2)
366
+ - [x] `[feature]` `[P1]` Sharp timeout 30s (`withTimeout` + `INCLUDIO_SHARP_TIMEOUT_MS`, 0.21.0)
367
+ - [x] `[chore]` `[P0]` `KNOWN-RISKS.md` — single source of truth dla zaakceptowanych ryzyk v1.0 (0.21.0)
361
368
  - [ ] `[chore]` `[P1]` Input sanitization audit — review all fields for XSS
362
369
 
363
370
  ## Backlog
@@ -1,7 +1,19 @@
1
1
  import { type RequestHandler } from '@sveltejs/kit';
2
2
  /**
3
- * REST API handler factory. Returns `{ GET, POST, PUT, DELETE }` `RequestHandler`s authenticated via `x-api-key` header. Mount in `src/routes/admin/api/rest/[...restPath]/+server.ts`.
3
+ * REST API handler factory. Returns `{ GET, POST, PUT, DELETE }`
4
+ * `RequestHandler`s authenticated via the `x-api-key` header. Mount in
5
+ * `src/routes/admin/api/rest/[...restPath]/+server.ts`.
6
+ *
7
+ * @returns An object with `GET`, `POST`, `PUT`, `DELETE` SvelteKit
8
+ * `RequestHandler`s, ready to re-export from a `+server.ts` route.
4
9
  * @public
10
+ * @example
11
+ * ```ts
12
+ * // src/routes/admin/api/rest/[...restPath]/+server.ts
13
+ * import { createRestApiHandler } from 'includio-cms/admin/remote';
14
+ *
15
+ * export const { GET, POST, PUT, DELETE } = createRestApiHandler();
16
+ * ```
5
17
  */
6
18
  export declare function createRestApiHandler(): {
7
19
  GET: RequestHandler;
@@ -50,8 +50,20 @@ function matchRoute(method, path) {
50
50
  return null;
51
51
  }
52
52
  /**
53
- * REST API handler factory. Returns `{ GET, POST, PUT, DELETE }` `RequestHandler`s authenticated via `x-api-key` header. Mount in `src/routes/admin/api/rest/[...restPath]/+server.ts`.
53
+ * REST API handler factory. Returns `{ GET, POST, PUT, DELETE }`
54
+ * `RequestHandler`s authenticated via the `x-api-key` header. Mount in
55
+ * `src/routes/admin/api/rest/[...restPath]/+server.ts`.
56
+ *
57
+ * @returns An object with `GET`, `POST`, `PUT`, `DELETE` SvelteKit
58
+ * `RequestHandler`s, ready to re-export from a `+server.ts` route.
54
59
  * @public
60
+ * @example
61
+ * ```ts
62
+ * // src/routes/admin/api/rest/[...restPath]/+server.ts
63
+ * import { createRestApiHandler } from 'includio-cms/admin/remote';
64
+ *
65
+ * export const { GET, POST, PUT, DELETE } = createRestApiHandler();
66
+ * ```
55
67
  */
56
68
  export function createRestApiHandler() {
57
69
  function handle(method) {
@@ -14,7 +14,15 @@ function safeEqual(a, b) {
14
14
  }
15
15
  export function validateApiKey(key) {
16
16
  const cms = getCMS();
17
- return cms.apiKeys.find((k) => safeEqual(k.key, key)) ?? null;
17
+ const found = cms.apiKeys.find((k) => safeEqual(k.key, key));
18
+ if (!found)
19
+ return null;
20
+ if (found.expiresAt) {
21
+ const exp = Date.parse(found.expiresAt);
22
+ if (Number.isFinite(exp) && exp < Date.now())
23
+ return null;
24
+ }
25
+ return found;
18
26
  }
19
27
  export function setSyntheticUser(event, apiKey) {
20
28
  const name = apiKey.name || 'api';
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Generate a cryptographically random API key (32 bytes, base64url-encoded).
3
+ * Use the result for `ApiKeyConfig.key`. Full rotation requires updating
4
+ * `cms.config.ts` and redeploying — see `KNOWN-RISKS.md` §2.
5
+ *
6
+ * @returns A 43-character base64url string (no padding).
7
+ * @public
8
+ * @example
9
+ * ```ts
10
+ * import { generateApiKey } from 'includio-cms/admin/remote';
11
+ *
12
+ * const key = generateApiKey();
13
+ * console.log(key); // 'A1b2C3...xYz'
14
+ * ```
15
+ */
16
+ export declare function generateApiKey(): string;
@@ -0,0 +1,19 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ /**
3
+ * Generate a cryptographically random API key (32 bytes, base64url-encoded).
4
+ * Use the result for `ApiKeyConfig.key`. Full rotation requires updating
5
+ * `cms.config.ts` and redeploying — see `KNOWN-RISKS.md` §2.
6
+ *
7
+ * @returns A 43-character base64url string (no padding).
8
+ * @public
9
+ * @example
10
+ * ```ts
11
+ * import { generateApiKey } from 'includio-cms/admin/remote';
12
+ *
13
+ * const key = generateApiKey();
14
+ * console.log(key); // 'A1b2C3...xYz'
15
+ * ```
16
+ */
17
+ export function generateApiKey() {
18
+ return randomBytes(32).toString('base64url');
19
+ }
@@ -897,7 +897,7 @@
897
897
  >
898
898
  <AlertDialog.Footer>
899
899
  <AlertDialog.Cancel>{lang[interfaceLanguage.current].cancel}</AlertDialog.Cancel>
900
- <AlertDialog.Action onclick={confirmDelete}
900
+ <AlertDialog.Action onclick={confirmDelete} data-testid="confirm-delete-entry"
901
901
  >{lang[interfaceLanguage.current].delete}</AlertDialog.Action
902
902
  >
903
903
  </AlertDialog.Footer>
@@ -20,7 +20,7 @@
20
20
  <h3 class="text-lg font-bold text-foreground mb-2">{title}</h3>
21
21
  <p class="text-sm text-muted-foreground max-w-[360px] mb-6">{description}</p>
22
22
  {#if ctaLabel && onCta}
23
- <Button variant="default" onclick={onCta}>
23
+ <Button variant="default" onclick={onCta} data-testid="create-entry-button">
24
24
  <Plus class="size-4 mr-1.5" />
25
25
  {ctaLabel}
26
26
  </Button>