@cosmicdrift/kumiko-framework 0.398.0 → 0.400.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 (143) hide show
  1. package/AGENTS.md +33 -19
  2. package/agents/api/dev-server-testing.md +23 -0
  3. package/agents/api/framework-db.md +277 -0
  4. package/agents/api/framework-engine.md +1810 -0
  5. package/agents/api/framework-errors.md +119 -0
  6. package/agents/api/framework-event-store.md +99 -0
  7. package/agents/api/framework-seeding.md +26 -0
  8. package/agents/api/framework-ui-types.md +542 -0
  9. package/agents/api/renderer-web.md +1450 -0
  10. package/agents/api/testing-helpers.md +189 -0
  11. package/agents/api/testing-stack.md +144 -0
  12. package/agents/api/types.md +5124 -0
  13. package/agents/api.md +20 -0
  14. package/agents/examples/derived-fields-sql.md +251 -0
  15. package/agents/examples/documents.md +161 -0
  16. package/agents/examples/inspection-checklist.md +807 -0
  17. package/agents/examples.md +3 -0
  18. package/agents/features-index.md +178 -0
  19. package/agents/features.md +9 -1
  20. package/dist/api/entity-read-access.d.ts +2 -0
  21. package/dist/api/entity-read-access.js +46 -2
  22. package/dist/api/server-boot-checks.js +2 -2
  23. package/dist/api/server-routes.js +3 -1
  24. package/dist/db/derived-field-sql.d.ts +23 -0
  25. package/dist/db/derived-field-sql.js +115 -0
  26. package/dist/db/derived-related-sql.d.ts +31 -0
  27. package/dist/db/derived-related-sql.js +137 -0
  28. package/dist/db/driver-row-coercion.d.ts +1 -0
  29. package/dist/db/driver-row-coercion.js +13 -0
  30. package/dist/db/eagerload.d.ts +3 -1
  31. package/dist/db/eagerload.js +4 -6
  32. package/dist/db/event-store-executor-context.js +1 -1
  33. package/dist/db/event-store-executor-read-paging.js +1 -1
  34. package/dist/db/event-store-executor-read-search.d.ts +3 -0
  35. package/dist/db/event-store-executor-read-search.js +17 -9
  36. package/dist/db/event-store-executor-read-sort.d.ts +9 -2
  37. package/dist/db/event-store-executor-read-sort.js +27 -14
  38. package/dist/db/event-store-executor-read-where.d.ts +0 -5
  39. package/dist/db/event-store-executor-read-where.js +3 -9
  40. package/dist/db/event-store-executor-read.js +23 -4
  41. package/dist/db/event-store-executor-write-steps.d.ts +6 -1
  42. package/dist/db/event-store-executor-write-steps.js +20 -1
  43. package/dist/db/event-store-executor-write.js +8 -8
  44. package/dist/db/money.d.ts +4 -0
  45. package/dist/db/money.js +34 -29
  46. package/dist/db/parent-ref-clause.d.ts +1 -0
  47. package/dist/db/parent-ref-clause.js +18 -8
  48. package/dist/db/read-gate-clause.d.ts +7 -0
  49. package/dist/db/read-gate-clause.js +188 -0
  50. package/dist/db/read-ownership.d.ts +9 -0
  51. package/dist/db/read-ownership.js +12 -0
  52. package/dist/engine/boot-validator/boot-check.d.ts +3 -1
  53. package/dist/engine/boot-validator/boot-check.js +7 -11
  54. package/dist/engine/boot-validator/boot-context.d.ts +26 -0
  55. package/dist/engine/boot-validator/boot-context.js +57 -0
  56. package/dist/engine/boot-validator/boot-options.d.ts +33 -0
  57. package/dist/engine/boot-validator/boot-options.js +1 -0
  58. package/dist/engine/boot-validator/boot-phases-app.d.ts +5 -0
  59. package/dist/engine/boot-validator/boot-phases-app.js +103 -0
  60. package/dist/engine/boot-validator/boot-phases.d.ts +11 -0
  61. package/dist/engine/boot-validator/boot-phases.js +140 -0
  62. package/dist/engine/boot-validator/boot-report.d.ts +6 -0
  63. package/dist/engine/boot-validator/boot-report.js +47 -0
  64. package/dist/engine/boot-validator/boot-rule.d.ts +12 -0
  65. package/dist/engine/boot-validator/boot-rule.js +14 -0
  66. package/dist/engine/boot-validator/boot-warnings.d.ts +2 -0
  67. package/dist/engine/boot-validator/boot-warnings.js +20 -0
  68. package/dist/engine/boot-validator/derived-sql-fields.d.ts +4 -0
  69. package/dist/engine/boot-validator/derived-sql-fields.js +184 -0
  70. package/dist/engine/boot-validator/entity-handler.d.ts +3 -0
  71. package/dist/engine/boot-validator/entity-handler.js +6 -3
  72. package/dist/engine/boot-validator/entity-list-screens.js +3 -1
  73. package/dist/engine/boot-validator/field-conditions.d.ts +2 -0
  74. package/dist/engine/boot-validator/field-conditions.js +178 -0
  75. package/dist/engine/boot-validator/i18n-keys.d.ts +2 -2
  76. package/dist/engine/boot-validator/i18n-keys.js +1 -3
  77. package/dist/engine/boot-validator/index.d.ts +5 -34
  78. package/dist/engine/boot-validator/index.js +24 -204
  79. package/dist/engine/boot-validator/pii-retention.js +2 -7
  80. package/dist/engine/boot-validator/projection-list-screens.js +19 -1
  81. package/dist/engine/boot-validator/read-gate.d.ts +2 -0
  82. package/dist/engine/boot-validator/read-gate.js +54 -0
  83. package/dist/engine/boot-validator/screens-forms.js +19 -6
  84. package/dist/engine/boot-validator/screens-kind-entity-list.js +8 -6
  85. package/dist/engine/boot-validator/screens-lists.js +8 -0
  86. package/dist/engine/boot-validator/zod-shape.js +4 -2
  87. package/dist/engine/build-app-schema.js +11 -5
  88. package/dist/engine/embedded-derived.d.ts +8 -6
  89. package/dist/engine/embedded-derived.js +25 -7
  90. package/dist/engine/entity-handlers.d.ts +1 -1
  91. package/dist/engine/entity-handlers.js +17 -18
  92. package/dist/engine/entity-list-runtime-options.d.ts +5 -1
  93. package/dist/engine/entity-list-runtime-options.js +103 -4
  94. package/dist/engine/factories.d.ts +5 -5
  95. package/dist/engine/factories.js +3 -5
  96. package/dist/engine/field-access.d.ts +2 -0
  97. package/dist/engine/field-access.js +48 -1
  98. package/dist/engine/field-state.d.ts +12 -0
  99. package/dist/engine/field-state.js +67 -0
  100. package/dist/engine/handler-helpers.d.ts +3 -0
  101. package/dist/engine/handler-helpers.js +7 -1
  102. package/dist/engine/index.d.ts +2 -1
  103. package/dist/engine/index.js +2 -1
  104. package/dist/engine/ownership.d.ts +1 -1
  105. package/dist/engine/ownership.js +9 -5
  106. package/dist/engine/registry-state.d.ts +1 -0
  107. package/dist/engine/registry-state.js +1 -1
  108. package/dist/engine/schema-builder.js +3 -0
  109. package/dist/engine/strip-unreadable-related-lists.d.ts +4 -0
  110. package/dist/engine/strip-unreadable-related-lists.js +80 -0
  111. package/dist/entrypoint/index.js +1 -0
  112. package/dist/entrypoint/internals.d.ts +2 -0
  113. package/dist/files/assert-file-refs-belong-to-tenant.js +26 -6
  114. package/dist/files/bind-pending-file-refs.d.ts +3 -2
  115. package/dist/files/bind-pending-file-refs.js +11 -12
  116. package/dist/files/bind-referenced-files.write.js +5 -5
  117. package/dist/files/create-file-ref-from-bytes.d.ts +2 -0
  118. package/dist/files/create-file-ref-from-bytes.js +2 -0
  119. package/dist/files/create-file-ref.d.ts +2 -0
  120. package/dist/files/create-file-ref.js +39 -22
  121. package/dist/files/dispose-removed-file-refs.js +5 -8
  122. package/dist/files/file-field-slots.d.ts +10 -0
  123. package/dist/files/file-field-slots.js +53 -0
  124. package/dist/files/file-ref-entity.d.ts +1 -1
  125. package/dist/files/file-ref-table.d.ts +1 -1
  126. package/dist/files/file-routes.js +30 -9
  127. package/dist/files/index.d.ts +1 -0
  128. package/dist/files/index.js +1 -0
  129. package/dist/files/load-save-file-refs.js +3 -4
  130. package/dist/files/record-read-check.js +2 -30
  131. package/dist/files/record-write-check.d.ts +2 -0
  132. package/dist/files/record-write-check.js +18 -3
  133. package/dist/files/upload-field-resolution.d.ts +4 -1
  134. package/dist/files/upload-field-resolution.js +57 -12
  135. package/dist/pipeline/dispatch-batch.js +6 -5
  136. package/dist/pipeline/dispatch-write.js +2 -1
  137. package/dist/pipeline/write-result-masking.js +2 -1
  138. package/dist/schema-cli/validate-command.js +46 -12
  139. package/dist/schema-cli.js +1 -1
  140. package/dist/ui-types/index.d.ts +2 -1
  141. package/dist/ui-types/index.js +2 -1
  142. package/package.json +3 -3
  143. package/src/changes.json +67 -0
@@ -27,7 +27,9 @@
27
27
  | `custom-handlers` | Beyond CRUD — when your domain refuses to fit a table. | `examples/custom-handlers.md` |
28
28
  | `dashboard-stats` | Key figures on a dashboard: stat panels backed by one counting query. | `examples/dashboard-stats.md` |
29
29
  | `delivery-notifications` | Delivery flows plus notifications. Package tracking, minus the tracking. | `examples/delivery-notifications.md` |
30
+ | `derived-fields-sql` | SQL derived fields on a list: coalesce, related count with a where, related first (text and money), sortable columns and a defaultSort on a derived value. | `examples/derived-fields-sql.md` |
30
31
  | `designer-demo` | Kumiko sample feature — scaffolded by `bun kumiko create` | `examples/designer-demo.md` |
32
+ | `documents` | A document register on top of existing records: captured files, expiry dates and per-record visibility. | `examples/documents.md` |
31
33
  | `e2e-generator` | generateE2ESpec + withBootValidatorFixture — Registry → JSON-Specs für externen Playwright-Worker. Zeigt die 4 Test-Kinds, Feldtyp→Interaktion-Mapping und welche Feldtypen heute noch nicht generisch unterstützt sind. | `examples/e2e-generator.md` |
32
34
  | `embedded` | Embedded fields on entities. Structured JSON without the JSON part. | `examples/embedded.md` |
33
35
  | `embedded-entity-form` | RenderEdit hosted in a Drawer, no screen or route — externally-sourced prefill, controlled mode, and a custom write handler instead of the built-in CRUD create. | `examples/embedded-entity-form.md` |
@@ -43,6 +45,7 @@
43
45
  | `i18n` | Feature-scoped translations. 'Hallo' and 'Hello' in the same app. | `examples/i18n.md` |
44
46
  | `idempotency` | Same request, same result. Dedupes on request ID. | `examples/idempotency.md` |
45
47
  | `inbound-mail-triage` | Inbound-mail consumer pattern: inbound-mail-foundation + in-memory provider, a worker-lane triage job attached to the ingest handler, verified end-to-end through createAllInOneEntrypoint. | `examples/inbound-mail-triage.md` |
48
+ | `inspection-checklist` | Embedded list as a phone inspection checklist: conditional fields, derived pass/fail, photos per row. | `examples/inspection-checklist.md` |
46
49
  | `lane-routing` | runIn lane-routing: r.job pinned to api/worker deploy-lanes, event-chain via ctx.events.append, verified end-to-end through createAllInOneEntrypoint. | `examples/lane-routing.md` |
47
50
  | `ledger-banking` | Bank accounts + transfers on the ledger primitive — balances are pure queries, never stored. | `examples/ledger-banking.md` |
48
51
  | `ledger-invoicing` | Accrual invoicing on the ledger primitive — revenue at invoice time, VAT as a liability, receivable cleared on payment. | `examples/ledger-invoicing.md` |
@@ -0,0 +1,178 @@
1
+ <!-- Generated by `bun run gen:agent-docs` from samples/apps/use-all-bundled/feature-manifest.json. Do not edit. -->
2
+
3
+ # Bundled features index
4
+
5
+ One entry per feature a package ships: purpose, import of its factory, required features, config keys without a default and secrets. Factory options: `api/` or the feature's types. Details: `features.md`.
6
+
7
+ - `account-security`: Self-service account dashboard: MFA enrollment, recovery codes and the user's own sessions, composed from the auth-mfa and sessions screens.
8
+ `import { createAccountSecurityFeature } from "@cosmicdrift/kumiko-bundled-features/account-security"`; requires `sessions`, `auth-mfa`
9
+ - `admin-shell`: Registers tenant-admin and platform-admin workspaces with provider nav into owner-feature screens (`tenant:screen:members`, `audit:screen:audit-log`, `audit:screen:audit-log-all-tenants`, `tenant:screen:tenant-list`, `jobs:screen:job-runs`, optional `tier-engine:screen:tier-admin`, optional `cap-overview:screen:my-caps`/`cap-overview:screen:tenant-cap-list`, optional `document-designer` template navs) and the two overview dashboards (the tenant one can be dropped with `includeTenantOverview: false`), built from the metrics list (`metrics` option, default DEFAULT_METRICS).
10
+ `import { createAdminShellFeature } from "@cosmicdrift/kumiko-bundled-features/admin-shell"`; requires `user`, `tenant`, `audit`, `jobs`, `metrics`, `metrics-system`, `tier-engine`, `cap-overview`, `document-designer`
11
+ - `agent-tools`: Builds a tool catalog and an agent manifest from the mounted registry so an LLM agent can call handlers and understand the app's shape.
12
+ `import { createAgentToolsFeature } from "@cosmicdrift/kumiko-bundled-features/agent-tools"`
13
+ - `audit`: Exposes the framework's event store as a paginated, filterable audit log via the `audit:query:list` handler (accessible to `Admin` and `SystemAdmin` roles).
14
+ `import { createAuditFeature } from "@cosmicdrift/kumiko-bundled-features/audit"`; requires `tenant`, `user`, `config`
15
+ - `auth-email-password`: Provides email+password authentication: the always-on handlers are `login`, `changePassword`, and `logout`; optional flows — password reset, email verification, magic-link self-signup, tenant invite, and account-unlock — are registered only when you pass their respective option objects (`passwordReset`, `emailVerification`, `signup`, `invite`, `accountUnlock`) to `createAuthEmailPasswordFeature(opts)`.
16
+ `import { createAuthEmailPasswordFeature } from "@cosmicdrift/kumiko-bundled-features/auth-email-password"`; requires `user`, `tenant`, `delivery`
17
+ - `auth-foundation`: Declares auth-middleware extension points: `tokenVerifier` (Bearer), `sessionStore` (revocable JWT sid), optional `tenantResolver` + `tenantExistence` for anonymous multi-tenant.
18
+ `import { createAuthFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation"`
19
+ - `auth-mfa`: TOTP-based two-factor authentication: enable/disable flow with QR-code setup, 8 single-use recovery codes, and (once wired into a login flow) a second login step after password verification.
20
+ `import { createAuthMfaFeature } from "@cosmicdrift/kumiko-bundled-features/auth-mfa"`; requires `user`, `config`, `tenant`
21
+ - `auth-mfa-user-data`: GDPR (Art.
22
+ `import { createAuthMfaUserDataFeature } from "@cosmicdrift/kumiko-bundled-features/auth-mfa-user-data"`; requires `user-data-rights`
23
+ - `auth-self-registration`: Runtime on/off switch for the auth-email-password self-signup flow.
24
+ `import { createAuthSelfRegistrationToggleFeature } from "@cosmicdrift/kumiko-bundled-features/auth-email-password"`
25
+ - `billing-foundation`: Plugin host for subscription billing.
26
+ `import { createBillingFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/billing-foundation"`; requires `tenant-lifecycle`, `compliance-profiles`
27
+ - `cap-counter`: Tracks per-tenant usage against configurable limits using two complementary storage models: calendar-period counters (one projection row per tenant/capName/period, reset implicitly by period rollover) and rolling-window counters (append-only event stream, no projection).
28
+ `import { createCapCounterFeature } from "@cosmicdrift/kumiko-bundled-features/cap-counter"`
29
+ - `cap-overview`: Read-only visibility into per-tenant tier assignment and cap usage.
30
+ `import { createCapOverviewFeature } from "@cosmicdrift/kumiko-bundled-features/cap-overview"`; requires `tenant`, `tier-engine`, `billing-foundation`
31
+ - `channel-discord`: Posts delivery notifications to a Discord webhook, registered as the `discord` channel in the delivery system.
32
+ `import { createChannelDiscordFeature } from "@cosmicdrift/kumiko-bundled-features/channel-discord"`; requires `delivery`, `secrets`
33
+ - `channel-email`: Wires an `EmailTransport` (typically `mail-transport-smtp` in production, `createInMemoryTransport()` in tests) into the delivery system as the `email` channel.
34
+ `import { createChannelEmailFeature } from "@cosmicdrift/kumiko-bundled-features/channel-email"`; requires `delivery`
35
+ - `channel-in-app`: Persists notifications to an in-app inbox table so users can page through them via `queries.inbox` (cursor, total, parsed `target`) and track unread state with `handlers.markRead` / `handlers.markAllRead` and `queries.unreadCount`.
36
+ `import { createChannelInAppFeature } from "@cosmicdrift/kumiko-bundled-features/channel-in-app"`; requires `delivery`
37
+ - `channel-push`: Delivers push notifications through a `PushTransport` (bring your own FCM/APNs adapter or use `createInMemoryPushTransport()` for tests) registered as the `push` channel in the delivery system.
38
+ `import { createChannelPushFeature } from "@cosmicdrift/kumiko-bundled-features/channel-push"`; requires `delivery`
39
+ - `channel-slack`: Posts delivery notifications to a Slack incoming webhook, registered as the `slack` channel in the delivery system.
40
+ `import { createChannelSlackFeature } from "@cosmicdrift/kumiko-bundled-features/channel-slack"`; requires `delivery`, `secrets`
41
+ - `channel-teams`: Posts delivery notifications to a Microsoft Teams webhook (Workflows or connector) as an Adaptive Card, registered as the `teams` channel in the delivery system.
42
+ `import { createChannelTeamsFeature } from "@cosmicdrift/kumiko-bundled-features/channel-teams"`; requires `delivery`, `secrets`
43
+ - `channel-telegram`: Sends delivery notifications through the Telegram Bot API (`sendMessage`, plain text), registered as the `telegram` channel in the delivery system.
44
+ `import { createChannelTelegramFeature } from "@cosmicdrift/kumiko-bundled-features/channel-telegram"`; requires `delivery`, `secrets`; secrets `channel-telegram:secret:bot-token`
45
+ - `compliance-profiles`: Lets each tenant select a compliance regime (e.g. `eu-dsgvo`, `swiss-dsg`, `de-hr-dsgvo-hgb`) that bundles user-rights grace periods, breach-disclosure deadlines, sub-processor requirements, and audit-retention rules into a single named profile.
46
+ `import { createComplianceProfilesFeature } from "@cosmicdrift/kumiko-bundled-features/compliance-profiles"`
47
+ - `compliance-profiles-ops`: Platform-wide SystemAdmin counterpart to `compliance-profiles`' `needs-profile` query (#2089): `tenants-missing-profile` lists every enabled tenant with no row in `tenantComplianceProfile` at all, tenant-wide instead of scoped to the caller's own tenant.
48
+ `import { createComplianceProfilesOpsFeature } from "@cosmicdrift/kumiko-bundled-features/compliance-profiles-ops"`; requires `compliance-profiles`, `tenant`
49
+ - `config`: Stores per-tenant (and optionally per-user) configuration values with a multi-layer cascade: user-row → tenant-row → system-row → app-override (deploy-time `AppConfigOverrides`) → computed → feature default.
50
+ `import { createConfigFeature } from "@cosmicdrift/kumiko-bundled-features/config"`
51
+ - `crypto-shredding`: Operator-level crypto-shredding trigger.
52
+ `import { createCryptoShreddingFeature } from "@cosmicdrift/kumiko-bundled-features/crypto-shredding"`
53
+ - `custom-fields`: Tenant- and system-scoped custom field definitions with generic value storage on any host entity.
54
+ `import { createCustomFieldsFeature } from "@cosmicdrift/kumiko-bundled-features/custom-fields"`
55
+ - `data-retention`: Resolves the effective retention policy for any entity using a 3-layer stack: entity-level default → tenant preset (`dsgvo-basic`, `dsgvo-hgb`, `swiss-dsg`) → per-tenant override stored in `tenantRetentionOverride`.
56
+ `import { createDataRetentionFeature } from "@cosmicdrift/kumiko-bundled-features/data-retention"`
57
+ - `delivery`: The notification dispatch core: call `ctx.notify(notificationType, { to, route, data, priority, idempotencyKey })` from any handler to fan out a notification across all registered channels (email, in-app, push).
58
+ `import { createDeliveryFeature } from "@cosmicdrift/kumiko-bundled-features/delivery"`; requires `tenant`
59
+ - `derivatives-sharp`: Registers an `image/*` renderer against the `derivativeRenderer` extension point declared by `file-derivatives`, backed by sharp.
60
+ `import { createDerivativesSharpFeature } from "@cosmicdrift/kumiko-bundled-features/derivatives-sharp"`; requires `file-derivatives`
61
+ - `document-designer`: Template overview and layout editor: lists the mail, PDF and layout templates of a tenant (platform admins see the system templates and how many tenants customised each) and edits the mail layout and the PDF letterhead in the browser.
62
+ `import { createDocumentDesignerFeature } from "@cosmicdrift/kumiko-document-designer"`; requires `template-resolver`
63
+ - `document-ingest-foundation`: Shared PDF/Scan/Image → normalized-text ingest primitive.
64
+ `import { createDocumentIngestFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/document-ingest-foundation"`; requires `config`, `tenant-lifecycle`
65
+ - `documents`: Filed documents with title, category, issue and expiry date and one file, shown on the records they are linked to.
66
+ `import { createDocumentsFeature } from "@cosmicdrift/kumiko-bundled-features/documents"`; requires `files`
67
+ - `feature-toggles`: Persists per-feature enabled/disabled state in the `store_global_feature_state` table and exposes a `set` write-handler plus `list`/`registered` query-handlers so operators can flip features at runtime without redeploying.
68
+ `import { createFeatureTogglesFeature } from "@cosmicdrift/kumiko-bundled-features/feature-toggles"`
69
+ - `file-derivatives`: Declares the `derivativeRenderer` extension point.
70
+ `import { createFileDerivativesFeature } from "@cosmicdrift/kumiko-bundled-features/file-derivatives"`; requires `file-foundation`, `files`
71
+ - `file-foundation`: Defines the `fileProvider` extension point and a per-tenant `provider` config key that selects which registered storage plugin to use at runtime.
72
+ `import { createFileFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/file-foundation"`; requires `config`
73
+ - `file-provider-inmemory`: Registers an in-process `"inmemory"` provider for `file-foundation` that stores file bytes per tenant in a module-level Map.
74
+ `import { createFileProviderInMemoryFeature } from "@cosmicdrift/kumiko-bundled-features/file-provider-inmemory"`; requires `file-foundation`
75
+ - `file-provider-s3`: Registers itself as the `"s3"` provider for `file-foundation` and owns the per-tenant config keys (`bucket`, `region`, `endpoint`, `forcePathStyle`, `accessKeyId`) and the encrypted `s3.secretAccessKey` secret.
76
+ `import { createFileProviderS3Feature, createFileProviderS3NamedEnvFeature } from "@cosmicdrift/kumiko-bundled-features/file-provider-s3"`; requires `config`, `secrets`, `file-foundation`; secrets `file-provider-s3:secret:s3-secret-access-key`
77
+ - `file-provider-s3-env`: Registers an `"s3-env"` provider for `file-foundation` that reads one S3 credential set from `process.env` (`S3_BUCKET`/`S3_REGION`/`S3_ACCESS_KEY`/`S3_SECRET_KEY`, optional `S3_ENDPOINT`/`S3_FORCE_PATH_STYLE`) and serves every tenant from one shared bucket — no per-tenant config or secret seeding.
78
+ `import { createFileProviderS3Feature } from "@cosmicdrift/kumiko-bundled-features/file-provider-s3"`, call `createFileProviderS3Feature({ fromEnv: true })`; requires `file-foundation`
79
+ - `files`: Exposes the `fileRef` entity from the framework core so that uploaded files — tracked in the `file_refs` table by `createFileRoutes` — participate in cross-feature hooks: `user-data-rights-defaults` automatically includes file blobs in GDPR exports and forget flows, and tenant-lifecycle cleanup deletes all refs on tenant destroy.
80
+ `import { createFilesFeature } from "@cosmicdrift/kumiko-framework/files"`
81
+ - `files-tenant-data`: GDPR coverage for the `files` feature's `fileRef` entity: tenant-destroy row purge (EXT_TENANT_DATA) and full storage-prefix binary wipe (EXT_STORAGE_PROVIDER), plus a manual backfill/GC job that sweeps derivatives orphaned by a forget/tenant-destroy that ran before #2461 wired binary cleanup into those flows.
82
+ `import { createFilesTenantDataFeature } from "@cosmicdrift/kumiko-bundled-features/files-tenant-data"`; requires `tenant-lifecycle`
83
+ - `folders`: Generic, host-agnostic hierarchical folders for any entity.
84
+ `import { createFoldersFeature } from "@cosmicdrift/kumiko-bundled-features/folders"`
85
+ - `folders-user-data`: GDPR (Art.
86
+ `import { createFoldersUserDataFeature } from "@cosmicdrift/kumiko-bundled-features/folders-user-data"`; requires `user-data-rights`
87
+ - `form-draft`: Per-user, per-tenant working copy of an in-progress form, saved BEFORE the real domain entity exists.
88
+ `import { createFormDraftFeature } from "@cosmicdrift/kumiko-bundled-features/form-draft"`
89
+ - `form-draft-user-data`: GDPR (Art.
90
+ `import { createFormDraftUserDataFeature } from "@cosmicdrift/kumiko-bundled-features/form-draft-user-data"`; requires `user-data-rights`
91
+ - `host-routing`: Host- and path-based tenant routing: an authoritative tenant resolver from the subdomain (tenantBy host), a tenant existence check against enabled, active tenants, and the tenant-key query behind createHostDispatch, which picks the HTML file, CSP, locale redirect or 404 per host for runProdApp's hostDispatch.
92
+ `import { createHostRoutingFeature } from "@cosmicdrift/kumiko-bundled-features/host-routing"`; requires `auth-foundation`, `tenant`
93
+ - `inbound-mail-foundation`: Plugin host for inbound e-mail (inbox federation) — provider plugins (inbound-provider-imap, later M365 Graph / Gmail REST) register at the `inboundMailProvider` extension point with verify/fetch and optional oauth/watch (live push) and send.
94
+ `import { createInboundMailFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/inbound-mail-foundation"`; requires `tenant-lifecycle`
95
+ - `inbound-provider-imap`: Registers the `"imap"` provider for `inbound-mail-foundation` using imapflow — password/app-password auth covers classic IMAP hosts without any OAuth (plus an XOAUTH2 fallback consuming an access token from the secret slot).
96
+ `import { createInboundProviderImapFeature } from "@cosmicdrift/kumiko-bundled-features/inbound-provider-imap"`; requires `inbound-mail-foundation`, `secrets`
97
+ - `inbound-provider-inmemory`: Registers a scriptable in-process `"inmemory"` provider for `inbound-mail-foundation`.
98
+ `import { createInboundProviderInMemoryFeature } from "@cosmicdrift/kumiko-bundled-features/inbound-provider-inmemory"`; requires `inbound-mail-foundation`
99
+ - `jobs`: Persistence and operator tooling for background jobs registered via `r.job(...)`.
100
+ `import { createJobsFeature } from "@cosmicdrift/kumiko-bundled-features/jobs"`
101
+ - `ledger`: Double-entry bookkeeping primitive.
102
+ `import { createLedgerFeature } from "@cosmicdrift/kumiko-bundled-features/ledger"`
103
+ - `legal-pages`: Opt-in wrapper around `template-resolver` text-blocks that registers public HTML routes (default: `/legal/impressum`, `/legal/datenschutz`, `/legal/imprint`, `/legal/privacy`) with Markdown-to-HTML rendering and a boot-time job that hard-fails in production when the required blocks (default: `imprint/de`, `privacy/de`) are not seeded in `SYSTEM_TENANT`.
104
+ `import { createLegalPagesFeature } from "@cosmicdrift/kumiko-bundled-features/legal-pages"`; requires `template-resolver`
105
+ - `locale-de`: German UI and mail copy for framework screens.
106
+ `import { localeDe } from "@cosmicdrift/kumiko-locale-de"`
107
+ - `mail-foundation`: Defines the `mailTransport` extension point and a per-tenant `provider` config key that selects which registered transport plugin to use at runtime.
108
+ `import { createMailFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/mail-foundation"`; requires `config`
109
+ - `mail-transport-inmemory`: Registers an in-process `"inmemory"` provider for `mail-foundation` that buffers sent mails per tenant instead of contacting an SMTP server.
110
+ `import { createMailTransportInMemoryFeature } from "@cosmicdrift/kumiko-bundled-features/mail-transport-inmemory"`; requires `mail-foundation`
111
+ - `mail-transport-smtp`: Registers itself as the `"smtp"` provider for `mail-foundation` and owns the per-tenant config keys (`host`, `port`, `secure`, `from`, `authUser`) and the encrypted `smtp.password` secret.
112
+ `import { createMailTransportSmtpFeature } from "@cosmicdrift/kumiko-bundled-features/mail-transport-smtp"`; requires `config`, `secrets`, `mail-foundation`; secrets `mail-transport-smtp:secret:smtp-password`
113
+ - `managed-pages`: Tenant-editable, server-rendered public pages with per-tenant branding.
114
+ `import { createManagedPagesFeature } from "@cosmicdrift/kumiko-bundled-features/managed-pages"`; requires `config`
115
+ - `metrics`: Declarative tenant-scoped metrics: each metric declaration becomes an admin-only query that aggregates a read-model table for the caller's own tenant and returns the shape dashboard panels consume (stat value with period delta, time series, ranked rows).
116
+ `import { createMetricsFeature } from "@cosmicdrift/kumiko-bundled-features/metrics"`; requires `jobs`, `delivery`, `sessions`
117
+ - `metrics-system`: Declarative platform-wide metrics for SystemAdmin: each metric declaration becomes a query that aggregates a read-model table across all tenants (optionally narrowed to one tenant) and returns the shape dashboard panels consume.
118
+ `import { createSystemMetricsFeature } from "@cosmicdrift/kumiko-bundled-features/metrics"`; requires `jobs`, `delivery`, `sessions`, `tenant`
119
+ - `notes-history`: Generic, host-agnostic, append-only note history for any entity.
120
+ `import { createNotesHistoryFeature } from "@cosmicdrift/kumiko-bundled-features/notes-history"`
121
+ - `notes-history-user-data`: GDPR (Art.
122
+ `import { createNotesHistoryUserDataFeature } from "@cosmicdrift/kumiko-bundled-features/notes-history-user-data"`; requires `user-data-rights`
123
+ - `personal-access-tokens`: Long-lived, revocable Personal Access Tokens for headless HTTP-API access.
124
+ `import { createPersonalAccessTokensFeature } from "@cosmicdrift/kumiko-bundled-features/personal-access-tokens"`; requires `user`, `tenant`, `auth-foundation`
125
+ - `rate-limiting`: Adds an ops-side `rate-limiting:query:status` query handler for inspecting current bucket state; the actual request throttling is wired automatically by the dispatcher when any handler declares a `rateLimit` option (e.g. `{ per: 'user', limit: 3, windowSeconds: 60 }`) or when you pass `context.rateLimit` to `buildServer`.
126
+ `import { createRateLimitingFeature } from "@cosmicdrift/kumiko-bundled-features/rate-limiting"`
127
+ - `readiness`: One-call tenant-onboarding probe: `readiness:query:status` rolls up every config key and secret declared `required: true` across all mounted features and reports which still lack a usable value for the calling tenant, plus a single `ready` boolean.
128
+ `import { createReadinessFeature } from "@cosmicdrift/kumiko-bundled-features/readiness"`; requires `config`, `secrets`
129
+ - `renderer-foundation`: Plugin registry for content rendering (mail HTML, PDF, images): call `foundation.createRendererForTenant({ tenantId, kind })` at render time to get the right renderer plugin selected by kind, with tenant-level overrides via the `rendererPluginByKind` config key.
130
+ `import { createRendererFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/renderer-foundation"`; requires `template-resolver`
131
+ - `renderer-mail-html`: A `renderer-foundation` plugin that handles `RenderRequest { kind: "mail" }` and returns `{ subject, html, text, previewText?
132
+ `import { createRendererMailHtmlFeature } from "@cosmicdrift/kumiko-renderer-mail-html"`; requires `renderer-foundation`, `template-resolver`
133
+ - `renderer-pdf`: Renderer plugin for `kind="document-pdf"` that draws a table document (title, meta lines, weighted columns, repeating header, page footer) as a PDF in pure JavaScript, without a browser or Puppeteer service.
134
+ `import { createRendererPdfFeature } from "@cosmicdrift/kumiko-bundled-features/renderer-pdf"`; requires `renderer-foundation`
135
+ - `renderer-puppeteer-client`: A `renderer-foundation` plugin that prints documents to PDF through a separately deployed `puppeteer-service`.
136
+ `import { createRendererPuppeteerClientFeature } from "@cosmicdrift/kumiko-renderer-puppeteer-client"`; requires `renderer-foundation`
137
+ - `secrets`: Stores arbitrary per-tenant secrets (API keys, tokens, credentials) encrypted at rest using AES-256 with a KEK loaded from `KUMIKO_SECRETS_MASTER_KEY_V1` (and successive versions for rotation).
138
+ `import { createSecretsFeature } from "@cosmicdrift/kumiko-bundled-features/secrets"`
139
+ - `seo`: Site-discovery + SEO/AEO/GEO surface for apex/content pages.
140
+ `import { createSeoFeature } from "@cosmicdrift/kumiko-bundled-features/seo"`; requires `config`
141
+ - `sessions`: Tracks signed-in clients in the `store_user_sessions` table (one row per JWT, keyed by the `sid`/`jti` claim) and exposes handlers for `mine` (list your sessions), `revoke`, and `revokeAllOthers`.
142
+ `import { createSessionsFeature } from "@cosmicdrift/kumiko-bundled-features/sessions"`; requires `user`, `auth-foundation`
143
+ - `step-dispatcher`: Internal system feature that drains deferred Tier-2 side-effects (currently `webhook.send` and `mail.send`) after their originating transaction commits.
144
+ `import { createStepDispatcherFeature } from "@cosmicdrift/kumiko-bundled-features/step-dispatcher"`; requires `secrets`
145
+ - `subscription-mollie`: Mollie payment provider plugin for `billing-foundation`, covering the DACH/EU mid-market use case.
146
+ `import { createSubscriptionMollieFeature } from "@cosmicdrift/kumiko-bundled-features/subscription-mollie"`; requires `billing-foundation`
147
+ - `subscription-paddle`: Paddle Billing provider plugin for `billing-foundation`.
148
+ `import { createSubscriptionPaddleFeature } from "@cosmicdrift/kumiko-bundled-features/subscription-paddle"`; requires `billing-foundation`, `config`, `secrets`; config without default `api-key`, `client-token`, `tenant-binding-key`, `webhook-secret`
149
+ - `subscription-stripe`: Stripe payment provider plugin for `billing-foundation`.
150
+ `import { createSubscriptionStripeFeature } from "@cosmicdrift/kumiko-bundled-features/subscription-stripe"`; requires `billing-foundation`, `config`, `secrets`; config without default `api-key`, `webhook-secret`
151
+ - `tags`: Generic, host-agnostic tagging for any entity.
152
+ `import { createTagsFeature } from "@cosmicdrift/kumiko-bundled-features/tags"`
153
+ - `template-resolver`: Every piece of editable text lives here, in one entity: mail bodies, notification texts, PDF document templates, AI prompts and plain text blocks.
154
+ `import { createTemplateResolverFeature } from "@cosmicdrift/kumiko-bundled-features/template-resolver"`
155
+ - `template-resolver-user-data`: GDPR (Art.
156
+ `import { createTemplateResolverUserDataFeature } from "@cosmicdrift/kumiko-bundled-features/template-resolver-user-data"`; requires `user-data-rights`
157
+ - `tenant`: Registers the three core multi-tenancy entities — `tenant`, `tenant-membership`, and `tenant-invitation` (DB tables `read_tenants`, `read_tenant_memberships`, and `read_tenant_invitations`) — along with write handlers for create/update/disable/enable/addMember/removeMember/updateMemberRoles and the matching queries.
158
+ `import { createTenantFeature } from "@cosmicdrift/kumiko-bundled-features/tenant"`; requires `config`; config without default `smtp-host`, `smtp-pass`
159
+ - `tenant-handover`: Try-before-signup ownership handover: claims the rows of a declared-transferable entity graph (root plus its parentRef-linked children) from an anonymous/public tenant into the caller's own tenant, legitimized by a row-bound grant the anonymous flow minted.
160
+ `import { createTenantHandoverFeature } from "@cosmicdrift/kumiko-bundled-features/tenant-handover"`
161
+ - `tenant-lifecycle`: Tenant-destroy lifecycle: request/cancel destruction with compliance-profile grace, auth 410 gate for teardown states, cron trigger after grace, and staged destroy runner (extension fan-out, subject-key erase, tenant tombstone).
162
+ `import { createTenantLifecycleFeature } from "@cosmicdrift/kumiko-bundled-features/tenant-lifecycle"`; requires `tenant`, `compliance-profiles`
163
+ - `tenant-settings`: Per-tenant Currency + Locale defaults, exposed as two config keys (tenant-settings:config:currency, tenant-settings:config:locale) that surface in the self-populating Settings-Hub via `mask`.
164
+ `import { createTenantSettingsFeature } from "@cosmicdrift/kumiko-bundled-features/tenant-settings"`; requires `config`
165
+ - `tier-engine`: Stores a `tier-assignment` entity per tenant (which pricing tier is active) and, when configured with a `TierMap`, registers itself as the `tenantTierResolver` extension so the dispatcher automatically gates `r.toggleable()` features per tenant based on their assigned tier.
166
+ `import { createTierEngineFeature } from "@cosmicdrift/kumiko-bundled-features/tier-engine"`; requires `config`, `tenant`
167
+ - `user`: Manages the cross-tenant user identity: the `read_users` table holds each user's email, `displayName`, global `roles`, `emailVerified` flag, and lifecycle `status` (active / restricted / deletionRequested / deleted).
168
+ `import { createUserFeature } from "@cosmicdrift/kumiko-bundled-features/user"`
169
+ - `user-data-rights`: Implements GDPR Art.
170
+ `import { createUserDataRightsFeature } from "@cosmicdrift/kumiko-bundled-features/user-data-rights"`; requires `user`, `data-retention`, `compliance-profiles`, `sessions`
171
+ - `user-data-rights-defaults`: Registers ready-made `EXT_USER_DATA` export and delete hooks for the bundled entities that hold per-user data: `user` (delete strategy sets email to `deleted-<id>@anonymized.invalid`, nulls `passwordHash`, sets status to `Deleted`; anonymize strategy sets email to `anonymized-<id>@anonymized.invalid` without touching `passwordHash`), `fileRef` (delete removes both the DB row and the storage binary), plus — gated on the source feature being mounted — `user-session` (ip/userAgent, hard-delete), `api-token` (hard-delete = revoke), `in-app-message` (hard-delete), `tenant-invitation` (invitee email forgotten/pseudonymized, inviter link severed), `notification-preference` and user-scoped `config-value` (purged via the forget verb), plus export-only hooks for the events-only aggregates `delivery-attempt` (recipientAddress) and `job-run` (payload) whose erasure runs via crypto-shredding, and `download-attempt` (rejected export-download attempts of the user: result, path, source IP, user agent, time; delete purges the user's own attempts via the forget verb, token-path rows without an owner expire by retention).
172
+ `import { createUserDataRightsDefaultsFeature } from "@cosmicdrift/kumiko-bundled-features/user-data-rights-defaults"`; requires `user`, `files`, `user-data-rights`
173
+ - `user-profile`: Self-service account page (fw#2312: declarative `projectionDetail` bound to the signed-in user's own `me` row) plus its `change-email` write handler (re-auth via current password, uniqueness check, resets emailVerified and expects the app to trigger the verification flow; opt out with `allowEmailChange: false`, which drops handler and section).
174
+ `import { createUserProfileFeature } from "@cosmicdrift/kumiko-bundled-features/user-profile"`; requires `user`, `auth-email-password`, `user-data-rights`, `user-data-rights-defaults`
175
+ - `waitlist`: Public waitlist: an anonymous, rate-limited `submit` handler collects name, email and optional company, portfolio and message (a honeypot field and per-email dedupe keep bots and repeat submits out, and the response body is identical whether or not an address was already listed), and mails the submitter a confirmation.
176
+ `import { createWaitlistFeature } from "@cosmicdrift/kumiko-bundled-features/waitlist"`; requires `tenant`, `auth-email-password`, `user-data-rights`
177
+ - `workflow-runner`: Writes the run-envelope (`workflow.run-started` / `workflow.run-completed` / `workflow.run-failed`) for a workflow run onto its `workflow-run` aggregate stream.
178
+ `import { createWorkflowRunnerFeature } from "@cosmicdrift/kumiko-bundled-features/workflow-runner"`
@@ -124,7 +124,7 @@ Requires: `delivery`
124
124
 
125
125
  ## channel-in-app
126
126
 
127
- Persists notifications to an in-app inbox table so users can retrieve them via `handlers.inbox` and track unread state with `handlers.markRead` / `handlers.markAllRead` and `queries.unreadCount`. Requires `delivery`; no external service needed — messages are stored in the app's own database.
127
+ Persists notifications to an in-app inbox table so users can page through them via `queries.inbox` (cursor, total, parsed `target`) and track unread state with `handlers.markRead` / `handlers.markAllRead` and `queries.unreadCount`. A notification links to a screen when `data.target` is set with `inAppTarget(screenId, entityId?)`. The renderer shell shows a bell with a live unread badge whenever this feature is mounted. Requires `delivery`; no external service needed — messages are stored in the app's own database.
128
128
 
129
129
  Category: notifications
130
130
 
@@ -253,6 +253,14 @@ Config keys:
253
253
  | `max-pages-per-file` | number | tenant | `50` |
254
254
  | `ocr-language` | text | tenant | `deu+eng` |
255
255
 
256
+ ## documents
257
+
258
+ Filed documents with title, category, issue and expiry date and one file, shown on the records they are linked to. A document is visible only through a linked record the viewer can read, and only while the record that owns its file is readable too, so linking never widens access to the file. Documents arrive through capture rules (a file saved into a configured field) or through the system-only import for migrations, which keeps the caller's id, refuses an id another record already uses, and stamps createdAt with the import time (issuedAt carries the business date; the event store does not backdate); users link, unlink, edit and delete them. Read a record's documents with for-record, count expired and expiring ones with expiry-overview, list them with an expiry state and an expiring filter with overview (the documents list screen), and add the documents tab to a record screen with documentsTab().
259
+
260
+ Category: data
261
+
262
+ Requires: `files`
263
+
256
264
  ## feature-toggles
257
265
 
258
266
  Persists per-feature enabled/disabled state in the `store_global_feature_state` table and exposes a `set` write-handler plus `list`/`registered` query-handlers so operators can flip features at runtime without redeploying. Each API instance keeps an in-memory `GlobalFeatureToggleRuntime` snapshot (initialize it via `createFeatureToggleRuntime`, pass a `() => runtime` accessor to `createFeatureTogglesFeature`) that the dispatcher gate reads on every request; a `toggle-cache-sync` multi-stream projection with `delivery: "shared"` syncs the snapshot across instances whenever a `toggle-set` event is appended.
@@ -2,5 +2,7 @@ import type { AccessRule } from "@cosmicdrift/kumiko-types/handlers";
2
2
  import type { OwnershipMap } from "@cosmicdrift/kumiko-types/ownership";
3
3
  import type { Registry } from "../engine/types/index.ts";
4
4
  export declare function assertNoAggregateTypeKeyCollisions(registry: Registry): void;
5
+ export declare function collectEntityDetailQueries(registry: Registry): ReadonlyMap<string, string>;
6
+ export declare function collectEntityDetailAccess(registry: Registry): ReadonlyMap<string, AccessRule>;
5
7
  export declare function collectEntityReadAccess(registry: Registry): ReadonlyMap<string, readonly AccessRule[]>;
6
8
  export declare function collectEntityRowReadMaps(registry: Registry): ReadonlyMap<string, OwnershipMap>;
@@ -14,6 +14,47 @@ export function assertNoAggregateTypeKeyCollisions(registry) {
14
14
  }
15
15
  }
16
16
  }
17
+ // Only the "<entity>:detail" handler counts: other "<entity>:...:detail"
18
+ // queries map to the same entity but may return a different shape or scope.
19
+ // Entity names are globally unique and only the owning feature maps handlers
20
+ // to its entity, so at most one QN matches per entity.
21
+ function isEntityDetailQuery(qn, entityName) {
22
+ return qn.endsWith(`:query:${toKebab(entityName)}:detail`);
23
+ }
24
+ export function collectEntityDetailQueries(registry) {
25
+ const detailQueryByEntity = new Map();
26
+ for (const qn of registry.getAllQueryHandlers().keys()) {
27
+ const entityName = registry.getHandlerEntity(qn);
28
+ if (entityName !== undefined && isEntityDetailQuery(qn, entityName)) {
29
+ detailQueryByEntity.set(entityName, qn);
30
+ }
31
+ }
32
+ for (const projection of registry.getAllProjections().values()) {
33
+ const entityName = projection.source;
34
+ if (typeof entityName !== "string")
35
+ continue;
36
+ const owner = registry.getReadModelEntityOwner(entityName);
37
+ if (owner === undefined)
38
+ continue;
39
+ const qn = `${owner}:query:${toKebab(entityName)}:detail`;
40
+ if (registry.getAllQueryHandlers().has(qn))
41
+ detailQueryByEntity.set(entityName, qn);
42
+ }
43
+ return detailQueryByEntity;
44
+ }
45
+ // The access rule of each entity's own detail query. The file route and the read gate both ask
46
+ // "may this viewer open the record?" through this one rule, so a liveEntities or openToAll query
47
+ // that merely names an entity never widens what a gated row exposes.
48
+ export function collectEntityDetailAccess(registry) {
49
+ const handlers = registry.getAllQueryHandlers();
50
+ const accessByEntity = new Map();
51
+ for (const [entityName, qn] of collectEntityDetailQueries(registry)) {
52
+ const handler = handlers.get(qn);
53
+ if (handler !== undefined)
54
+ accessByEntity.set(entityName, handler.access);
55
+ }
56
+ return accessByEntity;
57
+ }
17
58
  // Access rules of the query handlers that let a role read an entity (the
18
59
  // handler's own entity or one it declares via liveEntities). A live signal
19
60
  // carries the id of every row of the entity, so a signed-in connection gets it
@@ -45,8 +86,11 @@ export function collectEntityRowReadMaps(registry) {
45
86
  assertNoAggregateTypeKeyCollisions(registry);
46
87
  const mapsByEntity = new Map();
47
88
  for (const [entityName, entity] of registry.getAllEntities()) {
48
- const readMap = entity.access?.read;
49
- if (readMap === undefined || Object.keys(readMap).length === 0)
89
+ // A read gate narrows rows for every role, so no role reads every row: signals stay anonymous.
90
+ const readMap = entity.readGate !== undefined ? {} : entity.access?.read;
91
+ if (readMap === undefined)
92
+ continue;
93
+ if (entity.readGate === undefined && Object.keys(readMap).length === 0)
50
94
  continue;
51
95
  for (const aggregateType of new Set([entityName, toKebab(entityName)])) {
52
96
  mapsByEntity.set(aggregateType, readMap);
@@ -1,5 +1,5 @@
1
1
  import { EXT_FILE_PROVIDER, EXT_PRINCIPAL_STATUS } from "../engine/extension-names.js";
2
- import { isFileField } from "../engine/field-helpers.js";
2
+ import { holdsFileRefs } from "../files/file-field-slots.js";
3
3
  import { SHARED_INSTANCE_SENTINEL } from "../pipeline/event-consumer-state.js";
4
4
  import { generateId } from "../utils/index.js";
5
5
  // A payload-bucket handler without a resolver would answer every call with a
@@ -125,7 +125,7 @@ function registryDeclaresFileFields(registry) {
125
125
  for (const feature of registry.features.values()) {
126
126
  for (const entity of Object.values(feature.entities ?? {})) {
127
127
  for (const field of Object.values(entity.fields)) {
128
- if (isFileField(field))
128
+ if (holdsFileRefs(field))
129
129
  return true;
130
130
  }
131
131
  }
@@ -2,6 +2,7 @@ import { createProjectingTenantDb } from "../db/tenant-db.js";
2
2
  import { buildAppSchema } from "../engine/build-app-schema.js";
3
3
  import { projectAppSchemaForRoles } from "../engine/project-app-schema-for-roles.js";
4
4
  import { resolveDisabledFeatureNames, stripDisabledFeaturesFromAppSchema, } from "../engine/strip-disabled-features-from-app-schema.js";
5
+ import { stripUnreadableRelatedLists } from "../engine/strip-unreadable-related-lists.js";
5
6
  import { createAnonymousUser, isAnonymousSessionUser } from "../engine/system-user.js";
6
7
  import { createFileRoutes, readFilesRouteOptions } from "../files/file-routes.js";
7
8
  import { createRecordReadCheck } from "../files/record-read-check.js";
@@ -109,7 +110,8 @@ export function mountSchemaRoute(app, options) {
109
110
  searchAdapterMissing: !options.context.searchAdapter,
110
111
  });
111
112
  }
112
- const projected = projectAppSchemaForRoles(stripDisabledFeaturesFromAppSchema(fullSchema, options.registry, disabledFeatureNames), user?.roles ?? []);
113
+ const roles = user?.roles ?? [];
114
+ const projected = stripUnreadableRelatedLists(projectAppSchemaForRoles(stripDisabledFeaturesFromAppSchema(fullSchema, options.registry, disabledFeatureNames), roles), roles, (queryQn) => options.registry.getQueryHandler(queryQn)?.access);
113
115
  const json = JSON.stringify(projected);
114
116
  cached = { json, etag: computeStrongEtag(json) };
115
117
  projectedSchemaCache.set(roleKey, cached);
@@ -0,0 +1,23 @@
1
+ import type { ListRuntimeOptions } from "@cosmicdrift/kumiko-types/event-store-executor-types";
2
+ import { type EntityDefinition } from "@cosmicdrift/kumiko-types/fields";
3
+ import type { SessionUser } from "../engine/types/index.ts";
4
+ import type { Table } from "./event-store-executor-context.ts";
5
+ import type { FieldReadClause } from "./event-store-executor-read-where.ts";
6
+ import type { TenantDb } from "./tenant-db.ts";
7
+ type SqlBuilder = (params: unknown[]) => string;
8
+ export type DerivedSelect = {
9
+ readonly projections: (params: unknown[]) => string;
10
+ readonly sortExpr: (name: string) => SqlBuilder | undefined;
11
+ readonly takeValues: (rawRows: readonly Record<string, unknown>[]) => readonly Record<string, unknown>[];
12
+ };
13
+ type DerivedSelectInput = {
14
+ readonly entity: EntityDefinition;
15
+ readonly table: Table;
16
+ readonly tableName: string;
17
+ readonly fieldReadClause: FieldReadClause;
18
+ readonly user: SessionUser;
19
+ readonly db: TenantDb;
20
+ readonly runtime: ListRuntimeOptions | undefined;
21
+ };
22
+ export declare function buildDerivedSelect(input: DerivedSelectInput): DerivedSelect | undefined;
23
+ export {};
@@ -0,0 +1,115 @@
1
+ import { isSqlDerivedField, } from "@cosmicdrift/kumiko-types/fields";
2
+ import { parseRefTargetEntityName } from "../engine/parse-ref-target.js";
3
+ import { buildRelatedSubquery } from "./derived-related-sql.js";
4
+ import { coerceDriverValueOfPgType } from "./driver-row-coercion.js";
5
+ import { holdsCiphertextOrSecret } from "./entity-field-encryption.js";
6
+ import { FRAMEWORK_DEFAULT_CURRENCY, readMoney } from "./money.js";
7
+ import { physicalColumnName } from "./table-builder.js";
8
+ const DERIVED_ALIAS_PREFIX = "__kumiko_derived_";
9
+ // Driver value coercion by the pg type a stored column of this value type would have.
10
+ const PG_TYPE_OF_VALUE_TYPE = {
11
+ text: "text",
12
+ number: "integer",
13
+ decimal: "numeric",
14
+ boolean: "boolean",
15
+ date: "date",
16
+ timestamp: "timestamptz",
17
+ money: "bigint",
18
+ };
19
+ function buildCoalesceExpression(fields, input) {
20
+ const { entity, table, tableName, fieldReadClause } = input;
21
+ const allReadable = fields.every((f) => Object.hasOwn(table, f) &&
22
+ fieldReadClause(f).kind === "pass" &&
23
+ !holdsCiphertextOrSecret(entity, f));
24
+ if (!allReadable)
25
+ return undefined;
26
+ const columns = fields.map((f) => `"${tableName}"."${physicalColumnName(table, f)}"`);
27
+ const sql = `COALESCE(${columns.join(", ")})`;
28
+ return () => sql;
29
+ }
30
+ function relatedDefaultCurrency(spec, input) {
31
+ const related = spec.kind === "related"
32
+ ? input.runtime?.derivedSql?.resolveEntity(parseRefTargetEntityName(spec.entity))
33
+ : undefined;
34
+ return related?.defaultCurrency ?? FRAMEWORK_DEFAULT_CURRENCY;
35
+ }
36
+ function buildExpression(spec, input, currency = false) {
37
+ if (spec.kind === "coalesce")
38
+ return buildCoalesceExpression(spec.fields, input);
39
+ const runtime = input.runtime?.derivedSql;
40
+ if (runtime === undefined)
41
+ return undefined;
42
+ const outer = {
43
+ entity: input.entity,
44
+ table: input.table,
45
+ tableName: input.tableName,
46
+ isFieldReadable: (field) => input.fieldReadClause(field).kind === "pass",
47
+ };
48
+ return buildRelatedSubquery({
49
+ spec,
50
+ outer,
51
+ user: input.user,
52
+ db: input.db,
53
+ runtime,
54
+ parentVisibility: input.runtime?.parentVisibility,
55
+ currency,
56
+ });
57
+ }
58
+ export function buildDerivedSelect(input) {
59
+ if (input.runtime?.derivedSql === undefined)
60
+ return undefined;
61
+ const plans = [];
62
+ for (const [name, def] of Object.entries(input.entity.derivedFields ?? {})) {
63
+ if (!isSqlDerivedField(def))
64
+ continue;
65
+ const alias = `${DERIVED_ALIAS_PREFIX}${plans.length}`;
66
+ plans.push({
67
+ name,
68
+ alias,
69
+ valueType: def.valueType,
70
+ sortable: def.sortable === true,
71
+ expression: buildExpression(def.sql, input),
72
+ currency: def.valueType === "money"
73
+ ? {
74
+ alias: `${alias}_currency`,
75
+ expression: buildExpression(def.sql, input, true),
76
+ fallback: relatedDefaultCurrency(def.sql, input),
77
+ }
78
+ : undefined,
79
+ });
80
+ }
81
+ if (plans.length === 0)
82
+ return undefined;
83
+ return {
84
+ projections: (params) => plans
85
+ .flatMap((plan) => [
86
+ { alias: plan.alias, expression: plan.expression },
87
+ ...(plan.currency === undefined ? [] : [plan.currency]),
88
+ ])
89
+ .map(({ alias, expression }) => `, ${expression?.(params) ?? "NULL"} AS "${alias}"`)
90
+ .join(""),
91
+ sortExpr: (name) => {
92
+ const plan = plans.find((p) => p.name === name);
93
+ return plan?.sortable === true ? plan.expression : undefined;
94
+ },
95
+ // Stripped before row coercion and the entity cache: the values depend on the viewer.
96
+ takeValues: (rawRows) => rawRows.map((raw) => {
97
+ const values = {};
98
+ for (const plan of plans) {
99
+ if (plan.currency !== undefined) {
100
+ const amount = raw[plan.alias];
101
+ // A stored money field without an amount has no key on the row; match that.
102
+ if (amount !== null && amount !== undefined) {
103
+ values[plan.name] = readMoney(plan.name, amount, raw[plan.currency.alias], plan.currency.fallback);
104
+ }
105
+ delete raw[plan.alias];
106
+ delete raw[plan.currency.alias];
107
+ continue;
108
+ }
109
+ values[plan.name] = coerceDriverValueOfPgType(raw[plan.alias], PG_TYPE_OF_VALUE_TYPE[plan.valueType], plan.alias);
110
+ delete raw[plan.alias];
111
+ }
112
+ return values;
113
+ }),
114
+ };
115
+ }
@@ -0,0 +1,31 @@
1
+ import type { DerivedSqlSpec, EntityDefinition } from "@cosmicdrift/kumiko-types/fields";
2
+ import type { SessionUser } from "../engine/types/index.ts";
3
+ import type { Table } from "./event-store-executor-context.ts";
4
+ import type { ParentVisibilityOption } from "./parent-ref-clause.ts";
5
+ import type { TenantDb } from "./tenant-db.ts";
6
+ export type RelatedDerivedSpec = Extract<DerivedSqlSpec, {
7
+ readonly kind: "related";
8
+ }>;
9
+ export type DerivedSqlRuntime = {
10
+ readonly resolveEntity: (entityName: string) => EntityDefinition | undefined;
11
+ readonly resolveTable: (entityName: string) => Table;
12
+ readonly canReadEntity: (entityName: string) => boolean;
13
+ readonly today: string;
14
+ readonly now: string;
15
+ };
16
+ export type OuterRow = {
17
+ readonly entity: EntityDefinition;
18
+ readonly table: Table;
19
+ readonly tableName: string;
20
+ readonly isFieldReadable: (field: string) => boolean;
21
+ };
22
+ export declare function buildRelatedSubquery(input: {
23
+ readonly spec: RelatedDerivedSpec;
24
+ readonly outer: OuterRow;
25
+ readonly user: SessionUser;
26
+ readonly db: TenantDb;
27
+ readonly runtime: DerivedSqlRuntime;
28
+ readonly parentVisibility: ParentVisibilityOption | undefined;
29
+ /** Select the measured money field's currency column instead of its amount. */
30
+ readonly currency?: boolean;
31
+ }): ((params: unknown[]) => string) | undefined;