@cosmicdrift/kumiko-framework 0.404.0 → 0.404.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +103 -103
- package/README.md +19 -17
- package/agents/examples/apex-landing.md +3 -3
- package/agents/examples/apex-surface-auth.md +13 -9
- package/agents/examples/auth-mfa.md +2 -4
- package/agents/examples/boot-check.md +2 -2
- package/agents/examples/compliance-profiles-demo.md +5 -5
- package/agents/examples/compose-stacks.md +8 -8
- package/agents/examples/cross-feature-events.md +1 -0
- package/agents/examples/crypto-shredding-hr.md +3 -3
- package/agents/examples/custom-fields-basic.md +1 -1
- package/agents/examples/documents.md +28 -0
- package/agents/examples/encrypted-tenant-config.md +8 -8
- package/agents/examples/env-composition.md +10 -10
- package/agents/examples/error-contract.md +9 -9
- package/agents/examples/event-sourcing.md +9 -9
- package/agents/examples/ledger-invoicing.md +6 -6
- package/agents/examples/legal-pages.md +16 -16
- package/agents/examples/managed-config.md +15 -11
- package/agents/examples/managed-pages.md +7 -7
- package/agents/examples/pipeline-basics.md +13 -13
- package/agents/examples/public-share-token.md +4 -4
- package/agents/examples/security-baseline.md +4 -1
- package/agents/examples/session-revocation.md +1 -1
- package/agents/examples/tenant-settings.md +4 -4
- package/agents/examples/tier-admin.md +2 -2
- package/agents/examples/user-data-rights.md +21 -19
- package/dist/pipeline/event-retention.js +3 -1
- package/dist/search/meilisearch-peer.js +1 -1
- package/package.json +12 -12
- package/presets/biome-app.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -7,17 +7,17 @@ Next to it, under `agents/`, are generated files. They are rebuilt from the
|
|
|
7
7
|
framework source on every release, so they never describe an API that does
|
|
8
8
|
not exist in your version:
|
|
9
9
|
|
|
10
|
-
| File
|
|
11
|
-
|
|
|
12
|
-
| `agents/features-index.md`
|
|
13
|
-
| `agents/features.md`
|
|
14
|
-
| `agents/api.md`
|
|
15
|
-
| `agents/api/<entry>.md`
|
|
16
|
-
| `agents/api/types.md`
|
|
17
|
-
| `agents/widgets.md`
|
|
18
|
-
| `agents/boot-rules.md`
|
|
19
|
-
| `agents/examples.md`
|
|
20
|
-
| `agents/examples/<name>.md` | One tested sample app: README plus every source and test file
|
|
10
|
+
| File | What it holds |
|
|
11
|
+
| --------------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
12
|
+
| `agents/features-index.md` | One entry per bundled feature: purpose, factory import, required features, config keys without a default, secrets |
|
|
13
|
+
| `agents/features.md` | Every bundled feature in detail: purpose, config keys, secrets, dependencies, APIs |
|
|
14
|
+
| `agents/api.md` | Index of the API reference: one file per entry point an app imports |
|
|
15
|
+
| `agents/api/<entry>.md` | Signatures of one entry point; options and screen types as property lists |
|
|
16
|
+
| `agents/api/types.md` | Types the entry points use but do not export, such as nested option shapes |
|
|
17
|
+
| `agents/widgets.md` | Every value export of `@cosmicdrift/kumiko-renderer/web` with its short description |
|
|
18
|
+
| `agents/boot-rules.md` | Every boot rule by id: summary, fix and source file; a boot violation links its entry |
|
|
19
|
+
| `agents/examples.md` | Index of the worked examples, one per concept |
|
|
20
|
+
| `agents/examples/<name>.md` | One tested sample app: README plus every source and test file |
|
|
21
21
|
|
|
22
22
|
The paths are relative to this file. From an app root they live under
|
|
23
23
|
`node_modules/@cosmicdrift/kumiko-framework/`. For signatures and option
|
|
@@ -131,14 +131,14 @@ is mounted in `src/client.tsx`. Declarative screens need no client code.
|
|
|
131
131
|
Names are built from the feature name, a kind and an id. Use the QN wherever
|
|
132
132
|
another feature or a test refers to something.
|
|
133
133
|
|
|
134
|
-
| Thing
|
|
135
|
-
|
|
|
136
|
-
| Write handler
|
|
137
|
-
| Query handler
|
|
138
|
-
| Screen
|
|
139
|
-
| Nav entry
|
|
140
|
-
| Translation key
|
|
141
|
-
| Entity field label | `<feature>:entity:<entity>:field:<field>` | `tasks:entity:task:field:title`
|
|
134
|
+
| Thing | Pattern | Example from the scaffolded demo feature |
|
|
135
|
+
| ------------------ | ----------------------------------------- | ---------------------------------------- |
|
|
136
|
+
| Write handler | `<feature>:write:<entity>:<verb>` | `tasks:write:task:create` |
|
|
137
|
+
| Query handler | `<feature>:query:<entity>:<verb>` | `tasks:query:task:list` |
|
|
138
|
+
| Screen | `<feature>:screen:<id>` | `tasks:screen:task-list` |
|
|
139
|
+
| Nav entry | `<feature>:nav:<id>` | `tasks:nav:tasks` |
|
|
140
|
+
| Translation key | `<feature>:<path>` | `tasks:nav.tasks` |
|
|
141
|
+
| Entity field label | `<feature>:entity:<entity>:field:<field>` | `tasks:entity:task:field:title` |
|
|
142
142
|
|
|
143
143
|
Inside one feature, a screen id without prefix (`screen: "task-edit"`) refers
|
|
144
144
|
to a screen of the same feature.
|
|
@@ -147,20 +147,20 @@ to a screen of the same feature.
|
|
|
147
147
|
|
|
148
148
|
The scaffold (`bun create kumiko-app <name>`) produces this layout. Keep it.
|
|
149
149
|
|
|
150
|
-
| Path
|
|
151
|
-
|
|
|
152
|
-
| `kumiko.config.ts`
|
|
153
|
-
| `src/run-config.ts`
|
|
154
|
-
| `src/features/<name>/feature.ts`
|
|
155
|
-
| `src/features/<name>/index.ts`
|
|
156
|
-
| `src/features/<name>/web/index.ts`
|
|
157
|
-
| `src/client.tsx`
|
|
158
|
-
| `src/__tests__/*.test.ts`
|
|
159
|
-
| `src/__tests__/*.integration.test.ts`
|
|
160
|
-
| `e2e/*.spec.ts`
|
|
161
|
-
| `kumiko/schema.ts`, `kumiko/migrations/`
|
|
162
|
-
| `bin/server.ts`, `bin/main.ts`, `bin/env-schema.ts` | Dev server, production boot, env contract
|
|
163
|
-
| `e2e/_helpers/`, `src/__tests__/_setup.ts`
|
|
150
|
+
| Path | Content |
|
|
151
|
+
| --------------------------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
152
|
+
| `kumiko.config.ts` | `defineAppConfig({ features, prodFeatures?, client, ports })`: the one list the tooling reads |
|
|
153
|
+
| `src/run-config.ts` | `APP_FEATURES`, the mounted feature list |
|
|
154
|
+
| `src/features/<name>/feature.ts` | `defineFeature(...)` with entities, handlers, screens, navs |
|
|
155
|
+
| `src/features/<name>/index.ts` | Public exports of the feature |
|
|
156
|
+
| `src/features/<name>/web/index.ts` | Client feature definition |
|
|
157
|
+
| `src/client.tsx` | Web entry, mounts the client features |
|
|
158
|
+
| `src/__tests__/*.test.ts` | Unit tests, no services |
|
|
159
|
+
| `src/__tests__/*.integration.test.ts` | Integration tests against Postgres and Redis |
|
|
160
|
+
| `e2e/*.spec.ts` | Playwright tests against a booted app |
|
|
161
|
+
| `kumiko/schema.ts`, `kumiko/migrations/` | Generated schema shim over `kumiko.config.ts`, and the migrations |
|
|
162
|
+
| `bin/server.ts`, `bin/main.ts`, `bin/env-schema.ts` | Dev server, production boot, env contract |
|
|
163
|
+
| `e2e/_helpers/`, `src/__tests__/_setup.ts` | Shared e2e helpers and the shared integration stack setup |
|
|
164
164
|
|
|
165
165
|
Files whose first line reads `kumiko-managed` (deploy/, `.github/workflows/`,
|
|
166
166
|
`.husky/pre-push`, the bunfigs, `kumiko/schema.ts`) belong to the framework.
|
|
@@ -176,20 +176,20 @@ feature and test. Start from them.
|
|
|
176
176
|
|
|
177
177
|
## Commands in an app
|
|
178
178
|
|
|
179
|
-
| Goal
|
|
180
|
-
|
|
|
181
|
-
| Run the app with demo data
|
|
182
|
-
| Boot validation without serving
|
|
183
|
-
| Boot, guards, managed files and script standard
|
|
184
|
-
| Restore framework-owned files
|
|
185
|
-
| Types
|
|
186
|
-
| Lint
|
|
187
|
-
| Unit tests
|
|
188
|
-
| Integration tests (needs `docker compose up -d`)
|
|
189
|
-
| Browser tests
|
|
190
|
-
| New migration after an entity change or a newly mounted bundled feature | `bun run schema:generate <name>`
|
|
191
|
-
| Check migrations against the features, no database needed
|
|
192
|
-
| Apply migrations
|
|
179
|
+
| Goal | Command |
|
|
180
|
+
| ----------------------------------------------------------------------- | ---------------------------------- |
|
|
181
|
+
| Run the app with demo data | `bun dev` |
|
|
182
|
+
| Boot validation without serving | `bun run boot` |
|
|
183
|
+
| Boot, guards, managed files and script standard | `bun run check` |
|
|
184
|
+
| Restore framework-owned files | `bunx kumiko sync-managed --write` |
|
|
185
|
+
| Types | `bun run typecheck` |
|
|
186
|
+
| Lint | `bun run lint` |
|
|
187
|
+
| Unit tests | `bun run test` |
|
|
188
|
+
| Integration tests (needs `docker compose up -d`) | `bun run test:integration` |
|
|
189
|
+
| Browser tests | `bun run e2e` |
|
|
190
|
+
| New migration after an entity change or a newly mounted bundled feature | `bun run schema:generate <name>` |
|
|
191
|
+
| Check migrations against the features, no database needed | `bun kumiko-schema validate` |
|
|
192
|
+
| Apply migrations | `bun run schema:apply` |
|
|
193
193
|
|
|
194
194
|
`<name>` is required and becomes part of the file name
|
|
195
195
|
(`schema:generate add-invoice`). Mounting a bundled feature in
|
|
@@ -303,50 +303,50 @@ Before building anything here, search `agents/features-index.md`: auth, tenants,
|
|
|
303
303
|
mail, files, jobs, billing, audit, notifications and GDPR flows already exist
|
|
304
304
|
as bundled features.
|
|
305
305
|
|
|
306
|
-
| I need
|
|
307
|
-
|
|
|
308
|
-
| A CRUD entity with list and edit
|
|
309
|
-
| Business logic beyond CRUD
|
|
310
|
-
| Filtered lists and counts
|
|
311
|
-
| Key figures on a dashboard
|
|
312
|
-
| Relations between entities
|
|
313
|
-
| Structured sub-objects in a field
|
|
314
|
-
| List, edit, custom screens and navs
|
|
315
|
-
| A record page with header, metrics, tabs
|
|
316
|
-
| A multi-step form
|
|
317
|
-
| A form inside a drawer
|
|
318
|
-
| Role-based access
|
|
319
|
-
| Field-level access
|
|
320
|
-
| Row ownership
|
|
321
|
-
| Tenant separation
|
|
322
|
-
| Public or anonymous endpoints
|
|
323
|
-
| Allowed state changes
|
|
324
|
-
| React to a write in another feature
|
|
325
|
-
| Projections, event versions, history
|
|
326
|
-
| Before or after save logic
|
|
327
|
-
| Typed errors with reasons
|
|
328
|
-
| Translations
|
|
329
|
-
| Config per tenant or system
|
|
330
|
-
| Tenant defaults (currency, locale)
|
|
331
|
-
| Secrets such as API keys
|
|
332
|
-
| Personal data and erasure
|
|
333
|
-
| GDPR export and forget for my data
|
|
334
|
-
| Full-text search
|
|
335
|
-
| Live updates in the browser
|
|
336
|
-
| Feature toggles
|
|
337
|
-
| Rate limits
|
|
338
|
-
| Idempotent writes
|
|
339
|
-
| File variants and images
|
|
340
|
-
| Money fields
|
|
341
|
-
| Dates, times, time zones
|
|
342
|
-
| Tags or notes on any entity
|
|
343
|
-
| Tenant-defined extra fields
|
|
344
|
-
| Lookup tables and seeds
|
|
345
|
-
| Multi-step background flows
|
|
346
|
-
| A mount invariant for my feature
|
|
347
|
-
| A widget for a custom screen
|
|
348
|
-
| A bundled feature and its config
|
|
349
|
-
| The options of a function, screen or field | Search by name
|
|
306
|
+
| I need | Use | Read |
|
|
307
|
+
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------- |
|
|
308
|
+
| A CRUD entity with list and edit | `createEntity` + `r.crud` + `entityList` / `entityEdit` screens | `agents/examples/basic-entity.md` |
|
|
309
|
+
| Business logic beyond CRUD | `r.writeHandler` / `r.queryHandler` with `createEntityExecutor` | `agents/examples/custom-handlers.md` |
|
|
310
|
+
| Filtered lists and counts | Executor `list` with `filter` and `totalCount` | `agents/examples/custom-handlers.md` |
|
|
311
|
+
| Key figures on a dashboard | `dashboard` screen with `stat` / `stat-group` panels and a query handler | `agents/examples/dashboard-stats.md` |
|
|
312
|
+
| Relations between entities | `createReferenceField({ entity })` for the foreign key (user ids: `entity: "user:user"`), `r.relation(entity, name, { type, target, foreignKey, onDelete })` | `agents/examples/relations.md` |
|
|
313
|
+
| Structured sub-objects in a field | Embedded fields | `agents/examples/embedded.md` |
|
|
314
|
+
| List, edit, custom screens and navs | `r.screen`, `r.nav` | `agents/examples/screens-nav.md` |
|
|
315
|
+
| A record page with header, metrics, tabs | `projectionDetail` with `header`, `metrics`, `layout.mode: "tabs"` | `agents/examples/record-detail-layout.md` |
|
|
316
|
+
| A multi-step form | `layout.mode: "wizard"` | `agents/examples/wizard-form.md` |
|
|
317
|
+
| A form inside a drawer | `RenderEdit` in a `Drawer` | `agents/examples/embedded-entity-form.md` |
|
|
318
|
+
| Role-based access | `access: { roles }`, `openToAll` | `agents/examples/access-control.md` |
|
|
319
|
+
| Field-level access | Field access rules | `agents/examples/field-access.md` |
|
|
320
|
+
| Row ownership | Ownership rules | `agents/examples/ownership.md` |
|
|
321
|
+
| Tenant separation | Built in; see what it guarantees | `agents/examples/tenant-isolation.md` |
|
|
322
|
+
| Public or anonymous endpoints | `anonymous` role | `agents/examples/anonymous-access.md` |
|
|
323
|
+
| Allowed state changes | Entity transitions | `agents/examples/state-machine.md` |
|
|
324
|
+
| React to a write in another feature | `r.defineEvent` + `emitEvent` + `r.multiStreamProjection` | `agents/examples/cross-feature-events.md` |
|
|
325
|
+
| Projections, event versions, history | `r.projection`, `ctx.events.load`, `ctx.dispatch.queryProjection` | `agents/examples/event-sourcing.md` |
|
|
326
|
+
| Before or after save logic | Lifecycle hooks | `agents/examples/lifecycle-hooks.md` |
|
|
327
|
+
| Typed errors with reasons | `@cosmicdrift/kumiko-framework/errors` | `agents/examples/error-contract.md` |
|
|
328
|
+
| Translations | `r.translations` + client translations | `agents/examples/i18n.md` |
|
|
329
|
+
| Config per tenant or system | Managed config | `agents/examples/managed-config.md` |
|
|
330
|
+
| Tenant defaults (currency, locale) | Tenant settings | `agents/examples/tenant-settings.md` |
|
|
331
|
+
| Secrets such as API keys | Secrets with envelope encryption | `agents/examples/secrets-demo.md` |
|
|
332
|
+
| Personal data and erasure | `personal` / `find` annotations, crypto shredding | `agents/examples/crypto-shredding-hr.md` |
|
|
333
|
+
| GDPR export and forget for my data | `user-data-rights` extension | `agents/examples/user-data-rights.md` |
|
|
334
|
+
| Full-text search | Search feature | `agents/examples/search.md` |
|
|
335
|
+
| Live updates in the browser | Server-sent events, `useQuery({ live: true })` | `agents/examples/realtime-sse.md` |
|
|
336
|
+
| Feature toggles | Toggleable features | `agents/examples/feature-toggles.md` |
|
|
337
|
+
| Rate limits | Rate limiting | `agents/examples/rate-limiting.md` |
|
|
338
|
+
| Idempotent writes | Request id dedupe | `agents/examples/idempotency.md` |
|
|
339
|
+
| File variants and images | File fields, derivatives | `agents/examples/files-post-processing.md` |
|
|
340
|
+
| Money fields | Currency fields | `agents/examples/currencies-per-tenant.md` |
|
|
341
|
+
| Dates, times, time zones | Time-aware field types, `ctx.tz` | `agents/examples/timezones.md` |
|
|
342
|
+
| Tags or notes on any entity | Tags, notes history | `agents/examples/tags-basic.md`, `agents/examples/notes-history-basic.md` |
|
|
343
|
+
| Tenant-defined extra fields | Custom fields | `agents/examples/custom-fields-basic.md` |
|
|
344
|
+
| Lookup tables and seeds | Upsert seeds | `agents/examples/reference-data.md` |
|
|
345
|
+
| Multi-step background flows | Workflow engine | `agents/examples/workflow-engine.md` |
|
|
346
|
+
| A mount invariant for my feature | `r.bootCheck` | `agents/examples/boot-check.md` |
|
|
347
|
+
| A widget for a custom screen | Search by concern | `agents/widgets.md` |
|
|
348
|
+
| A bundled feature and its config | Search by name or purpose | `agents/features-index.md`, then `agents/features.md` |
|
|
349
|
+
| The options of a function, screen or field | Search by name | `agents/api.md` |
|
|
350
350
|
|
|
351
351
|
## Reading an example
|
|
352
352
|
|
|
@@ -367,15 +367,15 @@ Import only from these entry points. Deep imports into `src/` or `dist/`
|
|
|
367
367
|
break on the next release. Each one has its signatures in the file named in
|
|
368
368
|
`agents/api.md`.
|
|
369
369
|
|
|
370
|
-
| Entry point
|
|
371
|
-
|
|
|
372
|
-
| `@cosmicdrift/kumiko-framework/engine`
|
|
373
|
-
| `@cosmicdrift/kumiko-framework/errors`
|
|
374
|
-
| `@cosmicdrift/kumiko-framework/ui-types`
|
|
375
|
-
| `@cosmicdrift/kumiko-framework/db`
|
|
376
|
-
| `@cosmicdrift/kumiko-framework/event-store` | Event store helpers
|
|
377
|
-
| `@cosmicdrift/kumiko-framework/seeding`
|
|
378
|
-
| `@cosmicdrift/kumiko-renderer/web`
|
|
379
|
-
| `@cosmicdrift/kumiko-dev-server/testing`
|
|
380
|
-
| `@cosmicdrift/kumiko-testing/stack`
|
|
381
|
-
| `@cosmicdrift/kumiko-testing/helpers`
|
|
370
|
+
| Entry point | Use it for |
|
|
371
|
+
| ------------------------------------------- | ------------------------------------------------------------------------------------ |
|
|
372
|
+
| `@cosmicdrift/kumiko-framework/engine` | `defineFeature`, `createEntity`, field factories, handler helpers, executors, events |
|
|
373
|
+
| `@cosmicdrift/kumiko-framework/errors` | Typed failures and error helpers |
|
|
374
|
+
| `@cosmicdrift/kumiko-framework/ui-types` | Screen definition types (`EntityListScreenDefinition`, ...) |
|
|
375
|
+
| `@cosmicdrift/kumiko-framework/db` | `buildEntityTable` and database helpers for tests and migrations |
|
|
376
|
+
| `@cosmicdrift/kumiko-framework/event-store` | Event store helpers |
|
|
377
|
+
| `@cosmicdrift/kumiko-framework/seeding` | `TestUsers`, `createTestUser`, seed helpers |
|
|
378
|
+
| `@cosmicdrift/kumiko-renderer/web` | Widgets, hooks and the web app shell |
|
|
379
|
+
| `@cosmicdrift/kumiko-dev-server/testing` | `setupAppTestStack`, `seedTenant` for app integration tests |
|
|
380
|
+
| `@cosmicdrift/kumiko-testing/stack` | `setupTestStack`, the stack the samples use |
|
|
381
|
+
| `@cosmicdrift/kumiko-testing/helpers` | Assertion helpers such as `expectErrorIncludes` |
|
package/README.md
CHANGED
|
@@ -47,7 +47,9 @@ export const taskFeature = defineFeature("tasks", (r) => {
|
|
|
47
47
|
write: { access: { roles: ["User"] } },
|
|
48
48
|
read: {
|
|
49
49
|
access: {
|
|
50
|
-
openToAll: {
|
|
50
|
+
openToAll: {
|
|
51
|
+
reason: "any signed-in user may list and view tasks; only writes are role-gated",
|
|
52
|
+
},
|
|
51
53
|
},
|
|
52
54
|
},
|
|
53
55
|
});
|
|
@@ -80,22 +82,22 @@ export const taskFeature = defineFeature("tasks", (r) => {
|
|
|
80
82
|
|
|
81
83
|
## Package exports
|
|
82
84
|
|
|
83
|
-
| Entry
|
|
84
|
-
|
|
85
|
-
| `@cosmicdrift/kumiko-framework/engine`
|
|
86
|
-
| `@cosmicdrift/kumiko-framework/db`
|
|
87
|
-
| `@cosmicdrift/kumiko-framework/event-store` | `events` table, `append`, `loadAggregate`, `loadAggregateAsOf`
|
|
88
|
-
| `@cosmicdrift/kumiko-framework/pipeline`
|
|
89
|
-
| `@cosmicdrift/kumiko-framework/api`
|
|
90
|
-
| `@cosmicdrift/kumiko-framework/auth`
|
|
91
|
-
| `@cosmicdrift/kumiko-framework/search`
|
|
92
|
-
| `@cosmicdrift/kumiko-framework/jobs`
|
|
93
|
-
| `@cosmicdrift/kumiko-framework/files`
|
|
94
|
-
| `@cosmicdrift/kumiko-framework/i18n`
|
|
95
|
-
| `@cosmicdrift/kumiko-framework/ui`
|
|
96
|
-
| `@cosmicdrift/kumiko-testing/stack`
|
|
97
|
-
| `@cosmicdrift/kumiko-framework/utils`
|
|
98
|
-
| `@cosmicdrift/kumiko-framework/errors`
|
|
85
|
+
| Entry | What's in it |
|
|
86
|
+
| ------------------------------------------- | -------------------------------------------------------------------------------------- |
|
|
87
|
+
| `@cosmicdrift/kumiko-framework/engine` | `defineFeature`, `createEntity`, field helpers, access rules, registry |
|
|
88
|
+
| `@cosmicdrift/kumiko-framework/db` | `deriveEntityTableMeta`, `createEventStoreExecutor`, migrations, tenant-db |
|
|
89
|
+
| `@cosmicdrift/kumiko-framework/event-store` | `events` table, `append`, `loadAggregate`, `loadAggregateAsOf` |
|
|
90
|
+
| `@cosmicdrift/kumiko-framework/pipeline` | Dispatcher, event-dispatcher (AsyncDaemon), projection-rebuild, SSE + search consumers |
|
|
91
|
+
| `@cosmicdrift/kumiko-framework/api` | `buildServer`, auth middleware, SSE route, error contract |
|
|
92
|
+
| `@cosmicdrift/kumiko-framework/auth` | JWT helper, password hashing, session users |
|
|
93
|
+
| `@cosmicdrift/kumiko-framework/search` | `SearchAdapter` interface, in-memory adapter, Meili wrapper |
|
|
94
|
+
| `@cosmicdrift/kumiko-framework/jobs` | BullMQ-backed job runner, cron scheduling |
|
|
95
|
+
| `@cosmicdrift/kumiko-framework/files` | Signed-URL upload/download, tenant-scoped storage |
|
|
96
|
+
| `@cosmicdrift/kumiko-framework/i18n` | i18next setup, per-feature translation registration |
|
|
97
|
+
| `@cosmicdrift/kumiko-framework/ui` | React hooks (Zustand stores, SSE subscription, optimistic mutations) |
|
|
98
|
+
| `@cosmicdrift/kumiko-testing/stack` | `setupTestStack`, `createTestDb`, request helpers |
|
|
99
|
+
| `@cosmicdrift/kumiko-framework/utils` | Safe JSON, qualified-name helpers |
|
|
100
|
+
| `@cosmicdrift/kumiko-framework/errors` | Error classes, `writeFailure`, reason contracts |
|
|
99
101
|
|
|
100
102
|
## Core concepts
|
|
101
103
|
|
|
@@ -18,9 +18,9 @@ spec also paints a product-board mock (`hero-app.png`), uses it in the hero
|
|
|
18
18
|
|
|
19
19
|
`buildLandingPage`'s `head.schemaJson` combines `organizationSchema` +
|
|
20
20
|
`webPageSchema` (from the `seo` feature) into one `@graph` block — Organization
|
|
21
|
-
|
|
22
|
-
`renderApexPage` already knows how to emit. This is the seam an app extends
|
|
23
|
-
with `faqPageSchema` for an FAQ section, or its own custom schema.org type.
|
|
21
|
+
- WebPage nodes in a single `<script type="application/ld+json">`, which
|
|
22
|
+
`renderApexPage` already knows how to emit. This is the seam an app extends
|
|
23
|
+
with `faqPageSchema` for an FAQ section, or its own custom schema.org type.
|
|
24
24
|
|
|
25
25
|
## Site discovery: sitemap.xml / llms.txt
|
|
26
26
|
|
|
@@ -36,11 +36,15 @@ A typical app has two mounts sharing locale and primitives:
|
|
|
36
36
|
|
|
37
37
|
```tsx illustration
|
|
38
38
|
import {
|
|
39
|
-
ForgotPasswordScreen,
|
|
40
|
-
|
|
39
|
+
ForgotPasswordScreen,
|
|
40
|
+
SignupScreen,
|
|
41
|
+
createLoginRoute,
|
|
42
|
+
AuthShellProvider,
|
|
43
|
+
emailPasswordClient,
|
|
41
44
|
} from "@cosmicdrift/kumiko-bundled-features/auth-email-password/web";
|
|
42
45
|
import {
|
|
43
|
-
RequestAccountDeletionScreen,
|
|
46
|
+
RequestAccountDeletionScreen,
|
|
47
|
+
ConfirmAccountDeletionScreen,
|
|
44
48
|
} from "@cosmicdrift/kumiko-bundled-features/user-data-rights/web";
|
|
45
49
|
import { createPublicSurface } from "@cosmicdrift/kumiko-renderer/web";
|
|
46
50
|
|
|
@@ -51,7 +55,7 @@ import { createPublicSurface } from "@cosmicdrift/kumiko-renderer/web";
|
|
|
51
55
|
const LoginRoute = createLoginRoute({ loginScreenProps: { signupHref: "/signup" } });
|
|
52
56
|
|
|
53
57
|
createPublicSurface({
|
|
54
|
-
clientFeatures: [emailPasswordClient()],
|
|
58
|
+
clientFeatures: [emailPasswordClient()], // SessionProvider + i18n
|
|
55
59
|
shell: ({ children }) => (
|
|
56
60
|
<MarketingChrome>
|
|
57
61
|
<AuthShellProvider shell={(card) => <div className="py-12 flex justify-center">{card}</div>}>
|
|
@@ -60,10 +64,10 @@ createPublicSurface({
|
|
|
60
64
|
</MarketingChrome>
|
|
61
65
|
),
|
|
62
66
|
routes: [
|
|
63
|
-
{ path: "/login",
|
|
64
|
-
{ path: "/signup",
|
|
67
|
+
{ path: "/login", component: <LoginRoute /> },
|
|
68
|
+
{ path: "/signup", component: <SignupScreen loginHref="/login" /> },
|
|
65
69
|
{ path: "/forgot-password", component: <ForgotPasswordScreen loginHref="/login" /> },
|
|
66
|
-
{ path: "/delete-account",
|
|
70
|
+
{ path: "/delete-account", component: <RequestAccountDeletionScreen /> },
|
|
67
71
|
{ path: "/delete-account/confirm", component: <ConfirmAccountDeletionScreen /> },
|
|
68
72
|
],
|
|
69
73
|
fallback: <LoginRoute />,
|
|
@@ -98,8 +102,8 @@ runProdApp({
|
|
|
98
102
|
|
|
99
103
|
GDPR Art. 17 applies when the user **cannot log in** (lockout). The flow is
|
|
100
104
|
email-verified (magic link), not login-gated. The HMAC token carries `userId`
|
|
101
|
-
|
|
102
|
-
token endpoints. Second confirm is idempotent (`user_not_in_active_state`).
|
|
105
|
+
- expiry (no DB table); purpose `"deletion-request"` blocks replay on other
|
|
106
|
+
token endpoints. Second confirm is idempotent (`user_not_in_active_state`).
|
|
103
107
|
|
|
104
108
|
## Tests
|
|
105
109
|
|
|
@@ -30,9 +30,7 @@ const mfaFeature = createAuthMfaFeature({
|
|
|
30
30
|
issuer: "Your App",
|
|
31
31
|
});
|
|
32
32
|
const sessionCallbacks = createSessionCallbacks({ db });
|
|
33
|
-
bindMfaRevokeAllOtherSessionsFromFeature(mfaFeature)?.(
|
|
34
|
-
sessionCallbacks.sessionRevokeAllOthers,
|
|
35
|
-
);
|
|
33
|
+
bindMfaRevokeAllOtherSessionsFromFeature(mfaFeature)?.(sessionCallbacks.sessionRevokeAllOthers);
|
|
36
34
|
|
|
37
35
|
const server = buildServer({
|
|
38
36
|
registry: buildRegistry([
|
|
@@ -45,7 +43,7 @@ const server = buildServer({
|
|
|
45
43
|
mfaFeature,
|
|
46
44
|
// ... other features
|
|
47
45
|
]),
|
|
48
|
-
context: { db
|
|
46
|
+
context: { db /* ... */ },
|
|
49
47
|
jwtSecret: process.env.JWT_SECRET,
|
|
50
48
|
auth: {
|
|
51
49
|
membershipQuery: "tenant:query:memberships",
|
|
@@ -20,7 +20,7 @@ never mounted, and nothing caught it — until now.
|
|
|
20
20
|
ctx exposing every mounted feature. Throw to fail the boot with a clear,
|
|
21
21
|
feature-prefixed message.
|
|
22
22
|
- **Conditional invariants `r.requires` can't express** — the check only
|
|
23
|
-
fails when
|
|
23
|
+
fails when _this_ feature's own shape demands it (has a PII field), not
|
|
24
24
|
unconditionally. A plain `r.requires({ features: ["user-data-hook"] })` would fail even
|
|
25
25
|
for a prompt-store variant with no PII fields at all.
|
|
26
26
|
- **`validateBoot`** — the framework's boot-time validator, run directly
|
|
@@ -46,7 +46,7 @@ user-data-hook → companion feature the check requires when PII is present
|
|
|
46
46
|
|
|
47
47
|
## When to reach for it
|
|
48
48
|
|
|
49
|
-
You're writing a feature whose validity depends on
|
|
49
|
+
You're writing a feature whose validity depends on _another_ feature being
|
|
50
50
|
mounted, but only under a condition your feature alone can evaluate (e.g.
|
|
51
51
|
"only if I have PII fields", "only if I expose more than N screens"). If the
|
|
52
52
|
requirement is unconditional, `r.requires`/`r.optionalRequires` is simpler
|
|
@@ -16,11 +16,11 @@ branches in your domain handlers.
|
|
|
16
16
|
How a multi-tenant platform uses `compliance-profiles` to deliver different
|
|
17
17
|
regulatory behaviour per tenant:
|
|
18
18
|
|
|
19
|
-
| Tenant
|
|
20
|
-
|
|
21
|
-
| **A** (DACH)
|
|
22
|
-
| **B** (Switzerland) | `swiss-dsg`
|
|
23
|
-
| **C** (DACH-HR)
|
|
19
|
+
| Tenant | Profile | Supervisory authority | Languages | Tenant-destroy grace |
|
|
20
|
+
| ------------------- | ----------------- | --------------------- | ----------------- | --------------------- |
|
|
21
|
+
| **A** (DACH) | `eu-dsgvo` | BlnBDI Berlin | de / en | 30 days |
|
|
22
|
+
| **B** (Switzerland) | `swiss-dsg` | EDÖB Bern | de / fr / it / en | 30 days |
|
|
23
|
+
| **C** (DACH-HR) | `de-hr-dsgvo-hgb` | State DPA | de | 60 days (HR override) |
|
|
24
24
|
|
|
25
25
|
Plus tenant B with an override (`gracePeriod: { days: 90 }`) extending
|
|
26
26
|
user-rights grace without losing other profile fields — deep-merge on the
|
|
@@ -36,14 +36,14 @@ the option.
|
|
|
36
36
|
|
|
37
37
|
## Boot requirements
|
|
38
38
|
|
|
39
|
-
| Stack
|
|
40
|
-
|
|
|
41
|
-
| `composeMailStack`
|
|
42
|
-
| `composeFileStack` without providers
|
|
43
|
-
| `composeGdprStack`
|
|
44
|
-
| `composeGdprStack({ selfService })`, `composeUserDataRightsStack` | `mail`
|
|
45
|
-
| `composeSecurityStack`
|
|
46
|
-
| `composeSaasStack({ billing: "stripe" })`
|
|
39
|
+
| Stack | Boot option | Also satisfied by |
|
|
40
|
+
| ----------------------------------------------------------------- | ---------------------------------- | ------------------------------- |
|
|
41
|
+
| `composeMailStack` | `mail` | a mounted `channel-email` |
|
|
42
|
+
| `composeFileStack` without providers | `fileStorage` | a mounted file provider |
|
|
43
|
+
| `composeGdprStack` | `kmsMode` | `kms` or `allowPlaintextPii` |
|
|
44
|
+
| `composeGdprStack({ selfService })`, `composeUserDataRightsStack` | `mail` | a mounted `channel-email` |
|
|
45
|
+
| `composeSecurityStack` | `kmsMode`, `rateLimit` not `false` | `kms` or `allowPlaintextPii` |
|
|
46
|
+
| `composeSaasStack({ billing: "stripe" })` | `billing` (warning only) | a mounted subscription provider |
|
|
47
47
|
|
|
48
48
|
A missing option fails the boot with:
|
|
49
49
|
|
|
@@ -76,6 +76,7 @@ The integration test pins three guarantees:
|
|
|
76
76
|
```bash
|
|
77
77
|
bun kumiko test integration samples/cross-feature-events
|
|
78
78
|
```
|
|
79
|
+
|
|
79
80
|
4. **Catch-up job** — `catch-up-job.integration.test.ts` places orders
|
|
80
81
|
before the first dispatcher pass and checks that the `catchUp` job
|
|
81
82
|
invoices each of them exactly once.
|
|
@@ -15,7 +15,7 @@ unreadable forever, and reads render the `[[erased]]` sentinel.
|
|
|
15
15
|
|
|
16
16
|
The recipe ships a mini HR feature: an `employee` entity whose name and email
|
|
17
17
|
are the employee's own PII, and an `hr-comment` entity whose body is
|
|
18
|
-
encrypted under the key of the employee the comment is
|
|
18
|
+
encrypted under the key of the employee the comment is _about_ — so a
|
|
19
19
|
manager's comment dies with the employee's key, not the manager's.
|
|
20
20
|
|
|
21
21
|
## What it shows
|
|
@@ -49,7 +49,7 @@ manager's comment dies with the employee's key, not the manager's.
|
|
|
49
49
|
than an alphabetically paginated one — not a plaintext sort column beside
|
|
50
50
|
the encrypted one, which would survive the key erase and quietly undo the
|
|
51
51
|
shredding.
|
|
52
|
-
- **Forget = `kms.eraseKey(subject)`** — afterwards detail
|
|
52
|
+
- **Forget = `kms.eraseKey(subject)`** — afterwards detail _and_ list render
|
|
53
53
|
`[[erased]]` for every protected field while the stored ciphertext bytes
|
|
54
54
|
stay untouched.
|
|
55
55
|
|
|
@@ -70,7 +70,7 @@ deletion grace period.
|
|
|
70
70
|
|
|
71
71
|
1. Create an employee → the executor creates a subject key on first insert
|
|
72
72
|
and stores `displayName`/`email` as ciphertext.
|
|
73
|
-
2. A comment about the employee is encrypted under the
|
|
73
|
+
2. A comment about the employee is encrypted under the _employee's_ key via
|
|
74
74
|
`userOwned: { ownerField: "employeeId" }`.
|
|
75
75
|
3. `kms.eraseKey({ kind: "user", userId })` — idempotent, tombstone stays
|
|
76
76
|
for the audit trail.
|
|
@@ -11,7 +11,7 @@ Tenant-defined extra fields on an entity without writing migrations.
|
|
|
11
11
|
Let a tenant add their own fields to your entity at runtime — without
|
|
12
12
|
writing a migration, without rebuilding your handler, without a code
|
|
13
13
|
change. The recipe wires a `property` entity into the `custom-fields`
|
|
14
|
-
bundle and drives a full
|
|
14
|
+
bundle and drives a full _define → set → read_ roundtrip.
|
|
15
15
|
|
|
16
16
|
The result: the tenant defines `internalNumber: text`, sets it on a
|
|
17
17
|
property, and reads it back flattened onto the row — looking exactly
|
|
@@ -4,6 +4,34 @@
|
|
|
4
4
|
|
|
5
5
|
A document register on top of existing records: captured files, expiry dates and per-record visibility.
|
|
6
6
|
|
|
7
|
+
## README
|
|
8
|
+
|
|
9
|
+
# Documents
|
|
10
|
+
|
|
11
|
+
A document register on top of existing records: captured files with a category, linked to records, visible only to whoever may read those records.
|
|
12
|
+
|
|
13
|
+
## What it shows
|
|
14
|
+
|
|
15
|
+
- `createDocumentsFeature` (`@cosmicdrift/kumiko-bundled-features/documents`) with `categories`, `linkTargets` and `access` roles.
|
|
16
|
+
- A `capture` rule turns a file field into a document source without the entity knowing about the documents feature: every saved `contract.file` is filed as a document of category `contract`, titled from the row, and linked to the contract's property.
|
|
17
|
+
- Visibility follows the linked records: a document is visible to users who may read the linked records and the contract that owns the file. The `documents:query:for-record` query returns the documents of one record; a property reader sees none for the property because they may not read the contract that owns the file.
|
|
18
|
+
|
|
19
|
+
## Source
|
|
20
|
+
|
|
21
|
+
Feature entry point: `src/feature.ts`.
|
|
22
|
+
|
|
23
|
+
```ts file=<rootDir>/_samples/recipes-documents/src/feature.ts
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Tests
|
|
27
|
+
|
|
28
|
+
Needs a running Postgres and `TEST_DATABASE_URL` set (e.g. `postgres://postgres:postgres@127.0.0.1:5432/postgres`, see `demo/.env.example`).
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
cd samples/recipes/documents
|
|
32
|
+
bun test
|
|
33
|
+
```
|
|
34
|
+
|
|
7
35
|
## Source files
|
|
8
36
|
|
|
9
37
|
### src/__tests__/feature.integration.test.ts
|
|
@@ -18,10 +18,10 @@ the platform operator being able to read the plaintext from the DB.
|
|
|
18
18
|
import { access, createTenantConfig } from "@cosmicdrift/kumiko-framework/engine";
|
|
19
19
|
|
|
20
20
|
createTenantConfig("text", {
|
|
21
|
-
encrypted: true,
|
|
22
|
-
write: access.admin,
|
|
23
|
-
read: access.admin,
|
|
24
|
-
mask: { title: "billing.stripe-api-key", order: 1 },
|
|
21
|
+
encrypted: true, // ← ciphertext in the DB
|
|
22
|
+
write: access.admin, // ← the tenant admin writes their own
|
|
23
|
+
read: access.admin, // ← nobody else sees it
|
|
24
|
+
mask: { title: "billing.stripe-api-key", order: 1 }, // ← derives the edit screen + nav
|
|
25
25
|
});
|
|
26
26
|
```
|
|
27
27
|
|
|
@@ -47,10 +47,10 @@ i18n key of the field label, `mask.order` its position.
|
|
|
47
47
|
|
|
48
48
|
## Vs. `r.secret` / `samples/recipes/secrets-demo`
|
|
49
49
|
|
|
50
|
-
| Pattern
|
|
51
|
-
|
|
52
|
-
| **`r.secret`** (envelope encryption, KEK rotation) | App-global | Platform-owned secrets (e.g. the master Stripe key that bills ALL customers).
|
|
53
|
-
| **`encrypted: true` config key** (this recipe)
|
|
50
|
+
| Pattern | Scope | Use case |
|
|
51
|
+
| -------------------------------------------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
|
52
|
+
| **`r.secret`** (envelope encryption, KEK rotation) | App-global | Platform-owned secrets (e.g. the master Stripe key that bills ALL customers). |
|
|
53
|
+
| **`encrypted: true` config key** (this recipe) | Per-tenant | Customer-owned secrets. Customer sets, rotates, and never sees another customer's. Same envelope encryption + KEK keyring under the hood. |
|
|
54
54
|
|
|
55
55
|
Combinable: the platform uses `r.secret` for its internal secrets,
|
|
56
56
|
and in parallel customers have their tenant-owned `encrypted: true`
|
|
@@ -29,16 +29,16 @@ instead of keeping a hand-written copy of every feature's variables.
|
|
|
29
29
|
|
|
30
30
|
## Blocks
|
|
31
31
|
|
|
32
|
-
| Block
|
|
33
|
-
|
|
|
34
|
-
| `kmsEnvSchema`
|
|
35
|
-
| `smtpEnvSchema`
|
|
36
|
-
| `s3EnvSchema`, `createS3EnvSchema({ prefix })` | `file-provider-s3`
|
|
37
|
-
| `stripeEnvSchema({ prices })`
|
|
38
|
-
| `paddleEnvSchema({ prices })`
|
|
39
|
-
| `mfaTokenSecretsEnvSchema`
|
|
40
|
-
| `buildInfoEnvSchema`, `demoAdminEnvSchema`
|
|
41
|
-
| `prometheusMetricsEnvSchema`
|
|
32
|
+
| Block | Where it lives | Registered by |
|
|
33
|
+
| ---------------------------------------------- | ------------------------- | ----------------------------------------------------- |
|
|
34
|
+
| `kmsEnvSchema` | `crypto-shredding` | `createCryptoShreddingFeature()` |
|
|
35
|
+
| `smtpEnvSchema` | `channel-email` | `createChannelEmailFeature()` |
|
|
36
|
+
| `s3EnvSchema`, `createS3EnvSchema({ prefix })` | `file-provider-s3` | `createFileProviderS3Feature({ fromEnv: true })` |
|
|
37
|
+
| `stripeEnvSchema({ prices })` | `subscription-stripe` | app (`extend`); the feature registers the bridge keys |
|
|
38
|
+
| `paddleEnvSchema({ prices })` | `subscription-paddle` | app (`extend`); the feature registers the bridge keys |
|
|
39
|
+
| `mfaTokenSecretsEnvSchema` | `auth-mfa` | `createAuthMfaFeature()` |
|
|
40
|
+
| `buildInfoEnvSchema`, `demoAdminEnvSchema` | `foundation-shared` | app (`extend`) |
|
|
41
|
+
| `prometheusMetricsEnvSchema` | `framework/observability` | app (`extend`) |
|
|
42
42
|
|
|
43
43
|
## When to reach for it
|
|
44
44
|
|
|
@@ -22,15 +22,15 @@ Every Kumiko error class in a real handler context. A single feature `orders-lit
|
|
|
22
22
|
|
|
23
23
|
## The classes — when do I use which?
|
|
24
24
|
|
|
25
|
-
| Class
|
|
26
|
-
|
|
27
|
-
| `ValidationError`
|
|
28
|
-
| `AccessDeniedError`
|
|
29
|
-
| `NotFoundError`
|
|
30
|
-
| `ConflictError`
|
|
31
|
-
| `VersionConflictError` | 409
|
|
32
|
-
| `UnprocessableError`
|
|
33
|
-
| `InternalError`
|
|
25
|
+
| Class | HTTP | Use in handler |
|
|
26
|
+
| ---------------------- | ---- | ------------------------------------------------------------------------------------- |
|
|
27
|
+
| `ValidationError` | 400 | Automatic from Zod. Never throw manually, only for validation-hook errors. |
|
|
28
|
+
| `AccessDeniedError` | 403 | "You're not allowed" — ownership, role check, field lock. |
|
|
29
|
+
| `NotFoundError` | 404 | Entity doesn't exist. Automatic via `failNotFound(entity, id)`. |
|
|
30
|
+
| `ConflictError` | 409 | State collision without a version (e.g. "paid orders can't be cancelled"). |
|
|
31
|
+
| `VersionConflictError` | 409 | Optimistic lock — comes out of CrudExecutor automatically. You never throw it. |
|
|
32
|
+
| `UnprocessableError` | 422 | Business rule violated. The reason string describes what. |
|
|
33
|
+
| `InternalError` | 500 | You **don't throw it yourself**. The framework wraps unexpected throws automatically. |
|
|
34
34
|
|
|
35
35
|
## Convenience helpers
|
|
36
36
|
|
|
@@ -14,15 +14,15 @@ blocks Kumiko provides.
|
|
|
14
14
|
|
|
15
15
|
## What the sample shows
|
|
16
16
|
|
|
17
|
-
| API
|
|
18
|
-
|
|
19
|
-
| `r.defineEvent(name, schema, { version, migrations })`
|
|
20
|
-
| `ctx.events.append({ aggregateId, aggregateType, type, payload })` | Domain event on aggregate stream
|
|
21
|
-
| `r.projection(...)`
|
|
22
|
-
| `r.multiStreamProjection(...)`
|
|
23
|
-
| `ctx.events.load(id, { asOf })`
|
|
24
|
-
| `ctx.events.archive(id, { aggregateType })`
|
|
25
|
-
| `ctx.dispatch.queryProjection(name)`
|
|
17
|
+
| API | Purpose |
|
|
18
|
+
| ------------------------------------------------------------------ | ------------------------------------------------- |
|
|
19
|
+
| `r.defineEvent(name, schema, { version, migrations })` | Event schema with version number + upcaster chain |
|
|
20
|
+
| `ctx.events.append({ aggregateId, aggregateType, type, payload })` | Domain event on aggregate stream |
|
|
21
|
+
| `r.projection(...)` | Single-stream read model, inline in the write TX |
|
|
22
|
+
| `r.multiStreamProjection(...)` | Cross-aggregate read model, async via dispatcher |
|
|
23
|
+
| `ctx.events.load(id, { asOf })` | Live aggregation with point-in-time |
|
|
24
|
+
| `ctx.events.archive(id, { aggregateType })` | Archive a stream (Marten `ArchiveStream`) |
|
|
25
|
+
| `ctx.dispatch.queryProjection(name)` | Tenant-scoped read-model query |
|
|
26
26
|
|
|
27
27
|
## Domain
|
|
28
28
|
|
|
@@ -46,12 +46,12 @@ invoicingFlow …`):
|
|
|
46
46
|
Both recipes use the same primitive — accounts + balanced transactions — but
|
|
47
47
|
record different things:
|
|
48
48
|
|
|
49
|
-
|
|
|
50
|
-
|
|
51
|
-
| Accounts
|
|
52
|
-
| A transaction is…
|
|
53
|
-
| Revenue recognised | n/a
|
|
54
|
-
| Reports surfaced
|
|
49
|
+
| | [ledger-banking](../ledger-banking/) | ledger-invoicing |
|
|
50
|
+
| ------------------ | ------------------------------------ | ----------------------------------------- |
|
|
51
|
+
| Accounts | all `asset` (bank accounts) | `asset` + `income` + `liability` |
|
|
52
|
+
| A transaction is… | a transfer (cash moves) | an obligation (invoice) or its settlement |
|
|
53
|
+
| Revenue recognised | n/a | at invoice time (accrual) |
|
|
54
|
+
| Reports surfaced | balances | balances + P&L |
|
|
55
55
|
|
|
56
56
|
## Run
|
|
57
57
|
|
|
@@ -170,12 +170,12 @@ page in `createLegalPagesLayout({ siteName })`.
|
|
|
170
170
|
|
|
171
171
|
After the seed these URLs are immediately live:
|
|
172
172
|
|
|
173
|
-
| URL
|
|
174
|
-
|
|
175
|
-
| `https://your-app.example/legal/impressum`
|
|
176
|
-
| `https://your-app.example/legal/datenschutz` | Datenschutzerklärung (DE)
|
|
177
|
-
| `https://your-app.example/legal/imprint`
|
|
178
|
-
| `https://your-app.example/legal/privacy`
|
|
173
|
+
| URL | Content |
|
|
174
|
+
| -------------------------------------------- | ------------------------------ |
|
|
175
|
+
| `https://your-app.example/legal/impressum` | Impressum (DE) |
|
|
176
|
+
| `https://your-app.example/legal/datenschutz` | Datenschutzerklärung (DE) |
|
|
177
|
+
| `https://your-app.example/legal/imprint` | Imprint (EN, if seeded) |
|
|
178
|
+
| `https://your-app.example/legal/privacy` | Privacy Policy (EN, if seeded) |
|
|
179
179
|
|
|
180
180
|
→ Footer links are set per app (legal-pages does not ship a footer
|
|
181
181
|
component — deliberately, every app has its own layout).
|
|
@@ -203,22 +203,22 @@ ACL: `["TenantAdmin", "SystemAdmin"]`. Cache header `public, max-age=300`
|
|
|
203
203
|
When the required blocks (`imprint/de` + `privacy/de`) are missing
|
|
204
204
|
from SYSTEM_TENANT:
|
|
205
205
|
|
|
206
|
-
| Mode
|
|
207
|
-
|
|
206
|
+
| Mode | Behavior |
|
|
207
|
+
| --------------------- | ----------------------------------------------------------- |
|
|
208
208
|
| `NODE_ENV=production` | App boot throws an error with a slug list — container exits |
|
|
209
|
-
| otherwise (dev/test)
|
|
209
|
+
| otherwise (dev/test) | `console.warn` with a slug list, app starts anyway |
|
|
210
210
|
|
|
211
211
|
→ Safety net: no production deploy without populated legal pages.
|
|
212
212
|
|
|
213
213
|
## Troubleshooting
|
|
214
214
|
|
|
215
|
-
| Symptom
|
|
216
|
-
|
|
217
|
-
| Route returns `503 legal page unavailable`
|
|
218
|
-
| Boot check throws `ctx.templateResolver missing`
|
|
219
|
-
| Route returns `404 not configured`
|
|
220
|
-
| Multi-tenant app: tenant subdomain shows an empty page
|
|
221
|
-
| `<script>` tags in a Markdown body land 1:1 in the HTML | Deliberately accepted right now — see [legal-pages/README.md XSS section](../../../packages/bundled-features/src/legal-pages/README.md#xss--currently-not-secured-by-design) | DOMPurify is a Phase 2 once a multi-author setup arrives
|
|
215
|
+
| Symptom | Cause | Fix |
|
|
216
|
+
| ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
|
|
217
|
+
| Route returns `503 legal page unavailable` | `anonymousAccess` not configured in `runProdApp` | Set `anonymousAccess: { defaultTenantId: SYSTEM_TENANT_ID }` |
|
|
218
|
+
| Boot check throws `ctx.templateResolver missing` | `extraContext.templateResolver` not wired | Set `extraContext: ({ db, registry }) => ({ templateResolver: createTemplateResolverApi(db, registry) })` |
|
|
219
|
+
| Route returns `404 not configured` | Required block doesn't exist or has `content=null` | `seedTextBlock` with a content string |
|
|
220
|
+
| Multi-tenant app: tenant subdomain shows an empty page | (Bug regression?) Routes should ALWAYS show SYSTEM_TENANT texts | The `legal-pages.integration.test.ts` test "SYSTEM_TENANT routing" covers this — should be green |
|
|
221
|
+
| `<script>` tags in a Markdown body land 1:1 in the HTML | Deliberately accepted right now — see [legal-pages/README.md XSS section](../../../packages/bundled-features/src/legal-pages/README.md#xss--currently-not-secured-by-design) | DOMPurify is a Phase 2 once a multi-author setup arrives |
|
|
222
222
|
|
|
223
223
|
## Cross-refs
|
|
224
224
|
|
|
@@ -14,15 +14,19 @@ backings and two scopes, without a hand-written screen, nav, or env-wiring map.
|
|
|
14
14
|
|
|
15
15
|
Two keys, deliberately different:
|
|
16
16
|
|
|
17
|
-
| Key
|
|
18
|
-
|
|
17
|
+
| Key | Scope | Backing | Story |
|
|
18
|
+
| ----------------- | -------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
19
19
|
| `payment-api-key` | `system` | `secrets` | Platform-owned secret (one Stripe key bills all tenants). Stored envelope-encrypted in the secrets store, masked in every query, revealed only for the owning feature's `ctx.config` read. |
|
|
20
|
-
| `smtp-host`
|
|
20
|
+
| `smtp-host` | `tenant` | config | Platform default that a tenant admin overrides. Cascade resolves `tenant-row → system-row → default`; one tenant's override never leaks to another. |
|
|
21
21
|
|
|
22
22
|
## Pattern
|
|
23
23
|
|
|
24
24
|
```ts illustration
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
access,
|
|
27
|
+
createSystemConfig,
|
|
28
|
+
createTenantConfig,
|
|
29
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
26
30
|
|
|
27
31
|
r.config({
|
|
28
32
|
keys: {
|
|
@@ -56,7 +60,7 @@ r.config({
|
|
|
56
60
|
valid for `scope: "system"` — secrets are flat per `(tenant, key)` with no
|
|
57
61
|
cascade, so a tenant override would be incoherent and is rejected at boot.
|
|
58
62
|
- **`env: "SMTP_HOST"`** — at boot (`runProdApp`) the env var seeds the key's
|
|
59
|
-
default as an app-override, below any `system-row`. The env var
|
|
63
|
+
default as an app-override, below any `system-row`. The env var _is_ the
|
|
60
64
|
default; no manual `AppConfigOverrides` map, no re-typing in the admin UI.
|
|
61
65
|
- **`mask`** — the key surfaces automatically in the self-populating settings
|
|
62
66
|
hub: `buildConfigFeatureSchema` derives a `configEdit` screen + a child nav
|
|
@@ -94,14 +98,14 @@ the config backing only needs the `ctx.services.secrets` context shown above.
|
|
|
94
98
|
|
|
95
99
|
## Vs. the neighbouring recipes
|
|
96
100
|
|
|
97
|
-
| Pattern
|
|
98
|
-
|
|
99
|
-
| **`encrypted: true` config key** (`encrypted-tenant-config`) | `config_values`, AES ciphertext | system → tenant
|
|
100
|
-
| **`backing: "secrets"` config key** (this recipe)
|
|
101
|
-
| **`r.secret`** (`secrets-demo`)
|
|
101
|
+
| Pattern | Storage | Cascade | Use case |
|
|
102
|
+
| ------------------------------------------------------------ | ------------------------------- | ------------------ | --------------------------------------------------------------------------------- |
|
|
103
|
+
| **`encrypted: true` config key** (`encrypted-tenant-config`) | `config_values`, AES ciphertext | system → tenant | Per-tenant customer secret, no KEK rotation/audit. |
|
|
104
|
+
| **`backing: "secrets"` config key** (this recipe) | secrets envelope, system tenant | none (system-only) | Platform-owned secret needing rotation + audit-on-read, declared as a config key. |
|
|
105
|
+
| **`r.secret`** (`secrets-demo`) | secrets envelope | none | Same storage, but addressed directly — no config key, no settings-hub entry. |
|
|
102
106
|
|
|
103
107
|
`backing: "secrets"` is the bridge: you get the secrets store's guarantees
|
|
104
|
-
|
|
108
|
+
_and_ the config feature's declarative surface (masking, settings hub, the same
|
|
105
109
|
`config:write:set` write path).
|
|
106
110
|
|
|
107
111
|
## Flow
|
|
@@ -147,14 +147,14 @@ exact same helpers.
|
|
|
147
147
|
|
|
148
148
|
## Security posture
|
|
149
149
|
|
|
150
|
-
| Threat
|
|
151
|
-
|
|
152
|
-
| `<script>`/raw HTML in a page body
|
|
153
|
-
| Cross-tenant content on a shared CDN
|
|
154
|
-
| Draft leak to anonymous visitors
|
|
150
|
+
| Threat | Mitigation |
|
|
151
|
+
| --------------------------------------------------------- | ------------------------------------------------------------------- |
|
|
152
|
+
| `<script>`/raw HTML in a page body | `renderSafeMarkdown` escapes raw HTML (Markdown-only) |
|
|
153
|
+
| Cross-tenant content on a shared CDN | `Vary: Host` + per-tenant `resolveApexTenant` |
|
|
154
|
+
| Draft leak to anonymous visitors | published-only; drafts → 404 |
|
|
155
155
|
| `@import`/`url()`/`expression()`/`</style>` in custom CSS | allowlist-by-construction sanitizer (rebuilt from validated tokens) |
|
|
156
|
-
| Scope-escape / overlay onto host chrome
|
|
157
|
-
| Untrusted tenant abusing CSS
|
|
156
|
+
| Scope-escape / overlay onto host chrome | scope-prefix + `position`/`isolation`/`overflow` containment |
|
|
157
|
+
| Untrusted tenant abusing CSS | tier-gate (`managed-pages-css`) + `allowCustomCss` opt-in |
|
|
158
158
|
|
|
159
159
|
Custom-CSS sanitization for untrusted tenants is **best-effort defense-in-depth**:
|
|
160
160
|
a tenant can still restyle its **own** page area within the clip. Keep it
|
|
@@ -15,18 +15,18 @@ behaves end-to-end against the worktree's framework source.
|
|
|
15
15
|
|
|
16
16
|
## What's inside
|
|
17
17
|
|
|
18
|
-
| Step
|
|
19
|
-
|
|
20
|
-
| `r.step.aggregate.create`
|
|
21
|
-
| `r.step.aggregate.update`
|
|
22
|
-
| `r.step.aggregate.appendEvent`
|
|
23
|
-
| `r.step.read.findOne`
|
|
24
|
-
| `r.step.read.findMany`
|
|
25
|
-
| `r.step.compute`
|
|
26
|
-
| `r.step.branch`
|
|
27
|
-
| `r.step.forEach`
|
|
28
|
-
| `r.step.return`
|
|
29
|
-
| `r.step.unsafeProjectionUpsert` | `product:adjust-stock`
|
|
18
|
+
| Step | Where it shows up |
|
|
19
|
+
| ------------------------------- | ------------------------------------------------------------------------------ |
|
|
20
|
+
| `r.step.aggregate.create` | `product:create` |
|
|
21
|
+
| `r.step.aggregate.update` | `product:rename`, `product:adjust-stock`, `product:bulk-adjust` |
|
|
22
|
+
| `r.step.aggregate.appendEvent` | `product:adjust-stock`, `product:archive`, `report:archive-low-stock-products` |
|
|
23
|
+
| `r.step.read.findOne` | `product:rename`, `product:adjust-stock`, `product:bulk-adjust` |
|
|
24
|
+
| `r.step.read.findMany` | `report:archive-low-stock-products` |
|
|
25
|
+
| `r.step.compute` | `product:adjust-stock`, `product:bulk-adjust` |
|
|
26
|
+
| `r.step.branch` | `product:rename`, `product:adjust-stock` |
|
|
27
|
+
| `r.step.forEach` | `product:bulk-adjust`, `report:archive-low-stock-products` |
|
|
28
|
+
| `r.step.return` | every handler |
|
|
29
|
+
| `r.step.unsafeProjectionUpsert` | `product:adjust-stock` |
|
|
30
30
|
| `r.step.unsafeProjectionDelete` | `product:adjust-stock`, `product:archive`, `report:archive-low-stock-products` |
|
|
31
31
|
|
|
32
32
|
## Domain
|
|
@@ -92,7 +92,7 @@ engine APIs land in a published release.
|
|
|
92
92
|
in the pure event-sourcing shape — it appends a domain event
|
|
93
93
|
(`product-stock-adjusted`) and lets the inline
|
|
94
94
|
`product-stock-counter` projection update `currentStock`. The
|
|
95
|
-
alternative (calling `aggregate.update`
|
|
95
|
+
alternative (calling `aggregate.update` _and_ `appendEvent` in
|
|
96
96
|
one handler) inflates the stream-version mid-handler and trips
|
|
97
97
|
optimistic-locking on the next call. The other handlers
|
|
98
98
|
(`rename`, `bulk-adjust`) use `aggregate.update` directly
|
|
@@ -28,11 +28,11 @@ the URL carries a share token).
|
|
|
28
28
|
|
|
29
29
|
## Handlers
|
|
30
30
|
|
|
31
|
-
| QN
|
|
32
|
-
|
|
31
|
+
| QN | Access |
|
|
32
|
+
| -------------------------------------- | --------------------------- |
|
|
33
33
|
| `public-share:write:share-link:create` | authenticated (`openToAll`) |
|
|
34
|
-
| `public-share:write:share-link:revoke` | owner
|
|
35
|
-
| `public-share:query:share-by-token`
|
|
34
|
+
| `public-share:write:share-link:revoke` | owner |
|
|
35
|
+
| `public-share:query:share-by-token` | anonymous + authenticated |
|
|
36
36
|
|
|
37
37
|
## Run tests
|
|
38
38
|
|
|
@@ -53,7 +53,10 @@ mounts `sessions` as part of its own require-chain. Combine both directly;
|
|
|
53
53
|
`dedupeFeatures()` collapses the two identical `sessions` instances at boot:
|
|
54
54
|
|
|
55
55
|
```ts illustration
|
|
56
|
-
import {
|
|
56
|
+
import {
|
|
57
|
+
composeGdprStack,
|
|
58
|
+
composeSecurityStack,
|
|
59
|
+
} from "@cosmicdrift/kumiko-dev-server/compose-stacks";
|
|
57
60
|
|
|
58
61
|
export const APP_FEATURES = [
|
|
59
62
|
// ...config, user, tenant, auth-foundation...
|
|
@@ -64,12 +64,12 @@ role vocabulary overrides those instead of forking the feature.
|
|
|
64
64
|
|
|
65
65
|
## Why the schema needs a special case
|
|
66
66
|
|
|
67
|
-
`buildInsertSchema` always makes `currency` required
|
|
67
|
+
`buildInsertSchema` always makes `currency` required _inside_ a money field's
|
|
68
68
|
value object, even when the field itself is optional — a caller literally
|
|
69
69
|
cannot submit `{ amount: 1000 }` against the plain `defineEntityCreateHandler`.
|
|
70
70
|
`defineCreateWithTenantDefaults` swaps in its own schema (`.extend()` over the
|
|
71
71
|
same `buildInsertSchema` output, with just the declared `currencyFields`
|
|
72
|
-
relaxed) so the caller
|
|
72
|
+
relaxed) so the caller _can_ omit `currency`, then fills it from
|
|
73
73
|
`ctx.config(TenantSettingsConfig.currency)` before delegating to the same
|
|
74
74
|
`executor.create()` the generic factory uses.
|
|
75
75
|
|
|
@@ -82,13 +82,13 @@ changes them from **Settings → Tenant → Default Currency / Default Locale**.
|
|
|
82
82
|
|
|
83
83
|
## Flow
|
|
84
84
|
|
|
85
|
-
1. No tenant override yet → a new invoice gets the
|
|
85
|
+
1. No tenant override yet → a new invoice gets the _feature's own_ default
|
|
86
86
|
(`"EUR"`/`"en"`, set via `createTenantSettingsFeature()`'s own defaults).
|
|
87
87
|
2. Tenant sets `currency` to `"CHF"` and `locale` to `"de"` via the
|
|
88
88
|
Settings-Hub (or `config:write:set` directly).
|
|
89
89
|
3. A new invoice that doesn't specify `amount.currency`/`language` picks up
|
|
90
90
|
`"CHF"`/`"de"` — no entity code changed, no second migration.
|
|
91
|
-
4. A caller that
|
|
91
|
+
4. A caller that _does_ specify `amount.currency` still wins — the tenant
|
|
92
92
|
setting only fills gaps, it never overrides an explicit value.
|
|
93
93
|
|
|
94
94
|
## Tests
|
|
@@ -17,7 +17,7 @@ toggleable features unlock in the same request.
|
|
|
17
17
|
## What it shows
|
|
18
18
|
|
|
19
19
|
- **`tier-engine:write:set-tenant-tier`** — a SystemAdmin assigns a tier
|
|
20
|
-
to
|
|
20
|
+
to _any_ tenant, cross-tenant. Stamps `source: "manual"` so a future
|
|
21
21
|
Stripe → tier sync won't overwrite the grant.
|
|
22
22
|
- **`tier-engine:query:get-tenant-tier`** — SystemAdmin reads back which
|
|
23
23
|
tier a tenant is on plus its source (`"manual"` vs `"billing"`).
|
|
@@ -58,7 +58,7 @@ notes-export → r.toggleable() domain feature unlocked by pro tier
|
|
|
58
58
|
path bypasses the `postSave` entity-hook the resolver normally uses to
|
|
59
59
|
invalidate the cache after a tier-assignment change. Without an explicit
|
|
60
60
|
cache update the grant would persist (next request would see it) but
|
|
61
|
-
|
|
61
|
+
_this_ request still sees the old tier — surprising for an operator who
|
|
62
62
|
just clicked "set tier to pro".
|
|
63
63
|
|
|
64
64
|
The feature wires `onAssigned` into `createSetTenantTierWrite` for
|
|
@@ -25,13 +25,13 @@ defineFeature("notes", (r) => {
|
|
|
25
25
|
r.useExtension(EXT_USER_DATA, "note", {
|
|
26
26
|
export: async (ctx) => ({
|
|
27
27
|
entity: "note",
|
|
28
|
-
rows: await ctx.db
|
|
29
|
-
.
|
|
30
|
-
|
|
28
|
+
rows: await ctx.db
|
|
29
|
+
.select()
|
|
30
|
+
.from(notesTable)
|
|
31
|
+
.where(and(eq(notesTable.tenantId, ctx.tenantId), eq(notesTable.authorId, ctx.userId))),
|
|
31
32
|
}),
|
|
32
33
|
delete: async (ctx, strategy) => {
|
|
33
|
-
const where = and(eq(notesTable.tenantId, ctx.tenantId),
|
|
34
|
-
eq(notesTable.authorId, ctx.userId));
|
|
34
|
+
const where = and(eq(notesTable.tenantId, ctx.tenantId), eq(notesTable.authorId, ctx.userId));
|
|
35
35
|
if (strategy === "anonymize") {
|
|
36
36
|
await ctx.db.update(notesTable).set({ authorId: null }).where(where);
|
|
37
37
|
} else {
|
|
@@ -44,21 +44,21 @@ defineFeature("notes", (r) => {
|
|
|
44
44
|
|
|
45
45
|
## What runs automatically
|
|
46
46
|
|
|
47
|
-
| Article
|
|
48
|
-
|
|
47
|
+
| Article | What happens | Trigger |
|
|
48
|
+
| ------------ | ---------------------------------------------- | -------------------------------------- |
|
|
49
49
|
| Art. 15 + 20 | Notes snippet in export ZIP + magic-link email | `runUserExport` (via `request-export`) |
|
|
50
|
-
| Art. 17
|
|
51
|
-
| Art. 18
|
|
52
|
-
| Operator
|
|
50
|
+
| Art. 17 | Notes deleted or anonymized after grace | `runForgetCleanup` cron |
|
|
51
|
+
| Art. 18 | Restriction blocks login while active | Auth middleware |
|
|
52
|
+
| Operator | Brute-force detection on magic-link endpoint | Edge rate-limit + audit log |
|
|
53
53
|
|
|
54
54
|
## Strategy-aware delete
|
|
55
55
|
|
|
56
56
|
`retention.policyFor("note")` resolves per entity:
|
|
57
57
|
|
|
58
|
-
| Profile
|
|
59
|
-
|
|
60
|
-
| `eu-dsgvo` (default) | `delete`
|
|
61
|
-
| `de-hr-dsgvo-hgb`
|
|
58
|
+
| Profile | Strategy | Effect |
|
|
59
|
+
| -------------------- | --------------------------- | --------------------------------------------- |
|
|
60
|
+
| `eu-dsgvo` (default) | `delete` | Notes hard-deleted |
|
|
61
|
+
| `de-hr-dsgvo-hgb` | `anonymize` for HR entities | `authorId=null`, row kept for multi-user refs |
|
|
62
62
|
|
|
63
63
|
The integration test pins both paths.
|
|
64
64
|
|
|
@@ -99,7 +99,9 @@ const result = await runRetentionCleanup({
|
|
|
99
99
|
registry: stack.registry,
|
|
100
100
|
tenantId,
|
|
101
101
|
preloadedTenantPreset: null,
|
|
102
|
-
now: getTemporal()
|
|
102
|
+
now: getTemporal()
|
|
103
|
+
.Now.instant()
|
|
104
|
+
.add({ hours: 31 * 24 }), // keepFor 30d + 1 day
|
|
103
105
|
files: createFileContext(() => Promise.resolve(provider)), // the stack's storage provider
|
|
104
106
|
});
|
|
105
107
|
// result.hardDeleted === 1, bytes and file_refs row gone
|
|
@@ -107,10 +109,10 @@ const result = await runRetentionCleanup({
|
|
|
107
109
|
|
|
108
110
|
## Full app vs this recipe
|
|
109
111
|
|
|
110
|
-
|
|
|
111
|
-
|
|
112
|
-
| Scope | One hook pattern
|
|
113
|
-
| Boot
|
|
112
|
+
| | This recipe | [user-data-rights-demo](/en/samples/apps-user-data-rights-demo/) |
|
|
113
|
+
| ----- | --------------------- | ---------------------------------------------------------------- |
|
|
114
|
+
| Scope | One hook pattern | Runnable app + todos + files |
|
|
115
|
+
| Boot | Integration test only | `bun dev` on port 4291 |
|
|
114
116
|
|
|
115
117
|
## Dependencies
|
|
116
118
|
|
|
@@ -68,7 +68,9 @@ export async function pruneEvents(db, options) {
|
|
|
68
68
|
if (candidates.length === 0) {
|
|
69
69
|
return { deletedCount: 0, cutoff, aggregateTypes, dryRun };
|
|
70
70
|
}
|
|
71
|
-
const maxCandidateId = candidates.reduce(
|
|
71
|
+
const maxCandidateId = candidates.reduce(
|
|
72
|
+
// biome-ignore lint/complexity/useMathMinMax: ids are bigint, Math.max throws a TypeError on BigInt
|
|
73
|
+
(acc, row) => (row.id > acc ? row.id : acc), candidates[0]?.id ?? 0n);
|
|
72
74
|
// Step 2 — safety guard: check every ACTIVE consumer has moved past
|
|
73
75
|
// the candidate set. Disabled consumers are intentionally excluded —
|
|
74
76
|
// ops disables them precisely to park them while pruning happens.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-framework",
|
|
3
|
-
"version": "0.404.
|
|
3
|
+
"version": "0.404.1",
|
|
4
4
|
"description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -203,21 +203,21 @@
|
|
|
203
203
|
"./package.json": "./package.json"
|
|
204
204
|
},
|
|
205
205
|
"dependencies": {
|
|
206
|
-
"@cosmicdrift/kumiko-types": "0.404.
|
|
207
|
-
"bullmq": "^5.
|
|
206
|
+
"@cosmicdrift/kumiko-types": "0.404.1",
|
|
207
|
+
"bullmq": "^5.81.5",
|
|
208
208
|
"hono": "^4.13.14",
|
|
209
|
-
"i18next": "^26.
|
|
210
|
-
"ioredis": "^5.
|
|
211
|
-
"jose": "^6.2.
|
|
212
|
-
"pino": "^10.
|
|
209
|
+
"i18next": "^26.5.11",
|
|
210
|
+
"ioredis": "^5.11.1",
|
|
211
|
+
"jose": "^6.2.14",
|
|
212
|
+
"pino": "^10.4.0",
|
|
213
213
|
"temporal-polyfill": "^0.3.2",
|
|
214
|
-
"uuid": "^14.0.
|
|
214
|
+
"uuid": "^14.0.3",
|
|
215
215
|
"write-excel-file": "^4.1.1",
|
|
216
216
|
"zod": "^4.6.7"
|
|
217
217
|
},
|
|
218
218
|
"peerDependencies": {
|
|
219
219
|
"bun-types": "^1.4.4",
|
|
220
|
-
"meilisearch": "^0.
|
|
220
|
+
"meilisearch": "^0.62.0"
|
|
221
221
|
},
|
|
222
222
|
"peerDependenciesMeta": {
|
|
223
223
|
"meilisearch": {
|
|
@@ -225,11 +225,11 @@
|
|
|
225
225
|
}
|
|
226
226
|
},
|
|
227
227
|
"devDependencies": {
|
|
228
|
-
"@cosmicdrift/kumiko-testing": "0.404.
|
|
228
|
+
"@cosmicdrift/kumiko-testing": "0.404.1",
|
|
229
229
|
"bun-types": "^1.4.4",
|
|
230
230
|
"fflate": "^0.8.3",
|
|
231
|
-
"meilisearch": "^0.
|
|
232
|
-
"pino-pretty": "^13.
|
|
231
|
+
"meilisearch": "^0.62.0",
|
|
232
|
+
"pino-pretty": "^13.2.0"
|
|
233
233
|
},
|
|
234
234
|
"publishConfig": {
|
|
235
235
|
"registry": "https://registry.npmjs.org",
|
package/presets/biome-app.json
CHANGED