@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.
- 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/api/auth-routes-mfa.js +2 -2
- package/dist/api/index.d.ts +2 -2
- package/dist/api/index.js +1 -1
- package/dist/api/readiness.d.ts +0 -11
- package/dist/api/readiness.js +1 -30
- package/dist/crypto/index.d.ts +1 -0
- package/dist/crypto/index.js +1 -0
- package/dist/crypto/opaque-token-hash.d.ts +3 -0
- package/dist/crypto/opaque-token-hash.js +14 -0
- package/dist/crypto/subject-resolver.js +3 -8
- package/dist/db/blind-index-cleanup.js +3 -10
- package/dist/db/query.js +3 -18
- package/dist/derivatives/variant-key.d.ts +4 -0
- package/dist/derivatives/variant-key.js +4 -3
- package/dist/engine/boot-validator/field-conditions.js +2 -7
- package/dist/engine/boot-validator/list-table-aggregate-query.js +1 -1
- package/dist/engine/boot-validator/parent-ref.d.ts +0 -2
- package/dist/engine/boot-validator/parent-ref.js +1 -8
- package/dist/engine/boot-validator/projection-list-screens.js +1 -1
- package/dist/engine/boot-validator/read-gate.js +1 -1
- package/dist/engine/find-entity.d.ts +3 -0
- package/dist/engine/find-entity.js +8 -0
- package/dist/engine/project-app-schema-for-roles.js +19 -55
- package/dist/files/file-handle.js +3 -16
- package/dist/observability/ids.js +1 -8
- package/dist/pipeline/event-retention.js +3 -1
- package/dist/pipeline/projection-rebuild.js +3 -17
- package/dist/pipeline/tenant-timezone-cache.js +8 -27
- package/dist/search/index.d.ts +3 -0
- package/dist/search/index.js +3 -0
- package/dist/search/is-entity-searchable.d.ts +2 -0
- package/dist/search/is-entity-searchable.js +4 -0
- package/dist/search/meilisearch-peer.js +1 -1
- package/dist/search/reindex-tenants-search.d.ts +19 -0
- package/dist/search/reindex-tenants-search.js +69 -0
- package/dist/utils/chunk.d.ts +1 -0
- package/dist/utils/chunk.js +7 -0
- package/dist/utils/hex.d.ts +3 -0
- package/dist/utils/hex.js +11 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/lru-cache.d.ts +17 -0
- package/dist/utils/lru-cache.js +56 -0
- package/dist/utils/run-with-timeout.d.ts +11 -0
- package/dist/utils/run-with-timeout.js +30 -0
- package/package.json +12 -12
- package/presets/biome-app.json +1 -1
- 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
|
|
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`
|