@cosmicdrift/kumiko-framework 0.403.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/api/framework-engine.md +3 -0
- package/agents/api/renderer-web.md +1 -0
- package/agents/api/types.md +23 -0
- 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/lane-routing.md +40 -1
- 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/redis-sse-broker.d.ts +1 -0
- package/dist/api/redis-sse-broker.js +5 -3
- package/dist/engine/feature-config-events-jobs.js +11 -7
- package/dist/engine/soft-delete-cleanup.js +2 -2
- package/dist/entrypoint/index.d.ts +5 -2
- package/dist/entrypoint/index.js +15 -6
- package/dist/entrypoint/internals.d.ts +2 -0
- package/dist/files/feature.js +1 -1
- package/dist/jobs/cron-repeat-options.d.ts +9 -0
- package/dist/jobs/cron-repeat-options.js +9 -0
- package/dist/jobs/index.d.ts +1 -0
- package/dist/jobs/index.js +1 -0
- package/dist/jobs/job-runner-context.js +4 -1
- package/dist/jobs/job-runner-types.d.ts +5 -0
- package/dist/jobs/job-runner.js +14 -5
- package/dist/jobs/maintenance-window.d.ts +7 -0
- package/dist/jobs/maintenance-window.js +14 -0
- package/dist/pipeline/event-retention.js +3 -1
- package/dist/redis/cache-sync-bus.d.ts +1 -0
- package/dist/redis/cache-sync-bus.js +15 -6
- package/dist/redis/client.d.ts +12 -0
- package/dist/redis/client.js +30 -5
- package/dist/redis/index.d.ts +2 -2
- package/dist/redis/index.js +1 -1
- package/dist/search/meilisearch-peer.js +1 -1
- package/package.json +12 -12
- package/presets/biome-app.json +1 -1
- package/src/changes.json +12 -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
|
|
|
@@ -338,6 +338,7 @@ type CreateEventSourceLiveEventsOptions = {
|
|
|
338
338
|
url?: string; // URL of the SSE endpoint.
|
|
339
339
|
onSessionEnded?: () => void; // Fires once per connection when the session ended: the server refused the SSE handshake, or opened the stream with a `session-ended` frame (stale auth cookie downgraded to anonymous).
|
|
340
340
|
timers?: LiveEventTimers; // Reconnect-timer seam; defaults to the global setTimeout/clearTimeout.
|
|
341
|
+
stallTimers?: LiveEventTimers; // Stall-watchdog timer seam; defaults to the global setTimeout/clearTimeout.
|
|
341
342
|
random?: () => number; // Jitter source in [0, 1); defaults to Math.random.
|
|
342
343
|
now?: () => number; // Clock for synthetic resync events; defaults to Date.now.
|
|
343
344
|
};
|
package/agents/api/types.md
CHANGED
|
@@ -1247,6 +1247,7 @@ type JobQueuedMeta = {
|
|
|
1247
1247
|
type JobRunner = {
|
|
1248
1248
|
start(): Promise<void>;
|
|
1249
1249
|
stop(): Promise<void>;
|
|
1250
|
+
stopConsuming?(): Promise<void>;
|
|
1250
1251
|
dispatch(jobName: string, payload?: Record<string, unknown>, meta?: JobMeta): Promise<string>;
|
|
1251
1252
|
handleEvent(eventName: string, payload: Record<string, unknown>, user?: SessionUser, triggerEvent?: JobTriggerEvent): Promise<void>;
|
|
1252
1253
|
handleCatchUpEvent(jobName: string, eventName: string, payload: Record<string, unknown>, user: SessionUser, triggerEvent: JobTriggerEvent): Promise<void>;
|
|
@@ -1259,6 +1260,8 @@ type JobRunnerOptions = {
|
|
|
1259
1260
|
redisUrl: string;
|
|
1260
1261
|
consumerLane?: JobRunIn | undefined;
|
|
1261
1262
|
queueNamePrefix?: string | undefined;
|
|
1263
|
+
redisNamespace?: Pick<RedisNamespace, "keyPrefix" | "bullmqPrefix"> | undefined;
|
|
1264
|
+
maintenanceWindow?: MaintenanceWindow | undefined;
|
|
1262
1265
|
bootRedisTimeoutMs?: number | undefined;
|
|
1263
1266
|
schedulerSeenRefreshMs?: number | undefined;
|
|
1264
1267
|
jobRetention?: { completedAgeSec?: number | undefined; failedAgeSec?: number | undefined; } | undefined;
|
|
@@ -1272,6 +1275,15 @@ type JobRunnerOptions = {
|
|
|
1272
1275
|
};
|
|
1273
1276
|
```
|
|
1274
1277
|
|
|
1278
|
+
## packages/framework/src/jobs/maintenance-window.ts
|
|
1279
|
+
|
|
1280
|
+
```ts
|
|
1281
|
+
type MaintenanceWindow = {
|
|
1282
|
+
hour: number;
|
|
1283
|
+
minute: number;
|
|
1284
|
+
};
|
|
1285
|
+
```
|
|
1286
|
+
|
|
1275
1287
|
## packages/framework/src/lifecycle/lifecycle.ts
|
|
1276
1288
|
|
|
1277
1289
|
```ts
|
|
@@ -1436,6 +1448,17 @@ type SystemHooks = {
|
|
|
1436
1448
|
};
|
|
1437
1449
|
```
|
|
1438
1450
|
|
|
1451
|
+
## packages/framework/src/redis/client.ts
|
|
1452
|
+
|
|
1453
|
+
```ts
|
|
1454
|
+
type RedisNamespace = {
|
|
1455
|
+
keyPrefix: string; // ioredis keyPrefix for every non-BullMQ key; "" when unnamespaced.
|
|
1456
|
+
bullmqPrefix: string; // BullMQ `prefix` option ("bull" is BullMQ's own default).
|
|
1457
|
+
channelPrefix: string; // Prepended to every Pub/Sub channel (ioredis keyPrefix does not apply to Pub/Sub).
|
|
1458
|
+
channelSubscribePattern: string | undefined; // Set only with KUMIKO_REDIS_PREFIX.
|
|
1459
|
+
};
|
|
1460
|
+
```
|
|
1461
|
+
|
|
1439
1462
|
## packages/framework/src/secrets/dek-cache.ts
|
|
1440
1463
|
|
|
1441
1464
|
```ts
|
|
@@ -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
|