@cosmicdrift/kumiko-framework 0.362.0 → 0.364.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 (259) hide show
  1. package/AGENTS.md +307 -0
  2. package/agents/examples/access-control.md +355 -0
  3. package/agents/examples/anonymous-access-multitenant.md +443 -0
  4. package/agents/examples/anonymous-access.md +300 -0
  5. package/agents/examples/apex-landing.md +528 -0
  6. package/agents/examples/apex-surface-auth.md +355 -0
  7. package/agents/examples/api-worker-split.md +434 -0
  8. package/agents/examples/auth-claims.md +351 -0
  9. package/agents/examples/auth-foundation-providers.md +155 -0
  10. package/agents/examples/auth-mfa.md +323 -0
  11. package/agents/examples/basic-entity.md +420 -0
  12. package/agents/examples/boot-check.md +122 -0
  13. package/agents/examples/chat-channels.md +254 -0
  14. package/agents/examples/compliance-profiles-demo.md +307 -0
  15. package/agents/examples/cross-feature-events.md +317 -0
  16. package/agents/examples/crypto-shredding-hr.md +314 -0
  17. package/agents/examples/currencies-global.md +609 -0
  18. package/agents/examples/currencies-per-tenant.md +453 -0
  19. package/agents/examples/custom-fields-basic.md +314 -0
  20. package/agents/examples/custom-handlers.md +397 -0
  21. package/agents/examples/delivery-notifications.md +432 -0
  22. package/agents/examples/designer-demo.md +86 -0
  23. package/agents/examples/e2e-generator.md +211 -0
  24. package/agents/examples/embedded-entity-form.md +598 -0
  25. package/agents/examples/embedded.md +649 -0
  26. package/agents/examples/encrypted-tenant-config.md +405 -0
  27. package/agents/examples/error-contract.md +539 -0
  28. package/agents/examples/event-sourcing.md +1287 -0
  29. package/agents/examples/feature-toggles.md +263 -0
  30. package/agents/examples/field-access.md +271 -0
  31. package/agents/examples/files-post-processing.md +433 -0
  32. package/agents/examples/i18n.md +166 -0
  33. package/agents/examples/idempotency.md +179 -0
  34. package/agents/examples/inbound-mail-triage.md +315 -0
  35. package/agents/examples/lane-routing.md +246 -0
  36. package/agents/examples/ledger-banking.md +214 -0
  37. package/agents/examples/ledger-invoicing.md +235 -0
  38. package/agents/examples/legal-pages.md +393 -0
  39. package/agents/examples/lifecycle-hooks.md +368 -0
  40. package/agents/examples/managed-config.md +428 -0
  41. package/agents/examples/managed-pages.md +353 -0
  42. package/agents/examples/notes-history-basic.md +345 -0
  43. package/agents/examples/ownership.md +396 -0
  44. package/agents/examples/pdf-audit-export.md +270 -0
  45. package/agents/examples/pipeline-basics.md +792 -0
  46. package/agents/examples/public-share-token.md +351 -0
  47. package/agents/examples/rate-limiting.md +211 -0
  48. package/agents/examples/realtime-sse.md +187 -0
  49. package/agents/examples/record-detail-layout.md +297 -0
  50. package/agents/examples/reference-data.md +151 -0
  51. package/agents/examples/relations.md +280 -0
  52. package/agents/examples/screens-nav.md +353 -0
  53. package/agents/examples/search.md +233 -0
  54. package/agents/examples/secrets-demo.md +417 -0
  55. package/agents/examples/security-baseline.md +187 -0
  56. package/agents/examples/seed-migration.md +99 -0
  57. package/agents/examples/session-revocation.md +259 -0
  58. package/agents/examples/state-machine.md +920 -0
  59. package/agents/examples/tags-basic.md +401 -0
  60. package/agents/examples/tenant-isolation.md +173 -0
  61. package/agents/examples/tenant-settings.md +275 -0
  62. package/agents/examples/tier-admin.md +347 -0
  63. package/agents/examples/timezones.md +366 -0
  64. package/agents/examples/user-data-rights.md +410 -0
  65. package/agents/examples/user-profile.md +226 -0
  66. package/agents/examples/webhook-step.md +597 -0
  67. package/agents/examples/wizard-form.md +379 -0
  68. package/agents/examples/workflow-engine.md +1731 -0
  69. package/agents/examples/writeform-section.md +300 -0
  70. package/agents/examples.md +74 -0
  71. package/agents/features.md +768 -0
  72. package/agents/widgets.md +181 -0
  73. package/dist/api/auth-middleware.d.ts +2 -0
  74. package/dist/api/auth-middleware.js +14 -5
  75. package/dist/api/auth-routes.js +5 -3
  76. package/dist/api/extra-route.d.ts +3 -0
  77. package/dist/api/index.d.ts +2 -2
  78. package/dist/api/index.js +1 -1
  79. package/dist/api/redis-sse-broker.js +10 -8
  80. package/dist/api/routes.js +48 -19
  81. package/dist/api/server.d.ts +2 -1
  82. package/dist/api/server.js +18 -6
  83. package/dist/api/session-cookies.d.ts +1 -0
  84. package/dist/api/session-cookies.js +20 -5
  85. package/dist/api/sse-route.js +16 -1
  86. package/dist/bun-db/index.d.ts +1 -1
  87. package/dist/bun-db/index.js +1 -1
  88. package/dist/bun-db/query.d.ts +12 -5
  89. package/dist/bun-db/query.js +88 -94
  90. package/dist/crypto/kek-source.js +5 -3
  91. package/dist/crypto/pii-field-encryption.d.ts +4 -1
  92. package/dist/crypto/pii-field-encryption.js +2 -2
  93. package/dist/db/apply-entity-event.js +2 -2
  94. package/dist/db/collect-table-metas.js +11 -7
  95. package/dist/db/cursor.js +2 -1
  96. package/dist/db/dialect.d.ts +1 -0
  97. package/dist/db/dialect.js +28 -13
  98. package/dist/db/eagerload.js +2 -2
  99. package/dist/db/entity-table-from-registry.js +1 -1
  100. package/dist/db/entity-table-meta.js +5 -13
  101. package/dist/db/event-store-executor-context.d.ts +5 -6
  102. package/dist/db/event-store-executor-context.js +29 -27
  103. package/dist/db/event-store-executor-read.js +40 -25
  104. package/dist/db/event-store-executor-write.js +18 -20
  105. package/dist/db/index.d.ts +1 -1
  106. package/dist/db/index.js +1 -1
  107. package/dist/db/lock-guard.d.ts +4 -0
  108. package/dist/db/lock-guard.js +14 -0
  109. package/dist/db/migrate-generator.js +6 -4
  110. package/dist/db/migrate-runner.js +32 -35
  111. package/dist/db/parent-ref-clause.js +2 -2
  112. package/dist/db/queries/backfill-pii.js +11 -9
  113. package/dist/db/queries/es-ops.js +1 -1
  114. package/dist/db/queries/event-consumer.js +17 -17
  115. package/dist/db/queries/event-store-admin.js +6 -6
  116. package/dist/db/queries/event-store-transfer.js +2 -2
  117. package/dist/db/queries/event-store.js +2 -2
  118. package/dist/db/queries/pii-backfill-state.js +2 -2
  119. package/dist/db/queries/raw-sql.js +1 -1
  120. package/dist/db/queries/test-stack.js +1 -1
  121. package/dist/db/reference-data.js +3 -5
  122. package/dist/db/table-builder.js +6 -4
  123. package/dist/db/table-introspection.d.ts +16 -0
  124. package/dist/db/table-introspection.js +76 -0
  125. package/dist/db/tenant-db-runner.d.ts +4 -0
  126. package/dist/db/tenant-db-runner.js +17 -0
  127. package/dist/db/tenant-db.d.ts +5 -2
  128. package/dist/db/tenant-db.js +34 -14
  129. package/dist/derivatives/derivatives-context.js +1 -1
  130. package/dist/derivatives/variant-key.js +2 -1
  131. package/dist/engine/boot-validator/access-declarations.js +2 -2
  132. package/dist/engine/boot-validator/api-ext.js +3 -3
  133. package/dist/engine/boot-validator/boot-check.js +1 -1
  134. package/dist/engine/boot-validator/config-deps.js +13 -13
  135. package/dist/engine/boot-validator/detail-screens.js +1 -1
  136. package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
  137. package/dist/engine/boot-validator/entity-handler.js +84 -35
  138. package/dist/engine/boot-validator/entity-list-screens.js +3 -3
  139. package/dist/engine/boot-validator/handler-field-references.js +19 -9
  140. package/dist/engine/boot-validator/i18n-keys.js +2 -2
  141. package/dist/engine/boot-validator/nav.js +1 -1
  142. package/dist/engine/boot-validator/pii-retention.js +9 -9
  143. package/dist/engine/boot-validator/projection-list-screens.js +5 -5
  144. package/dist/engine/boot-validator/query-output-columns.js +7 -7
  145. package/dist/engine/boot-validator/screens.d.ts +2 -1
  146. package/dist/engine/boot-validator/screens.js +155 -96
  147. package/dist/engine/boot-validator/url-prefill-fields.js +54 -35
  148. package/dist/engine/changeset-changes.js +3 -3
  149. package/dist/engine/define-handler.d.ts +2 -2
  150. package/dist/engine/define-handler.js +11 -7
  151. package/dist/engine/embedded-derived.js +10 -6
  152. package/dist/engine/extensions/escape-hatch-usage.js +2 -1
  153. package/dist/engine/factories.js +5 -6
  154. package/dist/engine/feature-ast/extractors/events.js +43 -33
  155. package/dist/engine/feature-ast/extractors/handlers.js +2 -0
  156. package/dist/engine/feature-ast/patcher.d.ts +3 -3
  157. package/dist/engine/feature-ast/patcher.js +3 -3
  158. package/dist/engine/feature-ast/pattern-change-schema.d.ts +10 -8
  159. package/dist/engine/feature-ast/pattern-change-schema.js +4 -3
  160. package/dist/engine/feature-ast/patterns.d.ts +3 -3
  161. package/dist/engine/feature-ast/render.js +16 -5
  162. package/dist/engine/feature-changelog.d.ts +2 -1
  163. package/dist/engine/feature-changelog.js +8 -6
  164. package/dist/engine/feature-config-events-jobs.d.ts +17 -7
  165. package/dist/engine/feature-config-events-jobs.js +20 -5
  166. package/dist/engine/feature-entity-handlers.d.ts +2 -2
  167. package/dist/engine/feature-entity-handlers.js +12 -3
  168. package/dist/engine/feature-ui-extensions.js +2 -3
  169. package/dist/engine/handler-field-references.d.ts +21 -2
  170. package/dist/engine/handler-field-references.js +55 -17
  171. package/dist/engine/index.d.ts +3 -2
  172. package/dist/engine/index.js +1 -0
  173. package/dist/engine/ownership.js +7 -20
  174. package/dist/engine/pattern-library/mixed-schemas.js +0 -1
  175. package/dist/engine/registry-ingest.d.ts +2 -1
  176. package/dist/engine/registry-ingest.js +85 -2
  177. package/dist/engine/registry-validate.d.ts +1 -0
  178. package/dist/engine/registry-validate.js +38 -8
  179. package/dist/engine/registry.js +2 -1
  180. package/dist/engine/schema-builder.js +3 -4
  181. package/dist/engine/soft-delete-cleanup.js +12 -4
  182. package/dist/engine/steps/call-feature.js +1 -3
  183. package/dist/engine/steps/retry.js +2 -1
  184. package/dist/engine/steps/unsafe-projection-delete.js +2 -2
  185. package/dist/engine/steps/unsafe-projection-upsert.js +4 -15
  186. package/dist/engine/template-variables.d.ts +32 -0
  187. package/dist/engine/template-variables.js +80 -0
  188. package/dist/engine/tier-resolver-extension.js +2 -3
  189. package/dist/engine/types/index.d.ts +2 -2
  190. package/dist/engine/validate-projection-allowlist.js +12 -13
  191. package/dist/engine/where-rule-lint.js +8 -7
  192. package/dist/env/_zod-introspect.d.ts +1 -0
  193. package/dist/errors/classes.js +1 -0
  194. package/dist/errors/kumiko-error.d.ts +2 -0
  195. package/dist/errors/kumiko-error.js +2 -0
  196. package/dist/errors/reasons.d.ts +1 -0
  197. package/dist/errors/reasons.js +2 -0
  198. package/dist/errors/write-error-info.js +2 -0
  199. package/dist/event-store/tenant-db-append.js +2 -2
  200. package/dist/files/assert-file-refs-belong-to-tenant.d.ts +4 -5
  201. package/dist/files/assert-file-refs-belong-to-tenant.js +7 -15
  202. package/dist/files/bind-pending-file-refs.d.ts +2 -0
  203. package/dist/files/bind-pending-file-refs.js +3 -3
  204. package/dist/files/bind-referenced-files.write.d.ts +1 -1
  205. package/dist/files/bind-referenced-files.write.js +22 -13
  206. package/dist/files/create-file-ref-from-bytes.d.ts +6 -3
  207. package/dist/files/create-file-ref-from-bytes.js +67 -4
  208. package/dist/files/create-file-ref.d.ts +22 -0
  209. package/dist/files/create-file-ref.js +53 -25
  210. package/dist/files/file-handle.d.ts +1 -1
  211. package/dist/files/file-handle.js +8 -0
  212. package/dist/files/file-routes.js +2 -2
  213. package/dist/files/index.d.ts +2 -2
  214. package/dist/files/index.js +1 -1
  215. package/dist/files/load-save-file-refs.d.ts +10 -0
  216. package/dist/files/load-save-file-refs.js +47 -0
  217. package/dist/files/local-provider.js +2 -1
  218. package/dist/files/purge-expired-file-ref-bytes.d.ts +4 -1
  219. package/dist/files/purge-expired-file-ref-bytes.js +45 -19
  220. package/dist/i18n/index.d.ts +1 -1
  221. package/dist/i18n/index.js +1 -1
  222. package/dist/i18n/mail-registry.d.ts +2 -0
  223. package/dist/i18n/mail-registry.js +7 -0
  224. package/dist/jobs/index.d.ts +1 -1
  225. package/dist/jobs/job-runner.d.ts +4 -1
  226. package/dist/jobs/job-runner.js +13 -9
  227. package/dist/observability/escape-hatch-report.d.ts +1 -1
  228. package/dist/observability/escape-hatch-report.js +17 -5
  229. package/dist/pipeline/cascade-handler.js +5 -2
  230. package/dist/pipeline/dispatch-batch.js +79 -31
  231. package/dist/pipeline/dispatch-query.js +2 -3
  232. package/dist/pipeline/dispatch-shared.d.ts +7 -2
  233. package/dist/pipeline/dispatch-shared.js +142 -34
  234. package/dist/pipeline/dispatch-write.js +34 -22
  235. package/dist/pipeline/event-dispatcher-delivery.js +2 -3
  236. package/dist/pipeline/index.d.ts +2 -0
  237. package/dist/pipeline/index.js +2 -0
  238. package/dist/pipeline/member-read-only-transaction.js +1 -1
  239. package/dist/pipeline/notify-scope.d.ts +13 -0
  240. package/dist/pipeline/notify-scope.js +32 -0
  241. package/dist/pipeline/pre-transaction-reservations.d.ts +3 -1
  242. package/dist/pipeline/pre-transaction-reservations.js +28 -9
  243. package/dist/pipeline/system-hooks.js +2 -1
  244. package/dist/pipeline/system-identity-switch.js +2 -2
  245. package/dist/rate-limit/bucket.d.ts +3 -3
  246. package/dist/rate-limit/bucket.js +14 -4
  247. package/dist/redis/cache-sync-bus.js +9 -3
  248. package/dist/search/meilisearch-adapter.js +39 -13
  249. package/dist/secrets/stored-envelope.js +7 -7
  250. package/dist/stack/push-entity-projection-tables.js +2 -3
  251. package/dist/stack/table-helpers.js +10 -26
  252. package/dist/stack/test-stack.js +2 -5
  253. package/dist/testing/multipart-helper.d.ts +0 -6
  254. package/dist/testing/multipart-helper.js +5 -5
  255. package/dist/testing/without-ambient-temporal.js +4 -4
  256. package/dist/utils/is-plain-object.d.ts +1 -2
  257. package/dist/utils/is-plain-object.js +1 -4
  258. package/package.json +7 -5
  259. package/src/changes.json +251 -8
@@ -0,0 +1,187 @@
1
+ <!-- Generated by `bun run gen:agent-docs` from samples/recipes/security-baseline. Do not edit. -->
2
+
3
+ # security-baseline
4
+
5
+ securityBaselineFeatures() preset: sessions, crypto-shredding, rate-limiting, audit — plus the NODE_ENV=production boot warning when one is missing.
6
+
7
+ ## README
8
+
9
+ # Security baseline
10
+
11
+ Mount `securityBaselineFeatures()` instead of hand-picking the four features
12
+ every prod app needs for basic account/security hygiene: `sessions`
13
+ (revocable JWTs), `crypto-shredding` (operator-triggered subject-key erase),
14
+ `rate-limiting` (ops-side bucket-status query, dispatcher wiring is
15
+ automatic), and `audit` (tenant-scoped audit trail). Forgetting one of them
16
+ used to be silent; now the boot validator warns at `NODE_ENV=production`.
17
+
18
+ ## What it shows
19
+
20
+ - **`securityBaselineFeatures()`** (`@cosmicdrift/kumiko-bundled-features/presets`) —
21
+ returns the four features in one call, fresh instances per call.
22
+ - **`warnOnMissingSecurityBaseline`** — wired into `validateBoot` (fw#2857).
23
+ At `NODE_ENV=production`, boot compares mounted feature names against
24
+ `SECURITY_BASELINE_FEATURE_NAMES` and logs a `console.warn` naming whatever
25
+ is missing. It never throws — the baseline is a strong recommendation, not
26
+ a hard requirement — and it is silent outside production.
27
+ - **`includeSessions: false`** — an optional opt-out for dropping `sessions`
28
+ from the preset explicitly. Not required for combining with
29
+ `dsgvoSelfServiceFeatures()` any more: both mount `createSessionsFeature()`
30
+ with no-arg options, and the framework's `dedupeFeatures()` collapses the
31
+ two identical instances at boot instead of throwing.
32
+
33
+ ## Feature composition
34
+
35
+ ```
36
+ config → tenant's r.requires("config") dependency
37
+ user → cross-tenant identity (sessions' r.requires target)
38
+ tenant → memberships (audit's r.requires target)
39
+ auth-foundation → tokenVerifier extension point (sessions' r.requires target)
40
+ sessions → revocable JWTs
41
+ crypto-shredding → operator-triggered subject-key erase
42
+ rate-limiting → ops-side rate-limit status query
43
+ audit → tenant-scoped audit trail
44
+ ```
45
+
46
+ ## When to reach for it
47
+
48
+ Any app that also mounts `dsgvoSelfServiceFeatures()` — that preset already
49
+ mounts `sessions` as part of its own require-chain. Combine both directly;
50
+ `dedupeFeatures()` collapses the two identical `sessions` instances at boot:
51
+
52
+ ```ts illustration
53
+ import { dsgvoSelfServiceFeatures, securityBaselineFeatures } from "@cosmicdrift/kumiko-bundled-features/presets";
54
+
55
+ export const APP_FEATURES = [
56
+ // ...config, user, tenant, auth-foundation...
57
+ ...dsgvoSelfServiceFeatures(),
58
+ ...securityBaselineFeatures(),
59
+ ];
60
+ ```
61
+
62
+ For an app that does not mount `dsgvoSelfServiceFeatures()`, use
63
+ `securityBaselineFeatures()` with its default options, as this recipe does.
64
+
65
+ Dedupe keeps the first mounted instance. Late-bound state such as
66
+ `bindAutoRevokeOnPasswordChange` must be bound on the instance returned by
67
+ `composeFeatures`/`createApp`, not on a locally held handle to the dropped one:
68
+ the dropped instance's hooks never run and password changes silently stop
69
+ revoking sessions.
70
+
71
+ A `createSessionsFeature({ ... })` mounted with different options than the
72
+ preset's own no-arg instance still produces a clear boot error — dedupe only
73
+ collapses provably interchangeable instances, never silently picks one:
74
+
75
+ ```ts illustration
76
+ export const APP_FEATURES = [
77
+ // ...
78
+ createSessionsFeature({ expiresInMs: 1000 }),
79
+ ...securityBaselineFeatures(),
80
+ // throws: Duplicate feature: "sessions" mounted twice with different
81
+ // options — mount it once or pass identical options
82
+ ];
83
+ ```
84
+
85
+ ## Tests
86
+
87
+ ```bash
88
+ bun test samples/recipes/security-baseline/src/__tests__/feature.test.ts
89
+ ```
90
+
91
+ Three cases: `APP_FEATURES` boots clean; at `NODE_ENV=production` it produces
92
+ no security-baseline warning; dropping `crypto-shredding`/`rate-limiting`/
93
+ `audit` (`sessions` stays mounted — `auth-foundation`'s own bootCheck requires
94
+ a sessionStore) at `NODE_ENV=production` produces exactly one warning and
95
+ still does not throw.
96
+
97
+ ## Source files
98
+
99
+ ### src/__tests__/feature.test.ts
100
+
101
+ ```ts
102
+ // Security-Baseline Sample — Test
103
+ // Proves: APP_FEATURES boots clean, and the NODE_ENV=production boot warning
104
+ // fires only when the baseline features are missing, never for APP_FEATURES.
105
+
106
+ import { describe, expect, spyOn, test } from "bun:test";
107
+ import { validateBoot } from "@cosmicdrift/kumiko-framework/engine";
108
+ import { APP_FEATURES } from "../feature";
109
+
110
+ function securityBaselineWarnings(calls: readonly unknown[][]): unknown[][] {
111
+ return calls.filter(
112
+ (call) => typeof call[0] === "string" && call[0].includes("security baseline"),
113
+ );
114
+ }
115
+
116
+ describe("security-baseline sample", () => {
117
+ test("APP_FEATURES boots without throwing", () => {
118
+ expect(() => validateBoot(APP_FEATURES)).not.toThrow();
119
+ });
120
+
121
+ test("NODE_ENV=production + APP_FEATURES → no security-baseline warning", () => {
122
+ const originalNodeEnv = process.env["NODE_ENV"];
123
+ process.env["NODE_ENV"] = "production";
124
+ const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
125
+
126
+ try {
127
+ validateBoot(APP_FEATURES);
128
+ expect(securityBaselineWarnings(warnSpy.mock.calls)).toHaveLength(0);
129
+ } finally {
130
+ warnSpy.mockRestore();
131
+ if (originalNodeEnv === undefined) {
132
+ delete process.env["NODE_ENV"];
133
+ } else {
134
+ process.env["NODE_ENV"] = originalNodeEnv;
135
+ }
136
+ }
137
+ });
138
+
139
+ test("NODE_ENV=production + baseline features missing → warns but does not throw", () => {
140
+ const originalNodeEnv = process.env["NODE_ENV"];
141
+ process.env["NODE_ENV"] = "production";
142
+ const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
143
+ // Keep "sessions" mounted — auth-foundation's own bootCheck requires a
144
+ // tokenVerifier or sessionStore, and dropping it would fail boot for an
145
+ // unrelated reason before warnOnMissingSecurityBaseline ever runs.
146
+ const withoutBaseline = APP_FEATURES.filter(
147
+ (f) => !["crypto-shredding", "rate-limiting", "audit"].includes(f.name),
148
+ );
149
+
150
+ try {
151
+ expect(() => validateBoot(withoutBaseline)).not.toThrow();
152
+ expect(securityBaselineWarnings(warnSpy.mock.calls)).toHaveLength(1);
153
+ } finally {
154
+ warnSpy.mockRestore();
155
+ if (originalNodeEnv === undefined) {
156
+ delete process.env["NODE_ENV"];
157
+ } else {
158
+ process.env["NODE_ENV"] = originalNodeEnv;
159
+ }
160
+ }
161
+ });
162
+ });
163
+ ```
164
+
165
+ ### src/feature.ts
166
+
167
+ ```ts
168
+ // Security-Baseline Sample
169
+ // Shows: mounting securityBaselineFeatures() (sessions, crypto-shredding,
170
+ // rate-limiting, audit) and how it silences the NODE_ENV=production boot
171
+ // warning that fires when one of those four is missing.
172
+
173
+ import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
174
+ import { createConfigFeature } from "@cosmicdrift/kumiko-bundled-features/config";
175
+ import { securityBaselineFeatures } from "@cosmicdrift/kumiko-bundled-features/presets";
176
+ import { createTenantFeature } from "@cosmicdrift/kumiko-bundled-features/tenant";
177
+ import { createUserFeature } from "@cosmicdrift/kumiko-bundled-features/user";
178
+ import type { FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
179
+
180
+ export const APP_FEATURES: FeatureDefinition[] = [
181
+ createConfigFeature(),
182
+ createUserFeature(),
183
+ createTenantFeature(),
184
+ authFoundationFeature,
185
+ ...securityBaselineFeatures(),
186
+ ];
187
+ ```
@@ -0,0 +1,99 @@
1
+ <!-- Generated by `bun run gen:agent-docs` from samples/recipes/seed-migration. Do not edit. -->
2
+
3
+ # seed-migration
4
+
5
+ ES-Operations seed-migrations — file-based diff-and-apply für Aggregate-Updates die idempotent-Seeder nicht erfassen.
6
+
7
+ ## README
8
+
9
+ # Seed-Migration
10
+
11
+ File-based migrations for Event-Sourcing operations — like `kumiko schema apply`, but for aggregate-state-changes after the initial seed.
12
+
13
+ ## What it shows
14
+
15
+ - **`seedsDir`-Option** in `runProdApp({ seedsDir: "./seeds" })` — beim Boot werden pending Files in chronologischer Reihenfolge angewendet und in `kumiko_es_operations` markiert.
16
+ - **`SeedMigration`-Interface** — default-Export einer seed-File mit `description` + `run(ctx)`. ctx liefert `systemWriteAs` (System-User bypassed Access-Check) + Read-Helpers (`findUserByEmail`, `findMembershipsOfUser`, `findTenants`, `findTemplateResources`).
17
+ - **Idempotency** — Marker landet nach Erfolg in der Tracking-Tabelle, zweiter Boot skipped applied Seeds.
18
+ - **Tx-Atomicity** — Jede Migration läuft in eigener Transaction; Failure rollt zurück + bricht Boot ab (kein Partial-Apply).
19
+ - **Chronologische File-IDs** — Filename `<date>-<slug>.ts` (z.B. `2026-05-20-fix-admin-roles.ts`) ist die ID.
20
+
21
+ ### Phase 1.5 hinzugefügt
22
+
23
+ - **`tenantIdOverride`** in `ctx.systemWriteAs(qn, payload, tenantId)` — Pflicht wenn das Aggregate in einem Tenant-Stream lebt (sonst `version_conflict`).
24
+ - **Dry-Run-Validator** — Runner parsed seed-files vor Run + checked alle handler-QNs gegen registry. camelCase-typos & andere QN-Drift werden BEFORE the write erkannt.
25
+ - **`scripts/smoke.ts`** — copy-paste-Template für lokalen Pre-Push-Smoke. Bun-runnable, validiert Module-Load + QN-Resolution + Access offline. **Pflicht-Pattern vor Push.**
26
+ - **Docker-COPY-Pflicht** dokumentiert in `framework/src/es-ops/README.md`.
27
+
28
+ ## Lokaler Smoke vor Push (Pflicht)
29
+
30
+ ```bash
31
+ # Im App-Root:
32
+ bun samples/recipes/seed-migration/scripts/smoke.ts --seeds-dir ./seeds
33
+ ```
34
+
35
+ Erwarteter Output:
36
+ ```
37
+ seed: 2026-05-20-fix-admin-roles.ts
38
+ ✓ loads: "ergänze TenantAdmin-Rolle"
39
+ ✓ "tenant:write:update-member-roles" registered + accessible
40
+
41
+ ✓ all 1 seed-file(s) pass smoke.
42
+ ```
43
+
44
+ Bei typo / drift fail-t mit klarer message + exit-code 1 — CI nutzt es als prä-build-step. App-Author muss in `smoke.ts` den `features`-Array gegen die eigene App-Feature-Set tauschen (siehe TODO im File).
45
+
46
+ ### Archive template rows
47
+
48
+ `findTemplateResources({ slug?, kind?, status?, locale?, tenantId? })` replaces a raw `SELECT … FROM read_template_resources`. It defaults to the system tenant and returns `[]` when template-resolver is not mounted:
49
+
50
+ ```ts
51
+ for (const t of await ctx.findTemplateResources({ kind: "mail", status: "active" })) {
52
+ await ctx.systemWriteAs("template-resolver:write:archive", { id: t.id }, SYSTEM_TENANT_ID, [
53
+ "SystemAdmin",
54
+ ]);
55
+ }
56
+ ```
57
+
58
+ ## When to reach for it
59
+
60
+ Du hast einen idempotent-Seeder der bei initialer Erstellung „Insert wenn nicht existiert" macht (z.B. `auth.admin.memberships`), und dann ändern sich die Soll-Daten — neue Rolle, anderer Tier, korrigierte Bezeichnung. Idempotent-Seeder skippen die existing Rows → DB driftet vom Code-Stand ab.
61
+
62
+ Seed-Migrations sind der saubere Fix: ein File schreibt das gewünschte Update einmalig in den Event-Store, Projection läuft automatisch.
63
+
64
+ ## Don't reach for it when
65
+
66
+ - **Initial Seeding** (erste Daten beim leeren Stack). Dafür gibt es `r.config({seeds})` + `options.seeds`-Array — idempotent-by-design durch deterministische Aggregate-IDs.
67
+ - **Schema-Migrations**. Dafür gibt es `kumiko schema generate`. Seed-migrations sind
68
+ Data-Layer, nicht Schema-Layer.
69
+ - **Read-only Operationen / Reports**. Dafür gibt es App-spezifische Routes / Queries.
70
+
71
+ ## How it works
72
+
73
+ 1. Bei `runProdApp`-Boot scannt das Framework `seedsDir` nach `*.ts`-Files.
74
+ 2. SELECT `id` FROM `kumiko_es_operations` WHERE `operation_type='seed-migration'` → applied-Set.
75
+ 3. Pending = files-on-disk **minus** applied-set, sortiert chronologisch.
76
+ 4. Pro pending: Tx auf, `migration.run(ctx)` (kann beliebige Handler via `systemWriteAs` rufen), Marker einfügen, Tx commit.
77
+ 5. Failure → Tx rollback, kein Marker, Boot bricht ab. Operator fixt + Retry.
78
+
79
+ ## CLI
80
+
81
+ ```bash
82
+ # Scaffold neue Migration mit Datum-Prefix
83
+ bunx kumiko ops seed:new fix-admin-roles
84
+ # → seeds/2026-05-20-fix-admin-roles.ts
85
+
86
+ # Was applied, was pending
87
+ bunx kumiko ops seed:status
88
+
89
+ # (Phase 1.5) Direct apply ohne Boot
90
+ bunx kumiko ops seed:apply [--dry-run]
91
+ ```
92
+
93
+ ## Skippable im Notfall
94
+
95
+ Wenn eine kaputte Migration den Boot blockiert: setze `skippable: true` im seed-File und beim nächsten Boot `KUMIKO_SKIP_ES_OPS_<sanitized-id>=1` env-var. Marker wird nicht geschrieben → beim nächsten Boot ohne Flag würde der Seed wieder laufen. NICHT als Standard-Workflow — Recovery-only.
96
+
97
+ ## See also
98
+
99
+ - **Driver-Use-Case:** `seeds/2026-02-01-fix-admin-roles.ts` in this recipe
@@ -0,0 +1,259 @@
1
+ <!-- Generated by `bun run gen:agent-docs` from samples/recipes/session-revocation. Do not edit. -->
2
+
3
+ # session-revocation
4
+
5
+ createSessionCallbacks + sessionChecker — server-side session revocation via JWT jti.
6
+
7
+ ## README
8
+
9
+ # Sample: Session Revocation
10
+
11
+ **I want JWTs to be revocable server-side — logout, password change, or admin force-logout should invalidate outstanding tokens before they expire.**
12
+
13
+ ## What this sample shows
14
+
15
+ The production wiring shape: an app builds `createSessionCallbacks()` against a live DB handle, then hands the creator, revoker, and checker to `buildServer({ auth: ... })`. From there the framework handles the rest — login persists a `sid`, the middleware checks it on every request, logout flips the DB row, and a forged JWT without a matching session is rejected.
16
+
17
+ ## The wiring in 3 sentences
18
+
19
+ 1. `createSessionCallbacks({ db })` returns `sessionCreator`, `sessionRevoker`, and `sessionChecker` backed by `store_user_sessions`.
20
+ 2. `buildServer({ auth: { sessionCreator, sessionRevoker, sessionChecker, ... } })` links the JWT `jti` claim to that table on every request.
21
+ 3. After logout (or mass-revoke on password change), the same JWT returns 401 even though the signature is still valid.
22
+
23
+ ## What to copy into your app
24
+
25
+ ```ts illustration
26
+ const db = createDbConnection(config.databaseUrl);
27
+ const callbacks = createSessionCallbacks({ db });
28
+
29
+ const server = buildServer({
30
+ registry: buildRegistry([
31
+ createSessionsFeature({
32
+ autoRevokeOnPasswordChange: callbacks.sessionMassRevoker,
33
+ }),
34
+ // ... other features
35
+ ]),
36
+ context: { db, /* ... */ },
37
+ jwtSecret: process.env.JWT_SECRET,
38
+ auth: {
39
+ membershipQuery: "tenant:query:memberships",
40
+ loginHandler: "auth-email-password:write:login",
41
+ sessionCreator: callbacks.sessionCreator,
42
+ sessionRevoker: callbacks.sessionRevoker,
43
+ sessionChecker: callbacks.sessionChecker,
44
+ },
45
+ });
46
+ ```
47
+
48
+ ## Design rules
49
+
50
+ 1. **Sessions are a feature, not built-in.** Without `createSessionsFeature()` and wired callbacks, the app issues plain stateless JWTs — valid until expiry, no revocation path.
51
+ 2. **Storage is pluggable.** This sample uses the default DB-backed impl; swap in Redis or Memcached by implementing the same `SessionCreator` / `SessionRevoker` / `SessionChecker` signatures.
52
+ 3. **`jti` is the link.** When a checker is wired, the middleware never trusts the JWT alone — it confirms the session row is still live.
53
+
54
+ ## Tests
55
+
56
+ - **End-to-end:** login → authenticated query → logout → same token rejected with 401
57
+ - **Wiring contract:** copies the `buildServer({ auth: ... })` block from the sample comment verbatim
58
+
59
+ ## Source files
60
+
61
+ ### src/__tests__/feature.integration.test.ts
62
+
63
+ ```ts
64
+ // Session Revocation Sample — Integration Test
65
+ //
66
+ // Drives the wiring from the sample's comment snippet end-to-end against
67
+ // real HTTP + DB + Redis. If a reader copies the buildServer({...}) block
68
+ // verbatim, the resulting app behaves like this test asserts.
69
+
70
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
71
+ import {
72
+ AuthHandlers,
73
+ createAuthEmailPasswordFeature,
74
+ hashPassword,
75
+ } from "@cosmicdrift/kumiko-bundled-features/auth-email-password";
76
+ import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
77
+ import {
78
+ configValuesTable,
79
+ createConfigFeature,
80
+ } from "@cosmicdrift/kumiko-bundled-features/config";
81
+ import {
82
+ type SessionCallbacks,
83
+ userSessionEntity,
84
+ userSessionTable,
85
+ } from "@cosmicdrift/kumiko-bundled-features/sessions";
86
+ import {
87
+ sessionCallbacksFromLateBound,
88
+ withMintedSession,
89
+ } from "@cosmicdrift/kumiko-bundled-features/sessions/testing";
90
+ import {
91
+ createTenantFeature,
92
+ tenantEntity,
93
+ tenantMembershipsTable,
94
+ } from "@cosmicdrift/kumiko-bundled-features/tenant";
95
+ import { seedTenantMembership } from "@cosmicdrift/kumiko-bundled-features/tenant/seeding";
96
+ import {
97
+ createUserFeature,
98
+ UserHandlers,
99
+ userEntity,
100
+ userTable,
101
+ } from "@cosmicdrift/kumiko-bundled-features/user";
102
+ import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
103
+ import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
104
+ import {
105
+ setupTestStack,
106
+ type TestStack,
107
+ TestUsers,
108
+ testTenantId,
109
+ unsafeCreateEntityTable,
110
+ unsafePushTables,
111
+ } from "@cosmicdrift/kumiko-framework/stack";
112
+ import { createLateBoundHolder } from "@cosmicdrift/kumiko-framework/testing";
113
+ import * as jose from "jose";
114
+ import { createSessionCallbacks, createSessionsFeature } from "../feature";
115
+
116
+ const TENANT: TenantId = testTenantId(1);
117
+
118
+ let stack: TestStack;
119
+ const callbacks = createLateBoundHolder<SessionCallbacks>("session-callbacks");
120
+
121
+ beforeAll(async () => {
122
+ const bound = sessionCallbacksFromLateBound(callbacks);
123
+ stack = await setupTestStack({
124
+ features: [
125
+ createConfigFeature(),
126
+ createUserFeature(),
127
+ createTenantFeature(),
128
+ createAuthEmailPasswordFeature(),
129
+ authFoundationFeature,
130
+ createSessionsFeature(),
131
+ ],
132
+ authConfig: {
133
+ ...bound.asAuthConfig(),
134
+ membershipQuery: "tenant:query:memberships",
135
+ loginHandler: AuthHandlers.login,
136
+ },
137
+ });
138
+ callbacks.set(createSessionCallbacks({ db: stack.db }));
139
+
140
+ await unsafeCreateEntityTable(stack.db, userEntity);
141
+ await unsafeCreateEntityTable(stack.db, userSessionEntity);
142
+ await unsafeCreateEntityTable(stack.db, tenantEntity);
143
+ await unsafePushTables(stack.db, { configValuesTable, tenantMembershipsTable });
144
+ });
145
+
146
+ afterAll(async () => {
147
+ await stack.cleanup();
148
+ });
149
+
150
+ beforeEach(async () => {
151
+ await asRawClient(stack.db).unsafe(`DELETE FROM "${userSessionTable.tableName}"`);
152
+ await asRawClient(stack.db).unsafe(`DELETE FROM "${userTable.tableName}"`);
153
+ await asRawClient(stack.db).unsafe(`DELETE FROM "${tenantMembershipsTable.tableName}"`);
154
+ });
155
+
156
+ describe("session-revocation sample wiring", () => {
157
+ test("end-to-end: login, use, logout, reject", async () => {
158
+ const hash = await hashPassword("super-long-password");
159
+ const actor = await withMintedSession(
160
+ (user, meta) => callbacks.get().sessionCreator(user, meta),
161
+ TestUsers.systemAdmin,
162
+ );
163
+ const created = await stack.http.writeOk<{ id: string }>(
164
+ UserHandlers.create,
165
+ { email: "demo@example.com", passwordHash: hash, displayName: "Demo" },
166
+ actor,
167
+ );
168
+ await seedTenantMembership(stack.db, {
169
+ userId: created.id,
170
+ tenantId: TENANT,
171
+ roles: ["User"],
172
+ });
173
+
174
+ const loginRes = await stack.http.raw("POST", "/api/auth/login", {
175
+ email: "demo@example.com",
176
+ password: "super-long-password",
177
+ });
178
+ expect(loginRes.status).toBe(200);
179
+ const { token } = (await loginRes.json()) as { token: string };
180
+ expect(jose.decodeJwt(token).jti).toBeTypeOf("string");
181
+
182
+ const beforeLogout = await stack.http.raw(
183
+ "POST",
184
+ "/api/query",
185
+ { type: "user:query:user:me", payload: {} },
186
+ { Authorization: `Bearer ${token}` },
187
+ );
188
+ expect(beforeLogout.status).toBe(200);
189
+
190
+ const logout = await stack.http.raw("POST", "/api/auth/logout", undefined, {
191
+ Authorization: `Bearer ${token}`,
192
+ });
193
+ expect(logout.status).toBe(200);
194
+
195
+ const afterLogout = await stack.http.raw(
196
+ "POST",
197
+ "/api/query",
198
+ { type: "user:query:user:me", payload: {} },
199
+ { Authorization: `Bearer ${token}` },
200
+ );
201
+ expect(afterLogout.status).toBe(401);
202
+ });
203
+ });
204
+ ```
205
+
206
+ ### src/feature.ts
207
+
208
+ ```ts
209
+ // Session Revocation Sample
210
+ //
211
+ // Shows the production-wiring shape: an app builds `createSessionCallbacks()`
212
+ // against a live DB handle, then hands the creator/revoker/checker to
213
+ // `buildServer({ auth: ... })`. From there the framework handles the rest —
214
+ // login persists a sid, the middleware checks it on every request, logout
215
+ // flips the DB row, a forged JWT without a matching session is rejected.
216
+ //
217
+ // What to copy into your own app:
218
+ //
219
+ // const db = createDbConnection(config.databaseUrl);
220
+ // const callbacks = createSessionCallbacks({ db });
221
+ // const server = buildServer({
222
+ // registry: buildRegistry([
223
+ // // Pass the mass-revoker into the sessions feature to wire
224
+ // // "password-change signs you out everywhere" — the feature registers
225
+ // // a cross-feature entity-hook on the user entity that runs it.
226
+ // createSessionsFeature({
227
+ // autoRevokeOnPasswordChange: callbacks.sessionMassRevoker,
228
+ // }),
229
+ // // ... other features
230
+ // ]),
231
+ // context: { db, ... },
232
+ // jwtSecret: config.jwtSecret,
233
+ // auth: {
234
+ // membershipQuery: "tenant:query:memberships",
235
+ // loginHandler: "auth-email-password:write:login",
236
+ // sessionCreator: callbacks.sessionCreator,
237
+ // sessionRevoker: callbacks.sessionRevoker,
238
+ // sessionChecker: callbacks.sessionChecker,
239
+ // },
240
+ // });
241
+ //
242
+ // Design rules the sample demonstrates:
243
+ //
244
+ // 1. Sessions are a feature, not built-in. An app that doesn't register
245
+ // `createSessionsFeature()` (or doesn't wire callbacks) issues plain
246
+ // stateless JWTs — valid until expiry, no revocation path.
247
+ // 2. Session storage is abstracted behind the three callback signatures.
248
+ // This sample uses the default DB-backed impl; you could swap in a
249
+ // Redis- or Memcached-backed version by wiring your own callbacks that
250
+ // match `SessionCreator`/`SessionRevoker`/`SessionChecker`.
251
+ // 3. The `jti` claim on the JWT is the link between stateless token and
252
+ // stateful server. The middleware never trusts the JWT alone when a
253
+ // checker is wired — it confirms the sid is still live.
254
+
255
+ export {
256
+ createSessionCallbacks,
257
+ createSessionsFeature,
258
+ } from "@cosmicdrift/kumiko-bundled-features/sessions";
259
+ ```