@rakun-kit/core 1.5.6 → 1.5.8

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 (103) hide show
  1. package/README.md +1071 -1067
  2. package/dist/cjs/api/routes/manager/media/deleteMediaStorage.js +11 -0
  3. package/dist/cjs/api/routes/manager/media/deleteMediaStorage.js.map +1 -1
  4. package/dist/cjs/api/routes/manager/media/finalizeUpload.js +4 -1
  5. package/dist/cjs/api/routes/manager/media/finalizeUpload.js.map +1 -1
  6. package/dist/cjs/api/routes/manager/media/reimportMedia.js +7 -7
  7. package/dist/cjs/api/routes/manager/media/reimportMedia.js.map +1 -1
  8. package/dist/cjs/api/routes/manager/media/resolveMediaRecordUrls.js +2 -0
  9. package/dist/cjs/api/routes/manager/media/resolveMediaRecordUrls.js.map +1 -1
  10. package/dist/cjs/api/routes/manager/media/uploadBinary.js +1 -18
  11. package/dist/cjs/api/routes/manager/media/uploadBinary.js.map +1 -1
  12. package/dist/cjs/api/utils/dynamicData.js +20 -5
  13. package/dist/cjs/api/utils/dynamicData.js.map +1 -1
  14. package/dist/cjs/api/utils/mediaUploadKey.js +19 -0
  15. package/dist/cjs/api/utils/mediaUploadKey.js.map +1 -0
  16. package/dist/cjs/api/utils/populates/populateRelations.js +10 -8
  17. package/dist/cjs/api/utils/populates/populateRelations.js.map +1 -1
  18. package/dist/cjs/auth/passwordResetMailTemplate.js +59 -59
  19. package/dist/cjs/client.js +3 -1
  20. package/dist/cjs/client.js.map +1 -1
  21. package/dist/cjs/index.js +4 -2
  22. package/dist/cjs/index.js.map +1 -1
  23. package/dist/cjs/lib/dynamicData.js +7 -1
  24. package/dist/cjs/lib/dynamicData.js.map +1 -1
  25. package/dist/cjs/lib/fields/File.js +2 -2
  26. package/dist/cjs/lib/fields/File.js.map +1 -1
  27. package/dist/cjs/media/imageOptimization.js +27 -16
  28. package/dist/cjs/media/imageOptimization.js.map +1 -1
  29. package/dist/cjs/schemas/manager/media/finalizeUpload.js +1 -0
  30. package/dist/cjs/schemas/manager/media/finalizeUpload.js.map +1 -1
  31. package/dist/docs/index.md +7 -3
  32. package/dist/esm/api/operations/index.d.ts +1 -0
  33. package/dist/esm/api/operations/index.d.ts.map +1 -1
  34. package/dist/esm/api/operations/manager-contract.d.ts +1 -0
  35. package/dist/esm/api/operations/manager-contract.d.ts.map +1 -1
  36. package/dist/esm/api/operations/manager.d.ts +1 -0
  37. package/dist/esm/api/operations/manager.d.ts.map +1 -1
  38. package/dist/esm/api/routes/manager/media/deleteMediaStorage.d.ts.map +1 -1
  39. package/dist/esm/api/routes/manager/media/deleteMediaStorage.js +11 -0
  40. package/dist/esm/api/routes/manager/media/deleteMediaStorage.js.map +1 -1
  41. package/dist/esm/api/routes/manager/media/finalizeUpload.d.ts.map +1 -1
  42. package/dist/esm/api/routes/manager/media/finalizeUpload.js +4 -1
  43. package/dist/esm/api/routes/manager/media/finalizeUpload.js.map +1 -1
  44. package/dist/esm/api/routes/manager/media/reimportMedia.d.ts.map +1 -1
  45. package/dist/esm/api/routes/manager/media/reimportMedia.js +7 -7
  46. package/dist/esm/api/routes/manager/media/reimportMedia.js.map +1 -1
  47. package/dist/esm/api/routes/manager/media/resolveMediaRecordUrls.d.ts.map +1 -1
  48. package/dist/esm/api/routes/manager/media/resolveMediaRecordUrls.js +2 -0
  49. package/dist/esm/api/routes/manager/media/resolveMediaRecordUrls.js.map +1 -1
  50. package/dist/esm/api/routes/manager/media/uploadBinary.d.ts.map +1 -1
  51. package/dist/esm/api/routes/manager/media/uploadBinary.js +1 -18
  52. package/dist/esm/api/routes/manager/media/uploadBinary.js.map +1 -1
  53. package/dist/esm/api/utils/dynamicData.d.ts.map +1 -1
  54. package/dist/esm/api/utils/dynamicData.js +21 -6
  55. package/dist/esm/api/utils/dynamicData.js.map +1 -1
  56. package/dist/esm/api/utils/mediaUploadKey.d.ts +3 -0
  57. package/dist/esm/api/utils/mediaUploadKey.d.ts.map +1 -0
  58. package/dist/esm/api/utils/mediaUploadKey.js +12 -0
  59. package/dist/esm/api/utils/mediaUploadKey.js.map +1 -0
  60. package/dist/esm/api/utils/populates/populateLinks.d.ts +2 -2
  61. package/dist/esm/api/utils/populates/populateLinks.d.ts.map +1 -1
  62. package/dist/esm/api/utils/populates/populateRelations.d.ts +2 -2
  63. package/dist/esm/api/utils/populates/populateRelations.d.ts.map +1 -1
  64. package/dist/esm/api/utils/populates/populateRelations.js +10 -8
  65. package/dist/esm/api/utils/populates/populateRelations.js.map +1 -1
  66. package/dist/esm/auth/passwordResetMailTemplate.js +59 -59
  67. package/dist/esm/client.d.ts +2 -2
  68. package/dist/esm/client.d.ts.map +1 -1
  69. package/dist/esm/client.js +1 -1
  70. package/dist/esm/client.js.map +1 -1
  71. package/dist/esm/index.d.ts +2 -2
  72. package/dist/esm/index.d.ts.map +1 -1
  73. package/dist/esm/index.js +1 -1
  74. package/dist/esm/index.js.map +1 -1
  75. package/dist/esm/internal-content-types/Backup.d.ts.map +1 -1
  76. package/dist/esm/internal-content-types/ContentReview.d.ts.map +1 -1
  77. package/dist/esm/internal-content-types/ContentReviewPolicy.d.ts.map +1 -1
  78. package/dist/esm/internal-content-types/ContentTemplate.d.ts.map +1 -1
  79. package/dist/esm/internal-content-types/ContentVersion.d.ts.map +1 -1
  80. package/dist/esm/internal-content-types/LoginIpBlock.d.ts.map +1 -1
  81. package/dist/esm/internal-content-types/MfaChallenge.d.ts.map +1 -1
  82. package/dist/esm/internal-content-types/Migration.d.ts.map +1 -1
  83. package/dist/esm/internal-content-types/RobotsRule.d.ts.map +1 -1
  84. package/dist/esm/internal-content-types/Route.d.ts.map +1 -1
  85. package/dist/esm/internal-content-types/RouteLayoutModule.d.ts.map +1 -1
  86. package/dist/esm/internal-content-types/SchemaState.d.ts.map +1 -1
  87. package/dist/esm/internal-content-types/WebAuthnCredential.d.ts.map +1 -1
  88. package/dist/esm/lib/dynamicData.d.ts +5 -0
  89. package/dist/esm/lib/dynamicData.d.ts.map +1 -1
  90. package/dist/esm/lib/dynamicData.js +6 -0
  91. package/dist/esm/lib/dynamicData.js.map +1 -1
  92. package/dist/esm/lib/fields/File.js +2 -2
  93. package/dist/esm/lib/fields/File.js.map +1 -1
  94. package/dist/esm/media/imageOptimization.d.ts +1 -3
  95. package/dist/esm/media/imageOptimization.d.ts.map +1 -1
  96. package/dist/esm/media/imageOptimization.js +27 -16
  97. package/dist/esm/media/imageOptimization.js.map +1 -1
  98. package/dist/esm/schemas/manager/media/finalizeUpload.d.ts +1 -0
  99. package/dist/esm/schemas/manager/media/finalizeUpload.d.ts.map +1 -1
  100. package/dist/esm/schemas/manager/media/finalizeUpload.js +1 -0
  101. package/dist/esm/schemas/manager/media/finalizeUpload.js.map +1 -1
  102. package/dist/tsconfig.build.tsbuildinfo +1 -1
  103. package/package.json +1 -1
package/README.md CHANGED
@@ -1,382 +1,382 @@
1
- # @rakun-kit/core
2
-
3
- `@rakun-kit/core` contains Rakun's shared domain model and runtime. It defines content types, fields, Zod schemas, manager/web operations, request context, MongoDB connection handling, media service, permissions, translations, and typed contracts used by adapters (`@rakun-kit/next`, `@rakun-kit/express`, `@rakun-kit/trpc`) and the manager client.
4
-
5
- ## Main Entry
6
-
7
- Typical imports:
8
-
9
- ```ts
10
- import {
11
- ContentType,
12
- f,
13
- rakunBootstrap,
14
- ensureRakunInitialized,
15
- } from "@rakun-kit/core";
16
- ```
17
-
18
- The main entrypoint exports:
19
-
20
- - Bootstrap: `rakunBootstrap`, `ensureRakunBootstrap`, `ensureRakunInitialized`, `getRakunBootstrapOptions`.
21
- - Context: `createRequestContext`, `getSessionCookie`, `setSessionCookie`.
22
- - Operations: manager/web contracts and definitions.
23
- - Media: `createMediaService`, `getMediaService`, adapters, and types.
24
- - Mail: `createMailService`, `sendMail`, typed template registries, adapters, and types.
25
- - Permissions, translations, errors, contracts, and public types.
26
- - Basic internal content types: `Language`, `ManagerUser`, `Seo`.
27
-
28
- Relevant subpaths:
29
-
30
- - `@rakun-kit/core/manager`: manager operation metadata and types.
31
- - `@rakun-kit/core/contracts`: shared manager/web payload schemas.
32
- - `@rakun-kit/core/client`: client-facing types and utilities.
33
- - `@rakun-kit/core/logger`: logger.
34
- - `@rakun-kit/core/errors`: application errors.
35
- - `@rakun-kit/core/types`: types derived from content types.
36
-
37
- ## Bootstrap
38
-
39
- `rakunBootstrap(options)` registers global configuration and content types:
40
-
41
- ```ts
42
- rakunBootstrap({
43
- literals,
44
- contentTypes: [Page, Post],
45
- routes,
46
- apiOperations,
47
- mongo: {
48
- MONGO_URI: process.env.MONGO_URI!,
49
- ENVIRONMENT: process.env.NODE_ENV === "test" ? "test" : "production",
50
- },
51
- media,
52
- logger: {
53
- level: "info",
54
- prettify: true,
55
- },
56
- syncRoutes: true,
57
- });
58
- ```
59
-
60
- Options:
61
-
62
- - `literals`: text/translation catalog.
63
- - `contentTypes`: application content types.
64
- - `internalContentTypes`: optional overrides for Rakun internal content types, currently `Page`.
65
- - `routes`: configured routes used to resolve pages.
66
- - `apiOperations`: custom API operations added to the Rakun operation registry.
67
- - `mongo`: MongoDB connection. Required before serving Rakun requests.
68
- - `media`: media adapter/configuration. Optional.
69
- - `mail`: outbound mail adapter and default sender configuration. Optional.
70
- - `accountRecovery`: password-reset URL builder, expiry, and optional custom
71
- mail template. Requires `mail`; core provides the default template.
72
- - `login`: manager password-login toggle and external login adapters.
73
- - `logger`: logger configuration. If omitted, an `info` logger with `prettify` is created.
74
- - `syncRoutes`: syncs configured routes during initialization. Enabled by default.
75
-
76
- ## External manager login
77
-
78
- Configure external login methods in `rakunBootstrap`. The callback URL is the
79
- manager's public `/login/callback` route.
80
-
81
- ```ts
82
- import {
83
- createGitHubLoginAdapter,
84
- createGoogleLoginAdapter,
85
- createMicrosoftLoginAdapter,
86
- rakunBootstrap,
87
- } from "@rakun-kit/core";
88
-
89
- const callbackUrl = "https://cms.example.com/backend/login/callback";
90
-
91
- rakunBootstrap({
92
- // ...
93
- login: {
94
- password: true,
95
- adapters: [
96
- createGitHubLoginAdapter({
97
- clientId: process.env.GITHUB_CLIENT_ID!,
98
- clientSecret: process.env.GITHUB_CLIENT_SECRET!,
99
- redirectUri: callbackUrl,
100
- }),
101
- createGoogleLoginAdapter({
102
- clientId: process.env.GOOGLE_CLIENT_ID!,
103
- clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
104
- redirectUri: callbackUrl,
105
- }),
106
- createMicrosoftLoginAdapter({
107
- clientId: process.env.MICROSOFT_CLIENT_ID!,
108
- clientSecret: process.env.MICROSOFT_CLIENT_SECRET!,
109
- redirectUri: callbackUrl,
110
- tenant: "common",
111
- }),
112
- ],
113
- },
114
- });
115
- ```
116
-
117
- Register the exact callback URL in each provider console. External identities
118
- must return a verified email matching an
119
- existing manager user. Rakun does not create manager accounts automatically.
120
- The flow uses an HttpOnly state cookie and PKCE and preserves the existing MFA
121
- step. Set `password: false` only when at least one adapter is configured.
122
- Custom providers can be implemented with `defineLoginAdapter`.
123
-
124
- ### Persistent password-login IP blocking
125
-
126
- Password login enables persistent fail2ban-style IP blocking by default. Five
127
- failed attempts block the IP indefinitely; a successful login before reaching
128
- the limit clears its failed-attempt counter. Configure or disable it through
129
- `login.fail2ban`:
130
-
131
- ```ts
132
- rakunBootstrap({
133
- // ...
134
- login: {
135
- password: true,
136
- fail2ban: {
137
- maxAttempts: 5,
138
- // Optional when the deployment uses a custom trusted proxy:
139
- // resolveIp: (ctx) => ctx.req?.headers?.['your-trusted-ip-header'] as string,
140
- },
141
- },
142
- })
143
- ```
144
-
145
- Users with the `auth.ipBlocks.manage` permission can inspect and remove blocks
146
- under Manager Settings → Security. Admin roles receive this built-in permission
147
- automatically. A block can also be removed directly from MongoDB by deleting
148
- the matching document from the `LoginIpBlock` collection. Set `fail2ban: false`
149
- to disable this behavior. External login adapters are not affected.
150
-
151
- ## Account recovery and MFA recovery
152
-
153
- Password recovery is enabled when both `mail` and `accountRecovery` are
154
- configured. The reset URL must point to the manager's public
155
- `/reset-password` screen and preserve the token as the `token` query parameter:
156
-
157
- ```ts
158
- import { rakunBootstrap } from "@rakun-kit/core";
159
- import { createResendMailServiceConfig } from "@rakun-kit/resend";
160
-
161
- rakunBootstrap({
162
- // ...
163
- mail: createResendMailServiceConfig({
164
- apiKey: process.env.RESEND_API_KEY!,
165
- defaultFrom: process.env.RAKUN_MAIL_FROM!,
166
- }),
167
- accountRecovery: {
168
- passwordReset: {
169
- expiresInMs: 60 * 60 * 1000,
170
- createUrl: (token) =>
171
- `https://cms.example.com/backend/reset-password?token=${encodeURIComponent(token)}`,
172
- },
173
- },
174
- });
175
- ```
176
-
177
- Core uses its branded HTML and plain-text password-reset template by default.
178
- Pass `accountRecovery.passwordReset.template` only when the application needs
179
- to replace it with custom branding or copy.
180
-
181
- Reset tokens are random, stored only as SHA-256 hashes, expire, and are consumed
182
- once. A successful reset closes every existing session but never disables MFA.
183
- The request endpoint always returns the same result for known and unknown email
184
- addresses.
185
-
186
- Enabling TOTP or the first WebAuthn device returns one-time recovery codes to
187
- the manager UI. Only their hashes are persisted, each code is consumed after
188
- one login, and users can replace the entire set from their account after
189
- confirming their current password. MFA state cannot be disabled through generic
190
- manager content operations. If a user loses all MFA methods and recovery codes,
191
- recovery must be handled by an authorized person through a channel outside the
192
- CMS.
193
-
194
- Content types can define lifecycle `hooks` and opt into manager-selected
195
- `dynamicData` sources.
196
-
197
- Routes can define fixed layout module slots. Rakun syncs those slots and the
198
- manager lets users select an existing entry for each route:
199
-
200
- ```ts
201
- rakunBootstrap({
202
- // ...
203
- routes: [
204
- {
205
- key: "pages",
206
- contentType: "Page",
207
- field: "slug",
208
- hasPage: true,
209
- dynamic: false,
210
- defaultBasePath: "",
211
- layout: [
212
- { key: "header", contentType: "Header" },
213
- { type: "content" },
214
- { key: "footer", contentType: "Footer" },
215
- ],
216
- },
217
- ],
218
- });
219
- ```
220
-
221
- The web page response includes an ordered `layout` array containing module slots
222
- plus the content slot.
223
-
224
- ## Module picker
225
-
226
- Content types used as iterator modules can customize their card in the manager's
227
- add-module dialog. `modulePicker.preview` accepts an image URL available to the
228
- manager browser; relative URLs are also supported:
229
-
230
- ```ts
231
- const Hero = new ContentType({
232
- name: "Hero",
233
- modulePicker: {
234
- title: "Hero section",
235
- description: "Large introduction with heading, copy, and CTA.",
236
- category: "Marketing",
237
- icon: "PanelTop",
238
- preview: "/images/modules/hero.webp",
239
- keywords: ["banner", "cover"],
240
- },
241
- fields: {
242
- title: f.string().required(),
243
- },
244
- });
245
- ```
246
-
247
- When `preview` is omitted or cannot be loaded, the picker displays a neutral
248
- placeholder so module cards keep a consistent height.
249
-
250
- `ensureRakunInitialized()` prepares logger, MongoDB, media, and route syncing. It uses a singleton promise to avoid concurrent initialization; if initialization fails, the promise is cleared so the next call can retry.
251
-
252
- `ensureRakunBootstrap(options)` only calls `rakunBootstrap` if the runtime has not been bootstrapped yet.
253
-
254
- English is built into `@rakun-kit/manager-react`. Install only the extra
255
- manager UI locales an application needs:
256
-
257
- ```sh
258
- bun add @rakun-kit/manager-locales
259
- # or: npm install @rakun-kit/manager-locales
260
- ```
261
-
262
- Import the required language subpath and register it with `managerLanguages`;
263
- the public `manager.uiLocales` operation returns configured packs to the
264
- manager client:
265
-
266
- ```ts
267
- import { esManagerLocalePack } from "@rakun-kit/manager-locales/es";
268
-
269
- rakunBootstrap({
270
- // ...
271
- managerLanguages: [esManagerLocalePack],
272
- });
273
- ```
274
-
275
- `managerLanguages` may also extend locales with arbitrary project keys. This is
276
- useful for translatable content-type titles and categories without adding host
277
- keys to the manager's static `ManagerMessageKey` union:
278
-
279
- ```ts
280
- import { extendManagerLanguagePack } from "@rakun-kit/core/contracts";
281
- import { esManagerLocalePack } from "@rakun-kit/manager-locales/es";
282
-
283
- rakunBootstrap({
284
- // ...
285
- managerLanguages: [
286
- {
287
- code: "en",
288
- name: "English",
289
- messages: {
290
- "field.title": "Title",
291
- "layoutModule.header": "Header",
292
- "project.contentTypes.article.menu": "Articles",
293
- },
294
- },
295
- extendManagerLanguagePack(esManagerLocalePack, {
296
- "field.title": "Título",
297
- "layoutModule.header": "Cabecera",
298
- "project.contentTypes.article.menu": "Artículos",
299
- }),
300
- ],
301
- });
302
- ```
303
-
304
- Content-type field labels automatically use `field.<fieldName>` from these
305
- project messages. Missing translations fall back to a built-in manager label
306
- when available and otherwise to the humanized field name.
307
-
308
- Route layout module labels use `layoutModule.<layoutKey>`, so a slot configured
309
- with `key: 'header'` resolves `layoutModule.header`.
310
-
311
- ## Plugins
312
-
313
- Trusted server plugins contribute to the same bootstrap registry without coupling
314
- core to React:
315
-
316
- ```ts
317
- import { defineRakunPlugin, rakunBootstrap } from "@rakun-kit/core";
318
-
319
- export const analyticsPlugin = defineRakunPlugin({
320
- id: "@acme/rakun-analytics",
321
- contentTypes: [AnalyticsEvent],
322
- routes: analyticsRoutes,
323
- apiOperations: analyticsOperations,
324
- permissions: ["plugin.analytics.view"],
325
- literals: {},
326
- initialize: async ({ db }) => {
327
- // Services and migrations are ready here. Keep initialization idempotent.
328
- },
329
- });
330
-
331
- rakunBootstrap({
332
- plugins: [analyticsPlugin],
333
- contentTypes: [],
334
- literals: {},
335
- mongo,
336
- });
337
- ```
338
-
339
- Plugin ids and contributed content types, routes, operations, literals, and
340
- custom field editor ids must be unique. Rakun reports both owners on conflicts.
341
- Visual manager and web facets are registered separately in their browser
342
- runtimes.
343
-
344
- Custom field factories can use `createPluginField`. Their serializable
345
- `meta.editor` must match a field declaration in the server plugin and a React
346
- editor registered by its manager facet.
347
-
348
- ## Content Types
349
-
350
- A `ContentType` defines a logical collection:
351
-
352
- ```ts
353
- const Post = new ContentType({
354
- name: "Post",
355
- menu: {
356
- title: "Posts",
357
- icon: "newspaper",
358
- category: "Content",
359
- },
360
- fields: {
361
- title: f.string().required(),
362
- slug: f.string().type("Slug").required(),
363
- body: f.string().type("RichText"),
364
- published: f.boolean(),
365
- },
366
- uniques: [["slug"]],
367
- listFields: ["title", "slug", "published"],
368
- });
369
- ```
370
-
371
- Page-like content types can define ordered page modules with `iterator` outside
372
- `fields`. Rakun persists this generated field as `_iterator`:
373
-
374
- ```ts
375
- const Page = new ContentType({
376
- name: "Page",
377
- fields: {
378
- title: f.string().required(),
379
- slug: f.string().type("Slug").required(),
1
+ # @rakun-kit/core
2
+
3
+ `@rakun-kit/core` contains Rakun's shared domain model and runtime. It defines content types, fields, Zod schemas, manager/web operations, request context, MongoDB connection handling, media service, permissions, translations, and typed contracts used by adapters (`@rakun-kit/next`, `@rakun-kit/express`, `@rakun-kit/trpc`) and the manager client.
4
+
5
+ ## Main Entry
6
+
7
+ Typical imports:
8
+
9
+ ```ts
10
+ import {
11
+ ContentType,
12
+ f,
13
+ rakunBootstrap,
14
+ ensureRakunInitialized,
15
+ } from "@rakun-kit/core";
16
+ ```
17
+
18
+ The main entrypoint exports:
19
+
20
+ - Bootstrap: `rakunBootstrap`, `ensureRakunBootstrap`, `ensureRakunInitialized`, `getRakunBootstrapOptions`.
21
+ - Context: `createRequestContext`, `getSessionCookie`, `setSessionCookie`.
22
+ - Operations: manager/web contracts and definitions.
23
+ - Media: `createMediaService`, `getMediaService`, adapters, and types.
24
+ - Mail: `createMailService`, `sendMail`, typed template registries, adapters, and types.
25
+ - Permissions, translations, errors, contracts, and public types.
26
+ - Basic internal content types: `Language`, `ManagerUser`, `Seo`.
27
+
28
+ Relevant subpaths:
29
+
30
+ - `@rakun-kit/core/manager`: manager operation metadata and types.
31
+ - `@rakun-kit/core/contracts`: shared manager/web payload schemas.
32
+ - `@rakun-kit/core/client`: client-facing types and utilities.
33
+ - `@rakun-kit/core/logger`: logger.
34
+ - `@rakun-kit/core/errors`: application errors.
35
+ - `@rakun-kit/core/types`: types derived from content types.
36
+
37
+ ## Bootstrap
38
+
39
+ `rakunBootstrap(options)` registers global configuration and content types:
40
+
41
+ ```ts
42
+ rakunBootstrap({
43
+ literals,
44
+ contentTypes: [Page, Post],
45
+ routes,
46
+ apiOperations,
47
+ mongo: {
48
+ MONGO_URI: process.env.MONGO_URI!,
49
+ ENVIRONMENT: process.env.NODE_ENV === "test" ? "test" : "production",
50
+ },
51
+ media,
52
+ logger: {
53
+ level: "info",
54
+ prettify: true,
55
+ },
56
+ syncRoutes: true,
57
+ });
58
+ ```
59
+
60
+ Options:
61
+
62
+ - `literals`: text/translation catalog.
63
+ - `contentTypes`: application content types.
64
+ - `internalContentTypes`: optional overrides for Rakun internal content types, currently `Page`.
65
+ - `routes`: configured routes used to resolve pages.
66
+ - `apiOperations`: custom API operations added to the Rakun operation registry.
67
+ - `mongo`: MongoDB connection. Required before serving Rakun requests.
68
+ - `media`: media adapter/configuration. Optional.
69
+ - `mail`: outbound mail adapter and default sender configuration. Optional.
70
+ - `accountRecovery`: password-reset URL builder, expiry, and optional custom
71
+ mail template. Requires `mail`; core provides the default template.
72
+ - `login`: manager password-login toggle and external login adapters.
73
+ - `logger`: logger configuration. If omitted, an `info` logger with `prettify` is created.
74
+ - `syncRoutes`: syncs configured routes during initialization. Enabled by default.
75
+
76
+ ## External manager login
77
+
78
+ Configure external login methods in `rakunBootstrap`. The callback URL is the
79
+ manager's public `/login/callback` route.
80
+
81
+ ```ts
82
+ import {
83
+ createGitHubLoginAdapter,
84
+ createGoogleLoginAdapter,
85
+ createMicrosoftLoginAdapter,
86
+ rakunBootstrap,
87
+ } from "@rakun-kit/core";
88
+
89
+ const callbackUrl = "https://cms.example.com/backend/login/callback";
90
+
91
+ rakunBootstrap({
92
+ // ...
93
+ login: {
94
+ password: true,
95
+ adapters: [
96
+ createGitHubLoginAdapter({
97
+ clientId: process.env.GITHUB_CLIENT_ID!,
98
+ clientSecret: process.env.GITHUB_CLIENT_SECRET!,
99
+ redirectUri: callbackUrl,
100
+ }),
101
+ createGoogleLoginAdapter({
102
+ clientId: process.env.GOOGLE_CLIENT_ID!,
103
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
104
+ redirectUri: callbackUrl,
105
+ }),
106
+ createMicrosoftLoginAdapter({
107
+ clientId: process.env.MICROSOFT_CLIENT_ID!,
108
+ clientSecret: process.env.MICROSOFT_CLIENT_SECRET!,
109
+ redirectUri: callbackUrl,
110
+ tenant: "common",
111
+ }),
112
+ ],
113
+ },
114
+ });
115
+ ```
116
+
117
+ Register the exact callback URL in each provider console. External identities
118
+ must return a verified email matching an
119
+ existing manager user. Rakun does not create manager accounts automatically.
120
+ The flow uses an HttpOnly state cookie and PKCE and preserves the existing MFA
121
+ step. Set `password: false` only when at least one adapter is configured.
122
+ Custom providers can be implemented with `defineLoginAdapter`.
123
+
124
+ ### Persistent password-login IP blocking
125
+
126
+ Password login enables persistent fail2ban-style IP blocking by default. Five
127
+ failed attempts block the IP indefinitely; a successful login before reaching
128
+ the limit clears its failed-attempt counter. Configure or disable it through
129
+ `login.fail2ban`:
130
+
131
+ ```ts
132
+ rakunBootstrap({
133
+ // ...
134
+ login: {
135
+ password: true,
136
+ fail2ban: {
137
+ maxAttempts: 5,
138
+ // Optional when the deployment uses a custom trusted proxy:
139
+ // resolveIp: (ctx) => ctx.req?.headers?.['your-trusted-ip-header'] as string,
140
+ },
141
+ },
142
+ })
143
+ ```
144
+
145
+ Users with the `auth.ipBlocks.manage` permission can inspect and remove blocks
146
+ under Manager Settings → Security. Admin roles receive this built-in permission
147
+ automatically. A block can also be removed directly from MongoDB by deleting
148
+ the matching document from the `LoginIpBlock` collection. Set `fail2ban: false`
149
+ to disable this behavior. External login adapters are not affected.
150
+
151
+ ## Account recovery and MFA recovery
152
+
153
+ Password recovery is enabled when both `mail` and `accountRecovery` are
154
+ configured. The reset URL must point to the manager's public
155
+ `/reset-password` screen and preserve the token as the `token` query parameter:
156
+
157
+ ```ts
158
+ import { rakunBootstrap } from "@rakun-kit/core";
159
+ import { createResendMailServiceConfig } from "@rakun-kit/resend";
160
+
161
+ rakunBootstrap({
162
+ // ...
163
+ mail: createResendMailServiceConfig({
164
+ apiKey: process.env.RESEND_API_KEY!,
165
+ defaultFrom: process.env.RAKUN_MAIL_FROM!,
166
+ }),
167
+ accountRecovery: {
168
+ passwordReset: {
169
+ expiresInMs: 60 * 60 * 1000,
170
+ createUrl: (token) =>
171
+ `https://cms.example.com/backend/reset-password?token=${encodeURIComponent(token)}`,
172
+ },
173
+ },
174
+ });
175
+ ```
176
+
177
+ Core uses its branded HTML and plain-text password-reset template by default.
178
+ Pass `accountRecovery.passwordReset.template` only when the application needs
179
+ to replace it with custom branding or copy.
180
+
181
+ Reset tokens are random, stored only as SHA-256 hashes, expire, and are consumed
182
+ once. A successful reset closes every existing session but never disables MFA.
183
+ The request endpoint always returns the same result for known and unknown email
184
+ addresses.
185
+
186
+ Enabling TOTP or the first WebAuthn device returns one-time recovery codes to
187
+ the manager UI. Only their hashes are persisted, each code is consumed after
188
+ one login, and users can replace the entire set from their account after
189
+ confirming their current password. MFA state cannot be disabled through generic
190
+ manager content operations. If a user loses all MFA methods and recovery codes,
191
+ recovery must be handled by an authorized person through a channel outside the
192
+ CMS.
193
+
194
+ Content types can define lifecycle `hooks` and opt into manager-selected
195
+ `dynamicData` sources.
196
+
197
+ Routes can define fixed layout module slots. Rakun syncs those slots and the
198
+ manager lets users select an existing entry for each route:
199
+
200
+ ```ts
201
+ rakunBootstrap({
202
+ // ...
203
+ routes: [
204
+ {
205
+ key: "pages",
206
+ contentType: "Page",
207
+ field: "slug",
208
+ hasPage: true,
209
+ dynamic: false,
210
+ defaultBasePath: "",
211
+ layout: [
212
+ { key: "header", contentType: "Header" },
213
+ { type: "content" },
214
+ { key: "footer", contentType: "Footer" },
215
+ ],
216
+ },
217
+ ],
218
+ });
219
+ ```
220
+
221
+ The web page response includes an ordered `layout` array containing module slots
222
+ plus the content slot.
223
+
224
+ ## Module picker
225
+
226
+ Content types used as iterator modules can customize their card in the manager's
227
+ add-module dialog. `modulePicker.preview` accepts an image URL available to the
228
+ manager browser; relative URLs are also supported:
229
+
230
+ ```ts
231
+ const Hero = new ContentType({
232
+ name: "Hero",
233
+ modulePicker: {
234
+ title: "Hero section",
235
+ description: "Large introduction with heading, copy, and CTA.",
236
+ category: "Marketing",
237
+ icon: "PanelTop",
238
+ preview: "/images/modules/hero.webp",
239
+ keywords: ["banner", "cover"],
240
+ },
241
+ fields: {
242
+ title: f.string().required(),
243
+ },
244
+ });
245
+ ```
246
+
247
+ When `preview` is omitted or cannot be loaded, the picker displays a neutral
248
+ placeholder so module cards keep a consistent height.
249
+
250
+ `ensureRakunInitialized()` prepares logger, MongoDB, media, and route syncing. It uses a singleton promise to avoid concurrent initialization; if initialization fails, the promise is cleared so the next call can retry.
251
+
252
+ `ensureRakunBootstrap(options)` only calls `rakunBootstrap` if the runtime has not been bootstrapped yet.
253
+
254
+ English is built into `@rakun-kit/manager-react`. Install only the extra
255
+ manager UI locales an application needs:
256
+
257
+ ```sh
258
+ bun add @rakun-kit/manager-locales
259
+ # or: npm install @rakun-kit/manager-locales
260
+ ```
261
+
262
+ Import the required language subpath and register it with `managerLanguages`;
263
+ the public `manager.uiLocales` operation returns configured packs to the
264
+ manager client:
265
+
266
+ ```ts
267
+ import { esManagerLocalePack } from "@rakun-kit/manager-locales/es";
268
+
269
+ rakunBootstrap({
270
+ // ...
271
+ managerLanguages: [esManagerLocalePack],
272
+ });
273
+ ```
274
+
275
+ `managerLanguages` may also extend locales with arbitrary project keys. This is
276
+ useful for translatable content-type titles and categories without adding host
277
+ keys to the manager's static `ManagerMessageKey` union:
278
+
279
+ ```ts
280
+ import { extendManagerLanguagePack } from "@rakun-kit/core/contracts";
281
+ import { esManagerLocalePack } from "@rakun-kit/manager-locales/es";
282
+
283
+ rakunBootstrap({
284
+ // ...
285
+ managerLanguages: [
286
+ {
287
+ code: "en",
288
+ name: "English",
289
+ messages: {
290
+ "field.title": "Title",
291
+ "layoutModule.header": "Header",
292
+ "project.contentTypes.article.menu": "Articles",
293
+ },
294
+ },
295
+ extendManagerLanguagePack(esManagerLocalePack, {
296
+ "field.title": "Título",
297
+ "layoutModule.header": "Cabecera",
298
+ "project.contentTypes.article.menu": "Artículos",
299
+ }),
300
+ ],
301
+ });
302
+ ```
303
+
304
+ Content-type field labels automatically use `field.<fieldName>` from these
305
+ project messages. Missing translations fall back to a built-in manager label
306
+ when available and otherwise to the humanized field name.
307
+
308
+ Route layout module labels use `layoutModule.<layoutKey>`, so a slot configured
309
+ with `key: 'header'` resolves `layoutModule.header`.
310
+
311
+ ## Plugins
312
+
313
+ Trusted server plugins contribute to the same bootstrap registry without coupling
314
+ core to React:
315
+
316
+ ```ts
317
+ import { defineRakunPlugin, rakunBootstrap } from "@rakun-kit/core";
318
+
319
+ export const analyticsPlugin = defineRakunPlugin({
320
+ id: "@acme/rakun-analytics",
321
+ contentTypes: [AnalyticsEvent],
322
+ routes: analyticsRoutes,
323
+ apiOperations: analyticsOperations,
324
+ permissions: ["plugin.analytics.view"],
325
+ literals: {},
326
+ initialize: async ({ db }) => {
327
+ // Services and migrations are ready here. Keep initialization idempotent.
328
+ },
329
+ });
330
+
331
+ rakunBootstrap({
332
+ plugins: [analyticsPlugin],
333
+ contentTypes: [],
334
+ literals: {},
335
+ mongo,
336
+ });
337
+ ```
338
+
339
+ Plugin ids and contributed content types, routes, operations, literals, and
340
+ custom field editor ids must be unique. Rakun reports both owners on conflicts.
341
+ Visual manager and web facets are registered separately in their browser
342
+ runtimes.
343
+
344
+ Custom field factories can use `createPluginField`. Their serializable
345
+ `meta.editor` must match a field declaration in the server plugin and a React
346
+ editor registered by its manager facet.
347
+
348
+ ## Content Types
349
+
350
+ A `ContentType` defines a logical collection:
351
+
352
+ ```ts
353
+ const Post = new ContentType({
354
+ name: "Post",
355
+ menu: {
356
+ title: "Posts",
357
+ icon: "newspaper",
358
+ category: "Content",
359
+ },
360
+ fields: {
361
+ title: f.string().required(),
362
+ slug: f.string().type("Slug").required(),
363
+ body: f.string().type("RichText"),
364
+ published: f.boolean(),
365
+ },
366
+ uniques: [["slug"]],
367
+ listFields: ["title", "slug", "published"],
368
+ });
369
+ ```
370
+
371
+ Page-like content types can define ordered page modules with `iterator` outside
372
+ `fields`. Rakun persists this generated field as `_iterator`:
373
+
374
+ ```ts
375
+ const Page = new ContentType({
376
+ name: "Page",
377
+ fields: {
378
+ title: f.string().required(),
379
+ slug: f.string().type("Slug").required(),
380
380
  },
381
381
  iterator: [
382
382
  { contentType: PageSection, type: "new" },
@@ -397,156 +397,160 @@ document's iterator modules. This supports compositions such as a shared hero,
397
397
  a `LayoutWithInfo` wrapper containing the unique content, and a shared
398
398
  newsletter. Route layout modules such as header and footer still wrap the
399
399
  assembled result.
400
-
400
+
401
401
  Iterator and template modules can also be made conditional from the manager. A
402
402
  condition is stored on the module entry and evaluated against the current
403
403
  document:
404
-
405
- ```ts
406
- {
407
- name: "Credits",
408
- value: { /* module relation */ },
409
- visibleWhen: {
410
- field: "credits",
411
- operator: "notEmpty",
412
- },
413
- }
414
- ```
415
-
404
+
405
+ ```ts
406
+ {
407
+ name: "Credits",
408
+ value: { /* module relation */ },
409
+ visibleWhen: {
410
+ field: "credits",
411
+ operator: "notEmpty",
412
+ },
413
+ }
414
+ ```
415
+
416
416
  Supported operators are `notEmpty` and `empty`. Conditional modules remain in
417
417
  their stored Content or Template composition but are omitted from web and
418
418
  preview output when their condition does not match.
419
-
420
- Main properties:
421
-
422
- - `name`: stable type name. Also used as `_type`.
419
+
420
+ Main properties:
421
+
422
+ - `name`: stable type name. Also used as `_type`.
423
423
  - `fields`: field map.
424
424
  - `iterator`: page module entries. Generates the reserved `_iterator` field.
425
425
  - `menu`: manager metadata.
426
- - `uniques`: unique field groups.
427
- - `listFields`: preferred fields in lists and relations.
428
- - `hideFromManager()`: hides the content type from manager content type lists.
429
- - `apiOnly()`: applies `.apiOnly()` to every field in the content type.
430
- - `managerOnly()`: applies `.managerOnly()` to every field in the content type.
431
- - Saved manager documents support comments and user mentions by default.
432
- - `withHooks()`: attaches lifecycle hooks such as `beforeInsert`, `beforeUpdate`, and `onGet`.
433
- - Dynamic data bindings are available on manager-visible fields by default; use
434
- field-level `.noDynamic()` to opt out.
435
-
426
+ - `uniques`: unique field groups.
427
+ - `listFields`: preferred fields in lists and relations.
428
+ - `hideFromManager()`: hides the content type from manager content type lists.
429
+ - `apiOnly()`: applies `.apiOnly()` to every field in the content type.
430
+ - `managerOnly()`: applies `.managerOnly()` to every field in the content type.
431
+ - Saved manager documents support comments and user mentions by default.
432
+ - `withHooks()`: attaches lifecycle hooks such as `beforeInsert`, `beforeUpdate`, and `onGet`.
433
+ - Dynamic data bindings are available on manager-visible fields by default; use
434
+ field-level `.noDynamic()` to opt out.
435
+
436
436
  When a content type has a configured route with `hasPage: true`, Rakun adds an
437
437
  optional reserved `_seo` relation automatically. If that content type has an
438
438
  `iterator`, Rakun also enables its shared Template editor automatically.
439
-
440
- Hooks run around DB mutations and public output resolution:
441
-
442
- ```ts
443
- const User = new ContentType({
444
- name: "User",
445
- fields: {
446
- email: f.string().type("Email").required(),
447
- password: f.string().type("Password").required().managerOnly(),
448
- },
449
- }).withHooks({
450
- beforeInsert: ({ data }) => ({
451
- ...data,
452
- password: hashPassword(String(data.password)),
453
- }),
454
- onGet: ({ data }) => ({
455
- ...data,
456
- displayName: String(data.email).split("@")[0],
457
- }),
458
- });
459
- ```
460
-
461
- Dynamic data turns a content type into a reusable layout. The manager can bind
462
- manager-visible fields to another content item field, or to a generated `href`
463
- when a source content type has a page route. Fields are dynamic by default; call
464
- `.noDynamic()` on fields that should not accept bindings or be exported as
465
- source paths. Source content types are hidden by default; set
466
- `dynamicDataSource: true` on content types that should appear in the manager
467
- source selector. Field bindings can also select `Current document` to read values
468
- from the document being edited without marking its content type as a source.
469
-
470
- Source field selectors are type-aware. A string target only offers string-like
471
- source paths, number targets only offer numbers, and boolean targets only offer
472
- booleans. Object-like source fields are traversed so nested leaf fields can be
473
- selected, while reserved SEO metadata is omitted from dynamic data mappings.
474
- The generated `href` source is only shown for content types that have a
475
- configured route with `hasPage: true`.
476
-
477
- ```ts
478
- const Project = new ContentType({
479
- name: "Project",
480
- dynamicDataSource: true,
481
- fields: {
482
- title: f.string().required(),
483
- slug: f.string().type("Slug").required(),
484
- },
485
- });
486
-
487
- const Carousel = new ContentType({
488
- name: "Carousel",
489
- fields: {
490
- title: f.string().required(),
491
- internalNote: f.string().noDynamic(),
492
- items: f.blocks([
493
- {
494
- name: "CarouselItem",
495
- field: f.relation(CarouselItem, "new"),
496
- },
497
- ]),
498
- },
499
- });
500
- ```
501
-
502
- List bindings append dynamic items to manually stored items instead of replacing
503
- the list. If the same stable item id appears in both sources, the dynamically
504
- resolved item wins and the duplicate manual copy is skipped.
505
-
506
- A list inside a nested module can also use a compatible relation or blocks array
507
- from the current document. The manager exposes these sources as
508
- `Current document · <field>`, infers the content type of each array item, and
509
- lets each item be mapped with the same field mapping UI used for collections.
510
- These bindings store a `currentDocument` source alongside the inferred item
511
- content type:
512
-
513
- ```ts
514
- const headerBindings = {
515
- lists: {
516
- categories: {
517
- contentType: LinkItem.name,
518
- source: {
519
- kind: "currentDocument",
520
- contentType: Project.name,
521
- path: "categories",
522
- itemName: "Category",
523
- },
524
- itemName: "Category",
525
- map: {
526
- title: { contentType: LinkItem.name, path: "title" },
527
- href: { contentType: LinkItem.name, path: "href" },
528
- },
529
- },
530
- },
531
- };
532
- ```
533
-
534
- List query conditions can compare a source field with a value from the current
535
- document. The manager exposes this as `Current document`; programmatic bindings
536
- use `{ $current: "path.to.field" }`. For example, a category can query only the
537
- projects whose related category has the same slug:
538
-
539
- ```ts
540
- query: {
541
- filter: {
542
- "category.slug": { $current: "slug" },
543
- },
544
- options: { limit: 10 },
545
- }
546
- ```
547
-
548
- Current-document paths are checked against the content type's dynamic field
549
- rules before the query runs. `_id` is also available for relation queries.
439
+
440
+ Hooks run around DB mutations and public output resolution:
441
+
442
+ ```ts
443
+ const User = new ContentType({
444
+ name: "User",
445
+ fields: {
446
+ email: f.string().type("Email").required(),
447
+ password: f.string().type("Password").required().managerOnly(),
448
+ },
449
+ }).withHooks({
450
+ beforeInsert: ({ data }) => ({
451
+ ...data,
452
+ password: hashPassword(String(data.password)),
453
+ }),
454
+ onGet: ({ data }) => ({
455
+ ...data,
456
+ displayName: String(data.email).split("@")[0],
457
+ }),
458
+ });
459
+ ```
460
+
461
+ Dynamic data turns a content type into a reusable layout. The manager can bind
462
+ manager-visible fields to another content item field, or to a generated `href`
463
+ when a source content type has a page route. Fields are dynamic by default; call
464
+ `.noDynamic()` on fields that should not accept bindings or be exported as
465
+ source paths. Source content types are hidden by default; set
466
+ `dynamicDataSource: true` on content types that should appear in the manager
467
+ source selector. Field bindings can also select `Current document` to read values
468
+ from the document being edited without marking its content type as a source.
469
+
470
+ Source field selectors are type-aware. A string target only offers string-like
471
+ source paths, number targets only offer numbers, and boolean targets only offer
472
+ booleans. Object-like source fields are traversed so nested leaf fields can be
473
+ selected, while reserved SEO metadata is omitted from dynamic data mappings.
474
+ The generated `href` source is only shown for content types that have a
475
+ configured route with `hasPage: true`.
476
+
477
+ ```ts
478
+ const Project = new ContentType({
479
+ name: "Project",
480
+ dynamicDataSource: true,
481
+ fields: {
482
+ title: f.string().required(),
483
+ slug: f.string().type("Slug").required(),
484
+ },
485
+ });
486
+
487
+ const Carousel = new ContentType({
488
+ name: "Carousel",
489
+ fields: {
490
+ title: f.string().required(),
491
+ internalNote: f.string().noDynamic(),
492
+ items: f.blocks([
493
+ {
494
+ name: "CarouselItem",
495
+ field: f.relation(CarouselItem, "new"),
496
+ },
497
+ ]),
498
+ },
499
+ });
500
+ ```
501
+
502
+ List bindings append dynamic items to manually stored items instead of replacing
503
+ the list. If the same stable item id appears in both sources, the dynamically
504
+ resolved item wins and the duplicate manual copy is skipped.
505
+
506
+ A list inside a nested module can also use a compatible relation or blocks array
507
+ from the current document. The manager exposes these sources as
508
+ `Current document · <field>`, infers the content type of each array item, and
509
+ lets each item be mapped with the same field mapping UI used for collections.
510
+ These bindings store a `currentDocument` source alongside the inferred item
511
+ content type:
512
+
513
+ ```ts
514
+ const headerBindings = {
515
+ lists: {
516
+ categories: {
517
+ contentType: LinkItem.name,
518
+ source: {
519
+ kind: "currentDocument",
520
+ contentType: Project.name,
521
+ path: "categories",
522
+ itemName: "Category",
523
+ },
524
+ itemName: "Category",
525
+ map: {
526
+ title: { contentType: LinkItem.name, path: "title" },
527
+ href: { contentType: LinkItem.name, path: "href" },
528
+ },
529
+ },
530
+ },
531
+ };
532
+ ```
533
+
534
+ List query conditions can compare a source field with a value from the current
535
+ context. At the top level this is the current document; inside a nested list it
536
+ is the parent source item and the manager labels it `Current item`. Programmatic
537
+ bindings use `{ $current: "path.to.field" }`. For example, a category can query
538
+ only the projects whose related category has the same slug:
539
+
540
+ ```ts
541
+ query: {
542
+ filter: {
543
+ "category.slug": { $current: "slug" },
544
+ },
545
+ options: { limit: 10 },
546
+ }
547
+ ```
548
+
549
+ Nested conditions can still read the root document by selecting `Current
550
+ document`, which stores `{ $document: "path.to.field" }`. Current-item and
551
+ current-document paths are checked against their respective content type's
552
+ dynamic field rules before the query runs. `_id` is also available for relation
553
+ queries.
550
554
 
551
555
  List mappings can be nested recursively when a mapped target field is itself a
552
556
  `blocks` list. Use `kind: "list"` for that map entry and configure its source,
@@ -579,108 +583,108 @@ apply the same source opt-in, field compatibility, query validation, and
579
583
  `.noDynamic()` rules at every level.
580
584
 
581
585
  A list mapping can also collect an array through a reverse relation without
582
- persisting that relation on the source document. For example, a category gallery
583
- can create one item per `Category` and collect the images of its related
584
- `Project` documents:
585
-
586
- ```ts
587
- const Category = new ContentType({
588
- name: "Category",
589
- dynamicDataSource: true,
590
- fields: {
591
- title: f.string().required(),
592
- },
593
- });
594
-
595
- const Project = new ContentType({
596
- name: "Project",
597
- dynamicDataSource: true,
598
- fields: {
599
- category: f.relation(Category, "existing").required(),
600
- images: f.file().type("Image").multiple().required(),
601
- },
602
- });
603
-
604
- const galleryBindings = {
605
- lists: {
606
- items: {
607
- contentType: Category.name,
608
- itemName: "CategoriesGalleryItem",
609
- map: {
610
- title: { contentType: Category.name, path: "title" },
611
- images: {
612
- kind: "relatedCollection",
613
- contentType: Project.name,
614
- relation: "category",
615
- path: "images",
616
- limit: 10,
617
- },
618
- },
619
- },
620
- },
621
- };
622
- ```
623
-
624
- The related collection query matches `Project.category._id` against the current
625
- category, preserves project and image order, and flattens the selected array by
626
- one level. Its numeric limit applies to related projects and is capped at 100.
627
-
628
- Schema and validation methods:
629
-
630
- - `getInputSchema()`: write schema. Includes `_type`, `createdBy`, and `updatedBy`.
631
- - `getSchema()`: persisted schema.
632
- - `getPopulatedSchema()`: output with populated relations and `_id`.
633
- - `getOutputSchema()`: public API output.
634
- - `getOutputSchemaWithoutIterators()`: output variant without iterator fields.
635
- - `validate`, `partialValidate`, `validateOutput`: Zod helpers.
636
-
637
- The registry lives in `lib/Registry`:
638
-
639
- - `registerContentType`, `registerInternalContentType`.
640
- - `getContentTypes`, `getExternalContentTypes`, `getInternalContentTypes`.
641
- - `getContentTypesForManager`: returns serializable content types without Zod schemas and without content types marked with `hideFromManager()`.
642
- - `getContentTypeByName`.
643
-
644
- ## Fields
645
-
646
- Use the concise `f` namespace for field factories. `Fields` remains exported as
647
- a backward-compatible alias and references the same object.
648
-
649
- Main factories:
650
-
651
- ```ts
652
- f.string();
653
- f.number();
654
- f.boolean();
655
- f.date();
656
- f.select(["draft", "published"]);
657
- f.relation(Post);
658
- f.contentReference("Post");
659
- f.selfRelation();
660
- f.blocks([{ name: "title", field: f.string() }]);
661
- f.array(f.string());
586
+ persisting that relation on the source document. For example, a category gallery
587
+ can create one item per `Category` and collect the images of its related
588
+ `Project` documents:
589
+
590
+ ```ts
591
+ const Category = new ContentType({
592
+ name: "Category",
593
+ dynamicDataSource: true,
594
+ fields: {
595
+ title: f.string().required(),
596
+ },
597
+ });
598
+
599
+ const Project = new ContentType({
600
+ name: "Project",
601
+ dynamicDataSource: true,
602
+ fields: {
603
+ category: f.relation(Category, "existing").required(),
604
+ images: f.file().type("Image").multiple().required(),
605
+ },
606
+ });
607
+
608
+ const galleryBindings = {
609
+ lists: {
610
+ items: {
611
+ contentType: Category.name,
612
+ itemName: "CategoriesGalleryItem",
613
+ map: {
614
+ title: { contentType: Category.name, path: "title" },
615
+ images: {
616
+ kind: "relatedCollection",
617
+ contentType: Project.name,
618
+ relation: "category",
619
+ path: "images",
620
+ limit: 10,
621
+ },
622
+ },
623
+ },
624
+ },
625
+ };
626
+ ```
627
+
628
+ The related collection query matches `Project.category._id` against the current
629
+ category, preserves project and image order, and flattens the selected array by
630
+ one level. Its numeric limit applies to related projects and is capped at 100.
631
+
632
+ Schema and validation methods:
633
+
634
+ - `getInputSchema()`: write schema. Includes `_type`, `createdBy`, and `updatedBy`.
635
+ - `getSchema()`: persisted schema.
636
+ - `getPopulatedSchema()`: output with populated relations and `_id`.
637
+ - `getOutputSchema()`: public API output.
638
+ - `getOutputSchemaWithoutIterators()`: output variant without iterator fields.
639
+ - `validate`, `partialValidate`, `validateOutput`: Zod helpers.
640
+
641
+ The registry lives in `lib/Registry`:
642
+
643
+ - `registerContentType`, `registerInternalContentType`.
644
+ - `getContentTypes`, `getExternalContentTypes`, `getInternalContentTypes`.
645
+ - `getContentTypesForManager`: returns serializable content types without Zod schemas and without content types marked with `hideFromManager()`.
646
+ - `getContentTypeByName`.
647
+
648
+ ## Fields
649
+
650
+ Use the concise `f` namespace for field factories. `Fields` remains exported as
651
+ a backward-compatible alias and references the same object.
652
+
653
+ Main factories:
654
+
655
+ ```ts
656
+ f.string();
657
+ f.number();
658
+ f.boolean();
659
+ f.date();
660
+ f.select(["draft", "published"]);
661
+ f.relation(Post);
662
+ f.contentReference("Post");
663
+ f.selfRelation();
664
+ f.blocks([{ name: "title", field: f.string() }]);
665
+ f.array(f.string());
662
666
  f.link();
663
667
  f.file();
664
668
  f.breadcrums();
665
- ```
666
-
667
- Common modifiers:
668
-
669
- - `.required()`: marks field as required.
670
- - `.translatable()`: stores values per language with shape `{ _tag: "Translatable", ... }`.
671
- - `.apiOnly()`: available for API/persistence, hidden from manager.
672
- - `.managerOnly()`: visible to manager, excluded from API output.
673
-
674
- Notable fields:
675
-
676
- - `StringField`: UI `Text`, `Textarea`, `RichText`, `Email`, `Slug`, `Password`, `Id`, `Url`; supports `.min()` and `.max()`.
669
+ ```
670
+
671
+ Common modifiers:
672
+
673
+ - `.required()`: marks field as required.
674
+ - `.translatable()`: stores values per language with shape `{ _tag: "Translatable", ... }`.
675
+ - `.apiOnly()`: available for API/persistence, hidden from manager.
676
+ - `.managerOnly()`: visible to manager, excluded from API output.
677
+
678
+ Notable fields:
679
+
680
+ - `StringField`: UI `Text`, `Textarea`, `RichText`, `Email`, `Slug`, `Password`, `Id`, `Url`; supports `.min()` and `.max()`.
677
681
  - `NumberField`: supports `.min()` and `.max()`.
678
682
  - `DateField`: supports `.type("Date")`, `.type("DateTime")` and
679
683
  `.type("Time")`. Date and datetime writes accept JavaScript `Date` values or
680
684
  their ISO datetime JSON representation and normalize them to `Date`; time
681
685
  values remain ISO time strings.
682
686
  - `RelationField`: relation to another `ContentType`; accepts existing references or inline creation. `f.relation(Post, "existing")` restricts to existing records; `"new"` restricts to new records. `.multiple()` returns a homogeneous array of relations.
683
- - `ContentReferenceField`: reference by content type name.
687
+ - `ContentReferenceField`: reference by content type name.
684
688
  - `LinkField`: the manager stores a direct `{ href, title }` value or an
685
689
  internal `{ routeId, contentTypeId, title }` reference. Titled links resolve
686
690
  to `{ href, title }` in web output, with internal `href` values localized by
@@ -693,462 +697,462 @@ Notable fields:
693
697
  returns `null` when the content is resolved outside a routable page and is
694
698
  never accepted as input, persisted, or rendered in the manager.
695
699
  - `FileField`: integrates media and optimization options.
696
- - `f.blocks(...)`: heterogeneous ordered list. Each item stores a `name` and a `value`, and the value can match one of the named field shapes. Use it for block-like content where different item types can appear in the same list.
697
- - `f.array(...)`: homogeneous ordered list. Every item uses the same field shape. Multi-value fields such as relation `.multiple()` use this backing model.
698
- - `IteratorField`: repeatable structure based on content type entries.
699
-
700
- ## Derived Types
701
-
702
- `@rakun-kit/core/types` and `lib/types` derive types from `ContentType`:
703
-
704
- - `DataInput<T>`: accepted shape for create/update.
705
- - `DBOutput<T>`: persisted shape with metadata (`_id`, `_type`, timestamps, authorship).
706
- - `DataPopulated<T>`: shape with populated relations.
707
- - `DataFront<T>`: public output without `apiOnly` fields.
708
- - `Filter<T>`, `Query<T>`, `ListInput<T>`, `GetAllInput<T>`: typed filters and list inputs.
709
- - `MaybeTranslatableValue<T>`, `TranslatableValue<T>`.
710
-
711
- ## ORM and Mongo
712
-
713
- `core/src/orm` implements `DBService` on top of MongoDB:
714
-
715
- ```ts
716
- const db = await getMongoService();
717
-
718
- const post = await db.create(Post, {
719
- _type: "Post",
720
- title: "Hello",
721
- slug: "hello",
722
- });
723
- ```
724
-
725
- Operations:
726
-
727
- - `get(contentType, id, fields?)`
728
- - `list(contentType, query)`
729
- - `create(contentType, data)`
730
- - `update(contentType, id, data)`
731
- - `updateMany(contentType, filter, data)`
732
- - `delete(contentType, filter)`
733
- - `find(contentType, filter, fields?)`
734
- - `clear(contentType)`
735
- - `findDependencies(contentType, id)`
736
- - `upsert(contentType, filter, data)`
737
- - `getAll(contentType, query?)`
738
-
739
- Connection:
740
-
741
- - `createMongoConnection(config)`: stores config.
742
- - `createMongoService(config)`: connects and creates handlers.
743
- - `getMongoService()`: returns the singleton or creates it from config.
744
- - `closeMongoService()`: closes the connection and clears the singleton.
745
-
746
- `MongoConfig`:
747
-
748
- ```ts
749
- type MongoConfig = {
750
- MONGO_URI: string;
751
- ENVIRONMENT?: "local" | "development" | "test" | "production";
752
- };
753
- ```
754
-
755
- In environments other than `test`, the connection creates indexes defined by `createIndexes`.
756
-
757
- ## API Operations
758
-
759
- `api/operations` defines typed contracts and handlers for endpoints:
760
-
761
- - Manager: CRUD, auth, MFA, media, literals, settings.
762
- - Manager comments and mentions: `manager.comments.list`,
763
- `manager.comments.create`, `manager.comments.toggleReaction`,
764
- `manager.comments.markRead`, `manager.comments.unreadCount`,
765
- `manager.users.mentions`,
766
- `manager.notifications.list`, and `manager.notifications.markRead`.
767
- - Web: page resolution.
768
-
769
- Main helpers:
770
-
771
- - `defineOperationContract`: declares a contract with Zod input/output, method, path, and metadata.
772
- - `defineOperation`: combines a contract with its implementation.
773
- - `createRakunApiClient`: creates a browser/server HTTP client for custom operations.
774
- - `GetClient`: derives a typed client from an operation map.
775
- - `createManagerOperationContracts`, `createWebOperationContracts`.
776
- - `createManagerOperationDefinitions`, `createWebOperationDefinitions`.
777
- - `createRakunOperationDefinitions`: combines manager + web.
778
- - `createOperationManifest`: serializable metadata for clients.
779
- - `createOperationPath`: HTTP path from operation name.
780
- - `mergeOperationContracts`: combines maps.
781
-
782
- `@rakun-kit/core/manager` exposes the manifest and name-based types:
783
-
784
- ```ts
785
- import {
786
- managerOperationManifest,
787
- getManagerOperationMeta,
788
- type ManagerOperationInput,
789
- type ManagerOperationOutput,
790
- } from "@rakun-kit/core/manager";
791
- ```
792
-
793
- ### Custom Operations
794
-
795
- Applications can define operations in a separate object, pass that object to
796
- bootstrap, and reuse its type on the frontend:
797
-
798
- ```ts
799
- // server/api-operations.ts
800
- import { defineOperation } from "@rakun-kit/core";
801
- import { z } from "zod";
802
-
803
- export const apiOperations = {
804
- "demo.helloWorld": defineOperation<
805
- { text: string },
806
- { message: string },
807
- "query",
808
- "get",
809
- "public"
810
- >({
811
- access: "public",
812
- kind: "query",
813
- method: "get",
814
- description: "Return a hello world message with the provided text",
815
- input: z.object({
816
- text: z.string().default("world"),
817
- }),
818
- output: z.object({
819
- message: z.string(),
820
- }),
821
- resolve: ({ input }) => ({
822
- message: `Hello ${input.text}`,
823
- }),
824
- }),
825
- };
826
- ```
827
-
828
- ```ts
829
- // bootstrap
830
- import { apiOperations } from "./server/api-operations";
831
-
832
- rakunBootstrap({
833
- // ...
834
- apiOperations,
835
- });
836
- ```
837
-
838
- Operation names define their HTTP path: `demo.helloWorld` becomes
839
- `/demo/helloWorld`.
840
-
841
- Rules:
842
-
843
- - Names must not collide with built-in operations.
844
- - Operations prefixed with `manager.` are included in manager operation routers.
845
- - Operations prefixed with `web.` are included in web operation routers.
846
- - Operations with any other prefix are included when using the combined operation registry.
847
- - Use `access: "public"` for operations called from public web pages.
848
- - Use `access: "auth"` for operations that require a manager session.
849
-
850
- The manager route `manager.apiOperations` returns a JSON-serializable operation
851
- catalog for manager API docs/playgrounds. Input and output Zod schemas are
852
- converted to JSON Schema for display.
853
-
854
- ### Typed API Client
855
-
856
- `@rakun-kit/core/web` exposes a small typed HTTP client for operation maps:
857
-
858
- ```ts
859
- import { createRakunApiClient, type GetClient } from "@rakun-kit/core/web";
860
- import type { apiOperations } from "./server/api-operations";
861
-
862
- type ApiClient = GetClient<typeof apiOperations>;
863
-
864
- const client: ApiClient = createRakunApiClient<typeof apiOperations>({
865
- baseUrl: "/api",
866
- });
867
-
868
- const result = await client.query("demo.helloWorld", {
869
- text: "Rakun",
870
- });
871
-
872
- result.message;
873
- ```
874
-
875
- The client exposes:
876
-
877
- - `query(name, input?, options?)`: only accepts operations with `kind: "query"`.
878
- - `mutation(name, input?, options?)`: only accepts operations with `kind: "mutation"`.
879
-
880
- The client derives input and output types from the Zod schemas in
881
- `apiOperations`.
882
-
883
- ## Request Context and Auth
884
-
885
- `createRequestContext(input)` normalizes headers, cookies, and response:
886
-
887
- ```ts
888
- const ctx = await createRequestContext({
889
- headers: req.headers,
890
- cookies,
891
- res: {
892
- setHeader: res.setHeader.bind(res),
893
- cookie: res.cookie.bind(res),
894
- },
895
- });
896
- ```
897
-
898
- The resulting context includes:
899
-
900
- - `req.headers`, `req.cookies`
901
- - `res.setHeader`, `res.cookie`
902
- - `user`: manager user or `null`
903
- - `getUser()`: returns the user or throws `AUTH_REQUIRED`
904
-
905
- Session cookies are managed with `getSessionCookie` and `setSessionCookie`.
906
-
907
- ## Media
908
-
909
- Media uses a storage adapter:
910
-
911
- ```ts
912
- createMediaService({
913
- adapter,
914
- defaultAccess: "private",
915
- defaultGetExpiresInSeconds: 300,
916
- uploadUrl: "/api/rakun/manager/media/upload",
917
- });
918
- ```
919
-
920
- APIs:
921
-
922
- - `createMediaConnection(config)`: stores config without creating the service.
923
- - `createMediaService(config)`: creates the singleton with an adapter.
924
- - `getMediaService()`: returns the singleton or creates it from config.
925
- - `handleMediaBinaryUpload`: processes manager binary uploads.
926
-
700
+ - `f.blocks(...)`: heterogeneous ordered list. Each item stores a `name` and a `value`, and the value can match one of the named field shapes. Use it for block-like content where different item types can appear in the same list.
701
+ - `f.array(...)`: homogeneous ordered list. Every item uses the same field shape. Multi-value fields such as relation `.multiple()` use this backing model.
702
+ - `IteratorField`: repeatable structure based on content type entries.
703
+
704
+ ## Derived Types
705
+
706
+ `@rakun-kit/core/types` and `lib/types` derive types from `ContentType`:
707
+
708
+ - `DataInput<T>`: accepted shape for create/update.
709
+ - `DBOutput<T>`: persisted shape with metadata (`_id`, `_type`, timestamps, authorship).
710
+ - `DataPopulated<T>`: shape with populated relations.
711
+ - `DataFront<T>`: public output without `apiOnly` fields.
712
+ - `Filter<T>`, `Query<T>`, `ListInput<T>`, `GetAllInput<T>`: typed filters and list inputs.
713
+ - `MaybeTranslatableValue<T>`, `TranslatableValue<T>`.
714
+
715
+ ## ORM and Mongo
716
+
717
+ `core/src/orm` implements `DBService` on top of MongoDB:
718
+
719
+ ```ts
720
+ const db = await getMongoService();
721
+
722
+ const post = await db.create(Post, {
723
+ _type: "Post",
724
+ title: "Hello",
725
+ slug: "hello",
726
+ });
727
+ ```
728
+
729
+ Operations:
730
+
731
+ - `get(contentType, id, fields?)`
732
+ - `list(contentType, query)`
733
+ - `create(contentType, data)`
734
+ - `update(contentType, id, data)`
735
+ - `updateMany(contentType, filter, data)`
736
+ - `delete(contentType, filter)`
737
+ - `find(contentType, filter, fields?)`
738
+ - `clear(contentType)`
739
+ - `findDependencies(contentType, id)`
740
+ - `upsert(contentType, filter, data)`
741
+ - `getAll(contentType, query?)`
742
+
743
+ Connection:
744
+
745
+ - `createMongoConnection(config)`: stores config.
746
+ - `createMongoService(config)`: connects and creates handlers.
747
+ - `getMongoService()`: returns the singleton or creates it from config.
748
+ - `closeMongoService()`: closes the connection and clears the singleton.
749
+
750
+ `MongoConfig`:
751
+
752
+ ```ts
753
+ type MongoConfig = {
754
+ MONGO_URI: string;
755
+ ENVIRONMENT?: "local" | "development" | "test" | "production";
756
+ };
757
+ ```
758
+
759
+ In environments other than `test`, the connection creates indexes defined by `createIndexes`.
760
+
761
+ ## API Operations
762
+
763
+ `api/operations` defines typed contracts and handlers for endpoints:
764
+
765
+ - Manager: CRUD, auth, MFA, media, literals, settings.
766
+ - Manager comments and mentions: `manager.comments.list`,
767
+ `manager.comments.create`, `manager.comments.toggleReaction`,
768
+ `manager.comments.markRead`, `manager.comments.unreadCount`,
769
+ `manager.users.mentions`,
770
+ `manager.notifications.list`, and `manager.notifications.markRead`.
771
+ - Web: page resolution.
772
+
773
+ Main helpers:
774
+
775
+ - `defineOperationContract`: declares a contract with Zod input/output, method, path, and metadata.
776
+ - `defineOperation`: combines a contract with its implementation.
777
+ - `createRakunApiClient`: creates a browser/server HTTP client for custom operations.
778
+ - `GetClient`: derives a typed client from an operation map.
779
+ - `createManagerOperationContracts`, `createWebOperationContracts`.
780
+ - `createManagerOperationDefinitions`, `createWebOperationDefinitions`.
781
+ - `createRakunOperationDefinitions`: combines manager + web.
782
+ - `createOperationManifest`: serializable metadata for clients.
783
+ - `createOperationPath`: HTTP path from operation name.
784
+ - `mergeOperationContracts`: combines maps.
785
+
786
+ `@rakun-kit/core/manager` exposes the manifest and name-based types:
787
+
788
+ ```ts
789
+ import {
790
+ managerOperationManifest,
791
+ getManagerOperationMeta,
792
+ type ManagerOperationInput,
793
+ type ManagerOperationOutput,
794
+ } from "@rakun-kit/core/manager";
795
+ ```
796
+
797
+ ### Custom Operations
798
+
799
+ Applications can define operations in a separate object, pass that object to
800
+ bootstrap, and reuse its type on the frontend:
801
+
802
+ ```ts
803
+ // server/api-operations.ts
804
+ import { defineOperation } from "@rakun-kit/core";
805
+ import { z } from "zod";
806
+
807
+ export const apiOperations = {
808
+ "demo.helloWorld": defineOperation<
809
+ { text: string },
810
+ { message: string },
811
+ "query",
812
+ "get",
813
+ "public"
814
+ >({
815
+ access: "public",
816
+ kind: "query",
817
+ method: "get",
818
+ description: "Return a hello world message with the provided text",
819
+ input: z.object({
820
+ text: z.string().default("world"),
821
+ }),
822
+ output: z.object({
823
+ message: z.string(),
824
+ }),
825
+ resolve: ({ input }) => ({
826
+ message: `Hello ${input.text}`,
827
+ }),
828
+ }),
829
+ };
830
+ ```
831
+
832
+ ```ts
833
+ // bootstrap
834
+ import { apiOperations } from "./server/api-operations";
835
+
836
+ rakunBootstrap({
837
+ // ...
838
+ apiOperations,
839
+ });
840
+ ```
841
+
842
+ Operation names define their HTTP path: `demo.helloWorld` becomes
843
+ `/demo/helloWorld`.
844
+
845
+ Rules:
846
+
847
+ - Names must not collide with built-in operations.
848
+ - Operations prefixed with `manager.` are included in manager operation routers.
849
+ - Operations prefixed with `web.` are included in web operation routers.
850
+ - Operations with any other prefix are included when using the combined operation registry.
851
+ - Use `access: "public"` for operations called from public web pages.
852
+ - Use `access: "auth"` for operations that require a manager session.
853
+
854
+ The manager route `manager.apiOperations` returns a JSON-serializable operation
855
+ catalog for manager API docs/playgrounds. Input and output Zod schemas are
856
+ converted to JSON Schema for display.
857
+
858
+ ### Typed API Client
859
+
860
+ `@rakun-kit/core/web` exposes a small typed HTTP client for operation maps:
861
+
862
+ ```ts
863
+ import { createRakunApiClient, type GetClient } from "@rakun-kit/core/web";
864
+ import type { apiOperations } from "./server/api-operations";
865
+
866
+ type ApiClient = GetClient<typeof apiOperations>;
867
+
868
+ const client: ApiClient = createRakunApiClient<typeof apiOperations>({
869
+ baseUrl: "/api",
870
+ });
871
+
872
+ const result = await client.query("demo.helloWorld", {
873
+ text: "Rakun",
874
+ });
875
+
876
+ result.message;
877
+ ```
878
+
879
+ The client exposes:
880
+
881
+ - `query(name, input?, options?)`: only accepts operations with `kind: "query"`.
882
+ - `mutation(name, input?, options?)`: only accepts operations with `kind: "mutation"`.
883
+
884
+ The client derives input and output types from the Zod schemas in
885
+ `apiOperations`.
886
+
887
+ ## Request Context and Auth
888
+
889
+ `createRequestContext(input)` normalizes headers, cookies, and response:
890
+
891
+ ```ts
892
+ const ctx = await createRequestContext({
893
+ headers: req.headers,
894
+ cookies,
895
+ res: {
896
+ setHeader: res.setHeader.bind(res),
897
+ cookie: res.cookie.bind(res),
898
+ },
899
+ });
900
+ ```
901
+
902
+ The resulting context includes:
903
+
904
+ - `req.headers`, `req.cookies`
905
+ - `res.setHeader`, `res.cookie`
906
+ - `user`: manager user or `null`
907
+ - `getUser()`: returns the user or throws `AUTH_REQUIRED`
908
+
909
+ Session cookies are managed with `getSessionCookie` and `setSessionCookie`.
910
+
911
+ ## Media
912
+
913
+ Media uses a storage adapter:
914
+
915
+ ```ts
916
+ createMediaService({
917
+ adapter,
918
+ defaultAccess: "private",
919
+ defaultGetExpiresInSeconds: 300,
920
+ uploadUrl: "/api/rakun/manager/media/upload",
921
+ });
922
+ ```
923
+
924
+ APIs:
925
+
926
+ - `createMediaConnection(config)`: stores config without creating the service.
927
+ - `createMediaService(config)`: creates the singleton with an adapter.
928
+ - `getMediaService()`: returns the singleton or creates it from config.
929
+ - `handleMediaBinaryUpload`: processes manager binary uploads.
930
+
927
931
  The service supports prepare/finalize upload, URL generation, folders, and image optimization depending on adapter/configuration. Manager uploads encode original file names for transport, so Unicode names (including accents, non-Latin scripts, and emoji) are preserved without placing invalid characters in HTTP headers.
928
932
 
929
- Existing images can be reimported from the media manager with the selected optimization settings. The replacement uses new storage keys and updates the existing `Media` record only after every requested variant has been written, preserving its ID and content references.
930
-
931
- ## Persistent Event Log
932
-
933
- Rakun keeps business/audit events separate from its technical console logger.
934
- The event log keeps events immutable and defaults to a MongoDB collection
935
- with indexes for time, type, category, outcome, severity, correlation and tags:
936
-
937
- ```ts
938
- import { recordEvent, queryEvents } from "@rakun-kit/core";
939
-
940
- await recordEvent({
941
- type: "content.article.published",
942
- category: "content",
943
- outcome: "success",
944
- actor: { type: "manager-user", id: userId },
945
- resource: { type: "Article", id: articleId },
946
- correlationId: requestId,
947
- tags: ["editorial"],
948
- data: {
949
- locale: "es",
950
- changedFields: 3,
951
- },
952
- });
953
-
954
- const page = await queryEvents({
955
- categories: ["content"],
956
- outcomes: ["success"],
957
- from: new Date("2026-01-01T00:00:00.000Z"),
958
- limit: 50,
959
- });
960
- ```
961
-
962
- `data` accepts nested JSON values. Event queries use cursor pagination and can
963
- filter by types, categories, severities, outcomes, sources, correlation,
964
- required tags and a date range. A custom persistence implementation can be
965
- plugged in globally:
966
-
967
- ```ts
968
- import type { EventLogAdapter } from "@rakun-kit/core";
969
-
970
- const adapter: EventLogAdapter = {
971
- async append(event) {
972
- return customStore.append(event);
973
- },
974
- async query(filters) {
975
- return customStore.query(filters);
976
- },
977
- async deleteBefore(before) {
978
- return customStore.deleteBefore(before);
979
- },
980
- };
981
-
982
- rakunBootstrap({
983
- // ...
984
- eventLog: { adapter },
985
- });
986
- ```
987
-
988
- Plugins receive the resolved `eventLog` service in their initialization
989
- context. Reading a shared event stream should be protected with the built-in
990
- `system.eventLog.read` permission. The built-in `manager.logs.list` operation and the
991
- manager Settings → Logs screen both enforce it.
992
-
993
- Retention cleanup is available from the manager Logs screen to users with the
994
- built-in `system.eventLog.manage` permission. It deletes events strictly older
995
- than the selected cutoff. Custom adapters must implement `deleteBefore` to
996
- support this action. The cleanup mutation itself is logged after deletion.
997
-
998
- Failed API operations are persisted automatically as `api.operation.failed`
999
- events. This includes expected 4xx application errors and unexpected 5xx
1000
- failures across the core operation wrapper and the Express, Next.js, and tRPC
1001
- adapters. Events include the operation, status, method, kind, correlation ID,
1002
- and authenticated actor when available. Request payloads, application error
1003
- causes, and raw internal error messages are not persisted.
1004
-
1005
- Every successful API mutation is also persisted with an operation-specific
1006
- event type such as `manager.create.succeeded` or
1007
- `manager.backups.restore.succeeded`. Successful queries are not recorded by
1008
- default, which keeps the event stream focused on state changes. Mutation
1009
- events contain operation metadata and actor/correlation context, but never
1010
- copy the input payload or result.
1011
-
1012
- ## Mail
1013
-
1014
- Mail providers receive normalized, already-rendered messages through
1015
- `MailAdapter`, so neither `core` nor an adapter depends on React or a template
1016
- engine:
1017
-
1018
- ```ts
1019
- import type { MailAdapter } from "@rakun-kit/core";
1020
-
1021
- const adapter: MailAdapter = {
1022
- async send(message) {
1023
- const result = await provider.send(message);
1024
- return { id: result.id };
1025
- },
1026
- };
1027
-
1028
- rakunBootstrap({
1029
- // ...
1030
- mail: {
1031
- adapter,
1032
- defaultFrom: "hello@example.com",
1033
- defaultReplyTo: "support@example.com",
1034
- },
1035
- });
1036
- ```
1037
-
1038
- Send rendered content directly:
1039
-
1040
- ```ts
1041
- import { sendMail } from "@rakun-kit/core";
1042
-
1043
- await sendMail({
1044
- to: "ada@example.com",
1045
- subject: "Welcome",
1046
- html: "<p>Hello Ada</p>",
1047
- text: "Hello Ada",
1048
- });
1049
- ```
1050
-
1051
- Or create a typed application template registry:
1052
-
1053
- ```ts
1054
- import { createMailSender, defineMailTemplate } from "@rakun-kit/core";
1055
-
1056
- const mail = createMailSender({
1057
- templates: {
1058
- welcome: defineMailTemplate<{ name: string }>({
1059
- subject: ({ name }) => `Welcome, ${name}`,
1060
- render: ({ name }) => ({
1061
- html: `<p>Hello ${name}</p>`,
1062
- text: `Hello ${name}`,
1063
- }),
1064
- }),
1065
- },
1066
- });
1067
-
1068
- await mail.send({
1069
- template: "welcome",
1070
- props: { name: "Ada" },
1071
- to: "ada@example.com",
1072
- });
1073
- ```
1074
-
1075
- The common contract supports To/CC/BCC/Reply-To, custom headers and in-memory
1076
- `Uint8Array` attachments. Sending is immediate; queues, retries and delivery
1077
- events belong to application infrastructure.
1078
-
1079
- Every mail sent through a bootstrapped Rakun mail service creates append-only
1080
- `mail.send.attempted` and `mail.send.succeeded` or `mail.send.failed` events.
1081
- They share a correlation id and include only operational counts, provider,
1082
- template and duration. Recipient addresses, subject, HTML/text, headers,
1083
- attachment names/content, credentials and raw provider errors are never copied
1084
- to the persistent event log. If the initial attempt event cannot be persisted,
1085
- the provider is not called.
1086
-
1087
- ## Literals and Translation
1088
-
1089
- Bootstrap receives `literals`. Related utilities:
1090
-
1091
- - `getTranslation`: resolves translatable values.
1092
- - `translateObject`: translates objects with translatable fields.
1093
- - Manager schemas for listing/upserting website literals (not manager UI chrome).
1094
-
1095
- Translatable values use this shape:
1096
-
1097
- ```ts
1098
- {
1099
- _tag: "Translatable",
1100
- en: "Title",
1101
- es: "Titulo"
1102
- }
1103
- ```
1104
-
1105
- ## Permissions
1106
-
1107
- `lib/Permissions` exposes:
1108
-
1109
- - `getPermissionList`: generates a permission list.
1110
- - `hasPermissions`: validates user/role permissions.
1111
- - `mapPermissions`: transforms permissions.
1112
- - `Permission`: public type.
1113
-
1114
- Manager routes apply permissions, ownership, and auth through utilities in `api/utils`.
1115
-
1116
- ## Errors
1117
-
1118
- Application errors live in `lib/errors`:
1119
-
1120
- - `AppError`
1121
- - `throwAppError`
1122
- - `errors`
1123
- - `instanceofAppErrorShape`
1124
-
1125
- Database errors live in `orm/dbService`:
1126
-
1127
- - `DbError`
1128
- - `DbErrorUnknown`
1129
- - `DbErrorNotFound`
1130
- - `DbErrorInvalidData`
1131
- - `DbErrorConflict`
1132
- - `DbErrorSimulatedFailure`
1133
-
1134
- ## Runtime Flow
1135
-
1136
- 1. The app defines content types with `ContentType` and `f`.
1137
- 2. The app calls `rakunBootstrap(options)`.
1138
- 3. The HTTP adapter calls `ensureRakunInitialized()` before serving Rakun routes.
1139
- 4. `ensureRakunInitialized()` configures logger, MongoDB, the persistent event
1140
- log, media, mail, and route syncing.
1141
- 5. Each request creates a `RakunRequestContext`.
1142
- 6. Manager/web operations validate input, run logic, validate output, and return typed contracts.
1143
-
1144
- ## Tests and Development Notes
1145
-
1146
- The package includes targeted tests next to modules (`*.test.ts`), for example ORM, translation, routes, redirects, and populated relations.
1147
-
1148
- Package build:
1149
-
1150
- ```sh
1151
- bun run build --workspace @rakun-kit/core
1152
- ```
1153
-
1154
- The script generates ESM and CJS in `dist/`, adjusting imports and the CJS `package.json` through repo scripts.
933
+ Existing images can be reimported from the media manager with the selected optimization settings. The replacement uses new storage keys and updates the existing `Media` record only after every requested variant has been written, preserving its ID and content references. Preview generation stores a compact `data:image/...` string on `previewUrl` for LQIP use in `@rakun-kit/react` `Image`, instead of writing a separate preview object to storage.
934
+
935
+ ## Persistent Event Log
936
+
937
+ Rakun keeps business/audit events separate from its technical console logger.
938
+ The event log keeps events immutable and defaults to a MongoDB collection
939
+ with indexes for time, type, category, outcome, severity, correlation and tags:
940
+
941
+ ```ts
942
+ import { recordEvent, queryEvents } from "@rakun-kit/core";
943
+
944
+ await recordEvent({
945
+ type: "content.article.published",
946
+ category: "content",
947
+ outcome: "success",
948
+ actor: { type: "manager-user", id: userId },
949
+ resource: { type: "Article", id: articleId },
950
+ correlationId: requestId,
951
+ tags: ["editorial"],
952
+ data: {
953
+ locale: "es",
954
+ changedFields: 3,
955
+ },
956
+ });
957
+
958
+ const page = await queryEvents({
959
+ categories: ["content"],
960
+ outcomes: ["success"],
961
+ from: new Date("2026-01-01T00:00:00.000Z"),
962
+ limit: 50,
963
+ });
964
+ ```
965
+
966
+ `data` accepts nested JSON values. Event queries use cursor pagination and can
967
+ filter by types, categories, severities, outcomes, sources, correlation,
968
+ required tags and a date range. A custom persistence implementation can be
969
+ plugged in globally:
970
+
971
+ ```ts
972
+ import type { EventLogAdapter } from "@rakun-kit/core";
973
+
974
+ const adapter: EventLogAdapter = {
975
+ async append(event) {
976
+ return customStore.append(event);
977
+ },
978
+ async query(filters) {
979
+ return customStore.query(filters);
980
+ },
981
+ async deleteBefore(before) {
982
+ return customStore.deleteBefore(before);
983
+ },
984
+ };
985
+
986
+ rakunBootstrap({
987
+ // ...
988
+ eventLog: { adapter },
989
+ });
990
+ ```
991
+
992
+ Plugins receive the resolved `eventLog` service in their initialization
993
+ context. Reading a shared event stream should be protected with the built-in
994
+ `system.eventLog.read` permission. The built-in `manager.logs.list` operation and the
995
+ manager Settings → Logs screen both enforce it.
996
+
997
+ Retention cleanup is available from the manager Logs screen to users with the
998
+ built-in `system.eventLog.manage` permission. It deletes events strictly older
999
+ than the selected cutoff. Custom adapters must implement `deleteBefore` to
1000
+ support this action. The cleanup mutation itself is logged after deletion.
1001
+
1002
+ Failed API operations are persisted automatically as `api.operation.failed`
1003
+ events. This includes expected 4xx application errors and unexpected 5xx
1004
+ failures across the core operation wrapper and the Express, Next.js, and tRPC
1005
+ adapters. Events include the operation, status, method, kind, correlation ID,
1006
+ and authenticated actor when available. Request payloads, application error
1007
+ causes, and raw internal error messages are not persisted.
1008
+
1009
+ Every successful API mutation is also persisted with an operation-specific
1010
+ event type such as `manager.create.succeeded` or
1011
+ `manager.backups.restore.succeeded`. Successful queries are not recorded by
1012
+ default, which keeps the event stream focused on state changes. Mutation
1013
+ events contain operation metadata and actor/correlation context, but never
1014
+ copy the input payload or result.
1015
+
1016
+ ## Mail
1017
+
1018
+ Mail providers receive normalized, already-rendered messages through
1019
+ `MailAdapter`, so neither `core` nor an adapter depends on React or a template
1020
+ engine:
1021
+
1022
+ ```ts
1023
+ import type { MailAdapter } from "@rakun-kit/core";
1024
+
1025
+ const adapter: MailAdapter = {
1026
+ async send(message) {
1027
+ const result = await provider.send(message);
1028
+ return { id: result.id };
1029
+ },
1030
+ };
1031
+
1032
+ rakunBootstrap({
1033
+ // ...
1034
+ mail: {
1035
+ adapter,
1036
+ defaultFrom: "hello@example.com",
1037
+ defaultReplyTo: "support@example.com",
1038
+ },
1039
+ });
1040
+ ```
1041
+
1042
+ Send rendered content directly:
1043
+
1044
+ ```ts
1045
+ import { sendMail } from "@rakun-kit/core";
1046
+
1047
+ await sendMail({
1048
+ to: "ada@example.com",
1049
+ subject: "Welcome",
1050
+ html: "<p>Hello Ada</p>",
1051
+ text: "Hello Ada",
1052
+ });
1053
+ ```
1054
+
1055
+ Or create a typed application template registry:
1056
+
1057
+ ```ts
1058
+ import { createMailSender, defineMailTemplate } from "@rakun-kit/core";
1059
+
1060
+ const mail = createMailSender({
1061
+ templates: {
1062
+ welcome: defineMailTemplate<{ name: string }>({
1063
+ subject: ({ name }) => `Welcome, ${name}`,
1064
+ render: ({ name }) => ({
1065
+ html: `<p>Hello ${name}</p>`,
1066
+ text: `Hello ${name}`,
1067
+ }),
1068
+ }),
1069
+ },
1070
+ });
1071
+
1072
+ await mail.send({
1073
+ template: "welcome",
1074
+ props: { name: "Ada" },
1075
+ to: "ada@example.com",
1076
+ });
1077
+ ```
1078
+
1079
+ The common contract supports To/CC/BCC/Reply-To, custom headers and in-memory
1080
+ `Uint8Array` attachments. Sending is immediate; queues, retries and delivery
1081
+ events belong to application infrastructure.
1082
+
1083
+ Every mail sent through a bootstrapped Rakun mail service creates append-only
1084
+ `mail.send.attempted` and `mail.send.succeeded` or `mail.send.failed` events.
1085
+ They share a correlation id and include only operational counts, provider,
1086
+ template and duration. Recipient addresses, subject, HTML/text, headers,
1087
+ attachment names/content, credentials and raw provider errors are never copied
1088
+ to the persistent event log. If the initial attempt event cannot be persisted,
1089
+ the provider is not called.
1090
+
1091
+ ## Literals and Translation
1092
+
1093
+ Bootstrap receives `literals`. Related utilities:
1094
+
1095
+ - `getTranslation`: resolves translatable values.
1096
+ - `translateObject`: translates objects with translatable fields.
1097
+ - Manager schemas for listing/upserting website literals (not manager UI chrome).
1098
+
1099
+ Translatable values use this shape:
1100
+
1101
+ ```ts
1102
+ {
1103
+ _tag: "Translatable",
1104
+ en: "Title",
1105
+ es: "Titulo"
1106
+ }
1107
+ ```
1108
+
1109
+ ## Permissions
1110
+
1111
+ `lib/Permissions` exposes:
1112
+
1113
+ - `getPermissionList`: generates a permission list.
1114
+ - `hasPermissions`: validates user/role permissions.
1115
+ - `mapPermissions`: transforms permissions.
1116
+ - `Permission`: public type.
1117
+
1118
+ Manager routes apply permissions, ownership, and auth through utilities in `api/utils`.
1119
+
1120
+ ## Errors
1121
+
1122
+ Application errors live in `lib/errors`:
1123
+
1124
+ - `AppError`
1125
+ - `throwAppError`
1126
+ - `errors`
1127
+ - `instanceofAppErrorShape`
1128
+
1129
+ Database errors live in `orm/dbService`:
1130
+
1131
+ - `DbError`
1132
+ - `DbErrorUnknown`
1133
+ - `DbErrorNotFound`
1134
+ - `DbErrorInvalidData`
1135
+ - `DbErrorConflict`
1136
+ - `DbErrorSimulatedFailure`
1137
+
1138
+ ## Runtime Flow
1139
+
1140
+ 1. The app defines content types with `ContentType` and `f`.
1141
+ 2. The app calls `rakunBootstrap(options)`.
1142
+ 3. The HTTP adapter calls `ensureRakunInitialized()` before serving Rakun routes.
1143
+ 4. `ensureRakunInitialized()` configures logger, MongoDB, the persistent event
1144
+ log, media, mail, and route syncing.
1145
+ 5. Each request creates a `RakunRequestContext`.
1146
+ 6. Manager/web operations validate input, run logic, validate output, and return typed contracts.
1147
+
1148
+ ## Tests and Development Notes
1149
+
1150
+ The package includes targeted tests next to modules (`*.test.ts`), for example ORM, translation, routes, redirects, and populated relations.
1151
+
1152
+ Package build:
1153
+
1154
+ ```sh
1155
+ bun run build --workspace @rakun-kit/core
1156
+ ```
1157
+
1158
+ The script generates ESM and CJS in `dist/`, adjusting imports and the CJS `package.json` through repo scripts.