@cosmicdrift/kumiko-framework 0.404.0 → 0.405.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/AGENTS.md +103 -103
  2. package/README.md +19 -17
  3. package/agents/examples/apex-landing.md +3 -3
  4. package/agents/examples/apex-surface-auth.md +13 -9
  5. package/agents/examples/auth-mfa.md +2 -4
  6. package/agents/examples/boot-check.md +2 -2
  7. package/agents/examples/compliance-profiles-demo.md +5 -5
  8. package/agents/examples/compose-stacks.md +8 -8
  9. package/agents/examples/cross-feature-events.md +1 -0
  10. package/agents/examples/crypto-shredding-hr.md +3 -3
  11. package/agents/examples/custom-fields-basic.md +1 -1
  12. package/agents/examples/documents.md +28 -0
  13. package/agents/examples/encrypted-tenant-config.md +8 -8
  14. package/agents/examples/env-composition.md +10 -10
  15. package/agents/examples/error-contract.md +9 -9
  16. package/agents/examples/event-sourcing.md +9 -9
  17. package/agents/examples/ledger-invoicing.md +6 -6
  18. package/agents/examples/legal-pages.md +16 -16
  19. package/agents/examples/managed-config.md +15 -11
  20. package/agents/examples/managed-pages.md +7 -7
  21. package/agents/examples/pipeline-basics.md +13 -13
  22. package/agents/examples/public-share-token.md +4 -4
  23. package/agents/examples/security-baseline.md +4 -1
  24. package/agents/examples/session-revocation.md +1 -1
  25. package/agents/examples/tenant-settings.md +4 -4
  26. package/agents/examples/tier-admin.md +2 -2
  27. package/agents/examples/user-data-rights.md +21 -19
  28. package/dist/api/auth-routes-mfa.js +2 -2
  29. package/dist/api/index.d.ts +2 -2
  30. package/dist/api/index.js +1 -1
  31. package/dist/api/readiness.d.ts +0 -11
  32. package/dist/api/readiness.js +1 -30
  33. package/dist/crypto/index.d.ts +1 -0
  34. package/dist/crypto/index.js +1 -0
  35. package/dist/crypto/opaque-token-hash.d.ts +3 -0
  36. package/dist/crypto/opaque-token-hash.js +14 -0
  37. package/dist/crypto/subject-resolver.js +3 -8
  38. package/dist/db/blind-index-cleanup.js +3 -10
  39. package/dist/db/query.js +3 -18
  40. package/dist/derivatives/variant-key.d.ts +4 -0
  41. package/dist/derivatives/variant-key.js +4 -3
  42. package/dist/engine/boot-validator/field-conditions.js +2 -7
  43. package/dist/engine/boot-validator/list-table-aggregate-query.js +1 -1
  44. package/dist/engine/boot-validator/parent-ref.d.ts +0 -2
  45. package/dist/engine/boot-validator/parent-ref.js +1 -8
  46. package/dist/engine/boot-validator/projection-list-screens.js +1 -1
  47. package/dist/engine/boot-validator/read-gate.js +1 -1
  48. package/dist/engine/find-entity.d.ts +3 -0
  49. package/dist/engine/find-entity.js +8 -0
  50. package/dist/engine/project-app-schema-for-roles.js +19 -55
  51. package/dist/files/file-handle.js +3 -16
  52. package/dist/observability/ids.js +1 -8
  53. package/dist/pipeline/event-retention.js +3 -1
  54. package/dist/pipeline/projection-rebuild.js +3 -17
  55. package/dist/pipeline/tenant-timezone-cache.js +8 -27
  56. package/dist/search/index.d.ts +3 -0
  57. package/dist/search/index.js +3 -0
  58. package/dist/search/is-entity-searchable.d.ts +2 -0
  59. package/dist/search/is-entity-searchable.js +4 -0
  60. package/dist/search/meilisearch-peer.js +1 -1
  61. package/dist/search/reindex-tenants-search.d.ts +19 -0
  62. package/dist/search/reindex-tenants-search.js +69 -0
  63. package/dist/utils/chunk.d.ts +1 -0
  64. package/dist/utils/chunk.js +7 -0
  65. package/dist/utils/hex.d.ts +3 -0
  66. package/dist/utils/hex.js +11 -0
  67. package/dist/utils/index.d.ts +3 -0
  68. package/dist/utils/index.js +3 -0
  69. package/dist/utils/lru-cache.d.ts +17 -0
  70. package/dist/utils/lru-cache.js +56 -0
  71. package/dist/utils/run-with-timeout.d.ts +11 -0
  72. package/dist/utils/run-with-timeout.js +30 -0
  73. package/package.json +12 -12
  74. package/presets/biome-app.json +1 -1
  75. package/src/changes.json +36 -0
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 | 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 |
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 | 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` |
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 | 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 |
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 | 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` |
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 | 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` |
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 | 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` |
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: { reason: "any signed-in user may list and view tasks; only writes are role-gated" },
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 | What's in it |
84
- |---|---|
85
- | `@cosmicdrift/kumiko-framework/engine` | `defineFeature`, `createEntity`, field helpers, access rules, registry |
86
- | `@cosmicdrift/kumiko-framework/db` | `deriveEntityTableMeta`, `createEventStoreExecutor`, migrations, tenant-db |
87
- | `@cosmicdrift/kumiko-framework/event-store` | `events` table, `append`, `loadAggregate`, `loadAggregateAsOf` |
88
- | `@cosmicdrift/kumiko-framework/pipeline` | Dispatcher, event-dispatcher (AsyncDaemon), projection-rebuild, SSE + search consumers |
89
- | `@cosmicdrift/kumiko-framework/api` | `buildServer`, auth middleware, SSE route, error contract |
90
- | `@cosmicdrift/kumiko-framework/auth` | JWT helper, password hashing, session users |
91
- | `@cosmicdrift/kumiko-framework/search` | `SearchAdapter` interface, in-memory adapter, Meili wrapper |
92
- | `@cosmicdrift/kumiko-framework/jobs` | BullMQ-backed job runner, cron scheduling |
93
- | `@cosmicdrift/kumiko-framework/files` | Signed-URL upload/download, tenant-scoped storage |
94
- | `@cosmicdrift/kumiko-framework/i18n` | i18next setup, per-feature translation registration |
95
- | `@cosmicdrift/kumiko-framework/ui` | React hooks (Zustand stores, SSE subscription, optimistic mutations) |
96
- | `@cosmicdrift/kumiko-testing/stack` | `setupTestStack`, `createTestDb`, request helpers |
97
- | `@cosmicdrift/kumiko-framework/utils` | Safe JSON, qualified-name helpers |
98
- | `@cosmicdrift/kumiko-framework/errors` | Error classes, `writeFailure`, reason contracts |
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
- + 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.
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, SignupScreen, createLoginRoute,
40
- AuthShellProvider, emailPasswordClient,
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, ConfirmAccountDeletionScreen,
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()], // SessionProvider + i18n
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", component: <LoginRoute /> },
64
- { path: "/signup", component: <SignupScreen loginHref="/login" /> },
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", component: <RequestAccountDeletionScreen /> },
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
- + expiry (no DB table); purpose `"deletion-request"` blocks replay on other
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 *this* feature's own shape demands it (has a PII field), not
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 *another* feature being
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 | 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) |
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 | 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 |
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 *about* — so a
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 *and* list render
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 *employee's* key via
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 *define → set → read* roundtrip.
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, // ← 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
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 | 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. |
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`