@cosmicdrift/kumiko-samples 0.365.1 → 0.366.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 (174) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +1 -1
  3. package/packages/bundled-features/src/admin-shell/feature.ts +9 -9
  4. package/packages/bundled-features/src/agent-tools/changes.json +6 -0
  5. package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +87 -5
  6. package/packages/bundled-features/src/audit/feature.ts +1 -1
  7. package/packages/bundled-features/src/auth-email-password/changes.json +13 -0
  8. package/packages/bundled-features/src/auth-email-password/constants.ts +0 -2
  9. package/packages/bundled-features/src/auth-email-password/feature.ts +3 -3
  10. package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +1 -0
  11. package/packages/bundled-features/src/auth-mfa/feature.ts +4 -4
  12. package/packages/bundled-features/src/auth-mfa-user-data/index.ts +1 -1
  13. package/packages/bundled-features/src/billing-foundation/consumer-protection/register-consent.ts +1 -1
  14. package/packages/bundled-features/src/billing-foundation/feature.ts +1 -1
  15. package/packages/bundled-features/src/cap-counter/cap-reservation.ts +89 -28
  16. package/packages/bundled-features/src/cap-counter/changes.json +7 -0
  17. package/packages/bundled-features/src/cap-counter/constants.ts +3 -0
  18. package/packages/bundled-features/src/cap-counter/feature.ts +2 -2
  19. package/packages/bundled-features/src/cap-counter/tables.ts +5 -2
  20. package/packages/bundled-features/src/cap-overview/feature.ts +3 -3
  21. package/packages/bundled-features/src/channel-discord/feature.ts +2 -2
  22. package/packages/bundled-features/src/channel-email/feature.ts +1 -1
  23. package/packages/bundled-features/src/channel-in-app/feature.ts +1 -1
  24. package/packages/bundled-features/src/channel-push/feature.ts +1 -1
  25. package/packages/bundled-features/src/channel-slack/feature.ts +2 -2
  26. package/packages/bundled-features/src/channel-teams/feature.ts +2 -2
  27. package/packages/bundled-features/src/channel-telegram/feature.ts +2 -2
  28. package/packages/bundled-features/src/compliance-profiles/feature.ts +1 -1
  29. package/packages/bundled-features/src/compliance-profiles-ops/index.ts +3 -3
  30. package/packages/bundled-features/src/config/feature.ts +1 -1
  31. package/packages/bundled-features/src/crypto-shredding/changes.json +6 -0
  32. package/packages/bundled-features/src/custom-fields/feature.ts +2 -2
  33. package/packages/bundled-features/src/custom-fields/wire-for-entity.ts +17 -13
  34. package/packages/bundled-features/src/data-retention/feature.ts +1 -1
  35. package/packages/bundled-features/src/data-retention/presets.ts +4 -4
  36. package/packages/bundled-features/src/delivery/changes.json +6 -0
  37. package/packages/bundled-features/src/delivery/db/queries/address-opt-outs.ts +6 -2
  38. package/packages/bundled-features/src/delivery/db/queries/preferences.ts +4 -4
  39. package/packages/bundled-features/src/delivery/delivery-service.ts +17 -2
  40. package/packages/bundled-features/src/delivery/feature.ts +9 -5
  41. package/packages/bundled-features/src/delivery/handlers/resubscribe-address.write.ts +12 -8
  42. package/packages/bundled-features/src/delivery/handlers/resubscribe-user.write.ts +3 -1
  43. package/packages/bundled-features/src/delivery/handlers/set-preference.write.ts +3 -1
  44. package/packages/bundled-features/src/delivery/handlers/unsubscribe-address.write.ts +3 -1
  45. package/packages/bundled-features/src/delivery/handlers/unsubscribe-user.write.ts +3 -1
  46. package/packages/bundled-features/src/delivery/notification-type-aliases.ts +13 -0
  47. package/packages/bundled-features/src/derivatives-sharp/feature.ts +1 -1
  48. package/packages/bundled-features/src/document-ingest-foundation/feature.ts +2 -2
  49. package/packages/bundled-features/src/file-derivatives/feature.ts +3 -3
  50. package/packages/bundled-features/src/file-foundation/feature.ts +1 -1
  51. package/packages/bundled-features/src/file-provider-inmemory/feature.ts +2 -2
  52. package/packages/bundled-features/src/file-provider-s3/feature.ts +3 -3
  53. package/packages/bundled-features/src/file-provider-s3-env/feature.ts +2 -2
  54. package/packages/bundled-features/src/files/README.md +1 -1
  55. package/packages/bundled-features/src/files-tenant-data/index.ts +1 -1
  56. package/packages/bundled-features/src/folders/feature.ts +2 -2
  57. package/packages/bundled-features/src/folders-user-data/index.ts +1 -1
  58. package/packages/bundled-features/src/form-draft/feature.ts +2 -2
  59. package/packages/bundled-features/src/form-draft-user-data/index.ts +1 -1
  60. package/packages/bundled-features/src/inbound-mail-foundation/changes.json +13 -0
  61. package/packages/bundled-features/src/inbound-mail-foundation/connect-routes.ts +9 -1
  62. package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +1 -1
  63. package/packages/bundled-features/src/inbound-mail-foundation/types.ts +19 -7
  64. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +21 -2
  65. package/packages/bundled-features/src/inbound-provider-imap/feature.ts +2 -2
  66. package/packages/bundled-features/src/inbound-provider-inmemory/feature.ts +1 -1
  67. package/packages/bundled-features/src/ledger/feature.ts +5 -5
  68. package/packages/bundled-features/src/legal-pages/feature.ts +1 -1
  69. package/packages/bundled-features/src/mail-foundation/feature.ts +1 -1
  70. package/packages/bundled-features/src/mail-transport-inmemory/feature.ts +2 -2
  71. package/packages/bundled-features/src/mail-transport-smtp/feature.ts +3 -3
  72. package/packages/bundled-features/src/managed-pages/css-gate.ts +1 -1
  73. package/packages/bundled-features/src/managed-pages/feature.ts +2 -2
  74. package/packages/bundled-features/src/metrics/feature.ts +2 -2
  75. package/packages/bundled-features/src/notes-history/feature.ts +2 -2
  76. package/packages/bundled-features/src/notes-history-user-data/index.ts +1 -1
  77. package/packages/bundled-features/src/personal-access-tokens/feature.ts +15 -11
  78. package/packages/bundled-features/src/readiness/feature.ts +2 -2
  79. package/packages/bundled-features/src/renderer-foundation/README.md +1 -1
  80. package/packages/bundled-features/src/renderer-foundation/feature.ts +1 -1
  81. package/packages/bundled-features/src/renderer-pdf/feature.ts +1 -1
  82. package/packages/bundled-features/src/secrets/feature.ts +1 -1
  83. package/packages/bundled-features/src/seo/feature.ts +1 -1
  84. package/packages/bundled-features/src/sessions/feature.ts +17 -13
  85. package/packages/bundled-features/src/step-dispatcher/feature.ts +1 -1
  86. package/packages/bundled-features/src/subscription-mollie/feature.ts +1 -1
  87. package/packages/bundled-features/src/subscription-stripe/feature.ts +3 -3
  88. package/packages/bundled-features/src/tags/feature.ts +2 -2
  89. package/packages/bundled-features/src/template-resolver/feature.ts +2 -2
  90. package/packages/bundled-features/src/template-resolver-user-data/index.ts +1 -1
  91. package/packages/bundled-features/src/tenant/feature.ts +4 -4
  92. package/packages/bundled-features/src/tenant/seeding.ts +1 -1
  93. package/packages/bundled-features/src/tenant-lifecycle/feature.ts +2 -3
  94. package/packages/bundled-features/src/tenant-settings/feature.ts +1 -1
  95. package/packages/bundled-features/src/tier-engine/feature.ts +36 -27
  96. package/packages/bundled-features/src/user/feature.ts +1 -1
  97. package/packages/bundled-features/src/user-data-rights/README.md +1 -1
  98. package/packages/bundled-features/src/user-data-rights/feature.ts +4 -4
  99. package/packages/bundled-features/src/user-data-rights-defaults/feature.ts +2 -2
  100. package/packages/bundled-features/src/user-profile/feature.ts +4 -4
  101. package/packages/bundled-features/src/waitlist/feature.ts +2 -2
  102. package/samples/apps/cap-billing-demo/README.md +1 -1
  103. package/samples/apps/cap-billing-demo/src/feature.ts +4 -4
  104. package/samples/apps/cap-billing-demo/src/run-config.ts +1 -1
  105. package/samples/apps/config-demo/src/features/demo/feature.ts +2 -2
  106. package/samples/apps/styleguide/src/features/ai-settings/feature.ts +43 -28
  107. package/samples/apps/styleguide/src/features/demo/feature.ts +18 -18
  108. package/samples/apps/styleguide/src/features/widgets/ai-text-demo.ts +10 -12
  109. package/samples/apps/styleguide/src/features/widgets/feature.ts +90 -108
  110. package/samples/apps/ui-walkthrough/src/features/rental/feature.ts +36 -36
  111. package/samples/apps/ui-walkthrough/src/features/vehicles/feature.ts +6 -6
  112. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +2 -2
  113. package/samples/apps/use-all-bundled/scripts/gen-sample-index.ts +7 -7
  114. package/samples/apps/use-all-bundled/src/app/notes-feature.ts +3 -3
  115. package/samples/apps/use-all-bundled/src/app/screens-feature.ts +1 -1
  116. package/samples/apps/user-data-rights-demo/src/feature.ts +5 -5
  117. package/samples/recipes/access-control/src/feature.ts +21 -25
  118. package/samples/recipes/anonymous-access/src/feature.ts +36 -43
  119. package/samples/recipes/anonymous-access-multitenant/src/feature.ts +14 -16
  120. package/samples/recipes/auth-foundation-providers/src/feature.ts +1 -1
  121. package/samples/recipes/basic-entity/README.md +1 -1
  122. package/samples/recipes/boot-check/README.md +1 -1
  123. package/samples/recipes/boot-check/src/feature.ts +2 -2
  124. package/samples/recipes/chat-channels/src/feature.ts +1 -1
  125. package/samples/recipes/cross-feature-events/src/feature.ts +7 -7
  126. package/samples/recipes/currencies-global/src/feature.ts +3 -3
  127. package/samples/recipes/currencies-per-tenant/src/feature.ts +2 -2
  128. package/samples/recipes/custom-fields-basic/README.md +1 -1
  129. package/samples/recipes/custom-fields-basic/src/feature.ts +3 -2
  130. package/samples/recipes/custom-handlers/README.md +3 -3
  131. package/samples/recipes/custom-handlers/src/feature.ts +37 -41
  132. package/samples/recipes/dashboard-stats/src/feature.ts +6 -6
  133. package/samples/recipes/delivery-notifications/README.md +1 -1
  134. package/samples/recipes/delivery-notifications/src/feature.ts +8 -8
  135. package/samples/recipes/designer-demo/src/feature.ts +2 -1
  136. package/samples/recipes/embedded/src/feature.ts +1 -1
  137. package/samples/recipes/embedded-entity-form/src/feature.ts +13 -13
  138. package/samples/recipes/encrypted-tenant-config/src/feature.ts +1 -1
  139. package/samples/recipes/error-contract/src/feature.ts +25 -25
  140. package/samples/recipes/event-sourcing/src/feature.ts +70 -76
  141. package/samples/recipes/feature-toggles/src/feature.ts +28 -20
  142. package/samples/recipes/files-post-processing/src/feature.ts +22 -26
  143. package/samples/recipes/idempotency/src/feature.ts +16 -18
  144. package/samples/recipes/inbound-mail-triage/src/feature.ts +1 -1
  145. package/samples/recipes/lane-routing/src/feature.ts +10 -12
  146. package/samples/recipes/legal-pages/src/feature.ts +1 -1
  147. package/samples/recipes/lifecycle-hooks/README.md +4 -4
  148. package/samples/recipes/lifecycle-hooks/src/feature.ts +55 -35
  149. package/samples/recipes/managed-config/src/feature.ts +8 -8
  150. package/samples/recipes/managed-pages/src/feature.ts +1 -1
  151. package/samples/recipes/notes-history-basic/README.md +1 -1
  152. package/samples/recipes/notes-history-basic/src/feature.ts +2 -2
  153. package/samples/recipes/pdf-audit-export/src/feature.ts +7 -7
  154. package/samples/recipes/pipeline-basics/README.md +1 -1
  155. package/samples/recipes/pipeline-basics/src/feature.ts +7 -7
  156. package/samples/recipes/public-share-token/src/feature.ts +1 -1
  157. package/samples/recipes/rate-limiting/src/feature.ts +7 -9
  158. package/samples/recipes/record-detail-layout/src/feature.ts +18 -18
  159. package/samples/recipes/reference-data/src/feature.ts +1 -1
  160. package/samples/recipes/relations/src/feature.ts +2 -2
  161. package/samples/recipes/screens-nav/src/feature.ts +1 -1
  162. package/samples/recipes/search/src/feature.ts +17 -19
  163. package/samples/recipes/security-baseline/README.md +1 -1
  164. package/samples/recipes/state-machine/src/feature.ts +1 -1
  165. package/samples/recipes/tags-basic/README.md +1 -1
  166. package/samples/recipes/tags-basic/src/feature.ts +2 -2
  167. package/samples/recipes/tenant-settings/README.md +1 -1
  168. package/samples/recipes/tenant-settings/src/feature.ts +1 -1
  169. package/samples/recipes/timezones/src/feature.ts +12 -12
  170. package/samples/recipes/user-data-rights/README.md +2 -2
  171. package/samples/recipes/user-data-rights/src/feature.ts +1 -1
  172. package/samples/recipes/user-profile/src/feature.ts +1 -1
  173. package/samples/recipes/webhook-step/src/feature.ts +1 -1
  174. package/samples/recipes/writeform-section/src/feature.ts +12 -12
@@ -74,17 +74,17 @@ export const orderEntity = createEntity({
74
74
  const orderTable = buildEntityTable("order", orderEntity);
75
75
 
76
76
  export const ordersLiteFeature = defineFeature("orders-lite", (r) => {
77
- r.entity("order", orderEntity);
77
+ r.entity({ name: "order", ...orderEntity });
78
78
 
79
79
  // 1) Create — standard path. Zod schema rejects an empty cart; the
80
80
  // dispatcher converts the ZodIssue into a ValidationError with
81
81
  // details.fields[] (no handler code needed).
82
- r.writeHandler(
83
- "order:create",
84
- z.object({
82
+ r.writeHandler({
83
+ name: "order:create",
84
+ schema: z.object({
85
85
  totalCents: z.number().int().min(1, "cart_is_empty"),
86
86
  }),
87
- async (event, ctx) => {
87
+ handler: async (event, ctx) => {
88
88
  const crud = createEventStoreExecutor(orderTable, orderEntity, { entityName: "order" });
89
89
  return crud.create(
90
90
  { ...event.payload, ownerId: event.user.id, status: "draft" },
@@ -92,8 +92,8 @@ export const ordersLiteFeature = defineFeature("orders-lite", (r) => {
92
92
  ctx.db,
93
93
  );
94
94
  },
95
- { access: { roles: ["User", "Admin"] } },
96
- );
95
+ access: { roles: ["User", "Admin"] },
96
+ });
97
97
 
98
98
  // 2) Pay — business rule: only placed orders can be paid. Shows three
99
99
  // different failure shapes in a single handler:
@@ -103,10 +103,10 @@ export const ordersLiteFeature = defineFeature("orders-lite", (r) => {
103
103
  // - UnprocessableError with reason from OrdersLiteReasons
104
104
  // - Framework-generated UnprocessableError with reason
105
105
  // FrameworkReasons.invalidTransition via guardTransition
106
- r.writeHandler(
107
- "order:pay",
108
- z.object({ id: z.uuid() }),
109
- async (event, ctx) => {
106
+ r.writeHandler({
107
+ name: "order:pay",
108
+ schema: z.object({ id: z.uuid() }),
109
+ handler: async (event, ctx) => {
110
110
  const [current] = await ctx.db.selectMany(orderTable, { id: event.payload.id });
111
111
 
112
112
  // NotFoundError — automatic reason = "order_not_found" via snake-case
@@ -153,16 +153,16 @@ export const ordersLiteFeature = defineFeature("orders-lite", (r) => {
153
153
  ctx.db,
154
154
  );
155
155
  },
156
- { access: { roles: ["User", "Admin"] } },
157
- );
156
+ access: { roles: ["User", "Admin"] },
157
+ });
158
158
 
159
159
  // 3) Cancel — shows ConflictError for a business conflict that isn't a
160
160
  // transition or a stale write. E.g. a refund window expired; the row
161
161
  // is fine but the action is no longer allowed.
162
- r.writeHandler(
163
- "order:cancel",
164
- z.object({ id: z.uuid() }),
165
- async (event, ctx) => {
162
+ r.writeHandler({
163
+ name: "order:cancel",
164
+ schema: z.object({ id: z.uuid() }),
165
+ handler: async (event, ctx) => {
166
166
  const [current] = await ctx.db.selectMany(orderTable, { id: event.payload.id });
167
167
  if (!current) return failNotFound("order", event.payload.id);
168
168
 
@@ -192,18 +192,18 @@ export const ordersLiteFeature = defineFeature("orders-lite", (r) => {
192
192
  ctx.db,
193
193
  );
194
194
  },
195
- { access: { roles: ["User", "Admin"] } },
196
- );
195
+ access: { roles: ["User", "Admin"] },
196
+ });
197
197
 
198
198
  // 4) Update — demonstrates the throw-based path (KumikoError raised
199
199
  // directly, not via writeFailure return). The dispatcher catches it
200
200
  // and wraps it in a WriteErrorInfo the same as the return path.
201
201
  // Use this style when a helper deep in the call tree needs to abort
202
202
  // without threading a WriteResult back up.
203
- r.writeHandler(
204
- "order:rename",
205
- z.object({ id: z.uuid(), nickname: z.string().min(1) }),
206
- async (event, ctx) => {
203
+ r.writeHandler({
204
+ name: "order:rename",
205
+ schema: z.object({ id: z.uuid(), nickname: z.string().min(1) }),
206
+ handler: async (event, ctx) => {
207
207
  const [current] = await ctx.db.selectMany(orderTable, { id: event.payload.id });
208
208
 
209
209
  if (!current) {
@@ -233,6 +233,6 @@ export const ordersLiteFeature = defineFeature("orders-lite", (r) => {
233
233
  },
234
234
  };
235
235
  },
236
- { access: { roles: ["User", "Admin"] } },
237
- );
236
+ access: { roles: ["User", "Admin"] },
237
+ });
238
238
  });
@@ -126,7 +126,7 @@ export const approverDirectoryTable = table("read_showcase_approver_directory",
126
126
  // --- Feature ---
127
127
 
128
128
  export const invoiceFeature = defineFeature("showcase", (r) => {
129
- r.entity("showcase-invoice", invoiceEntity);
129
+ r.entity({ name: "showcase-invoice", ...invoiceEntity });
130
130
 
131
131
  // Two domain events. "approved" is versioned — v1 stored `amount` as
132
132
  // string, v2 uses `amountCents` integer. The DECLARATIVE migration form
@@ -259,21 +259,21 @@ export const invoiceFeature = defineFeature("showcase", (r) => {
259
259
 
260
260
  // --- Write handlers ---
261
261
 
262
- r.writeHandler(
263
- "invoice:create",
264
- z.object({ customer: z.string() }),
265
- async (event, ctx) =>
262
+ r.writeHandler({
263
+ name: "invoice:create",
264
+ schema: z.object({ customer: z.string() }),
265
+ handler: async (event, ctx) =>
266
266
  invoiceExecutor.create(
267
267
  { customer: event.payload.customer, status: "draft" },
268
268
  event.user,
269
269
  ctx.db,
270
270
  ),
271
- { access: { roles: ["Admin"] } },
272
- );
271
+ access: { roles: ["Admin"] },
272
+ });
273
273
 
274
- r.writeHandler(
275
- "invoice:approve",
276
- z.object({
274
+ r.writeHandler({
275
+ name: "invoice:approve",
276
+ schema: z.object({
277
277
  id: z.uuid(),
278
278
  amountCents: z.number().int(),
279
279
  approvedBy: z.string(),
@@ -282,7 +282,7 @@ export const invoiceFeature = defineFeature("showcase", (r) => {
282
282
  geoRegion: z.string().optional(),
283
283
  abTestBucket: z.string().optional(),
284
284
  }),
285
- async (event, ctx) => {
285
+ handler: async (event, ctx) => {
286
286
  const headers: Record<string, string> = {};
287
287
  if (event.payload.geoRegion) headers["geoRegion"] = event.payload.geoRegion;
288
288
  if (event.payload.abTestBucket) headers["abTestBucket"] = event.payload.abTestBucket;
@@ -295,13 +295,13 @@ export const invoiceFeature = defineFeature("showcase", (r) => {
295
295
  });
296
296
  return { isSuccess: true as const, data: { id: event.payload.id } };
297
297
  },
298
- { access: { roles: ["Admin"] } },
299
- );
298
+ access: { roles: ["Admin"] },
299
+ });
300
300
 
301
- r.writeHandler(
302
- "invoice:acknowledge",
303
- z.object({ id: z.uuid(), approverId: z.string() }),
304
- async (event, ctx) => {
301
+ r.writeHandler({
302
+ name: "invoice:acknowledge",
303
+ schema: z.object({ id: z.uuid(), approverId: z.string() }),
304
+ handler: async (event, ctx) => {
305
305
  const row = await fetchOne<{ displayName: string }>(ctx.db, approverDirectoryTable, {
306
306
  approverId: event.payload.approverId,
307
307
  });
@@ -314,13 +314,13 @@ export const invoiceFeature = defineFeature("showcase", (r) => {
314
314
  });
315
315
  return { isSuccess: true as const, data: { id: event.payload.id } };
316
316
  },
317
- { access: { roles: ["Admin"] } },
318
- );
317
+ access: { roles: ["Admin"] },
318
+ });
319
319
 
320
- r.writeHandler(
321
- "invoice:pay",
322
- z.object({ id: z.uuid(), amountCents: z.number().int() }),
323
- async (event, ctx) => {
320
+ r.writeHandler({
321
+ name: "invoice:pay",
322
+ schema: z.object({ id: z.uuid(), amountCents: z.number().int() }),
323
+ handler: async (event, ctx) => {
324
324
  await ctx.appendEvent({
325
325
  aggregateId: event.payload.id,
326
326
  aggregateType: "showcase-invoice",
@@ -329,35 +329,33 @@ export const invoiceFeature = defineFeature("showcase", (r) => {
329
329
  });
330
330
  return { isSuccess: true as const, data: { id: event.payload.id } };
331
331
  },
332
- {
333
- access: { roles: ["Admin"] },
334
- // Per-user cap: a single admin shouldn't fire more than 5 pay
335
- // operations per minute. Real production caps would be per
336
- // tenant + handler ("tenant+handler") to keep one admin from
337
- // monopolising the tenant's quota — kept simple here for the
338
- // sample. Demonstrates the fourth dispatcher gate (rate-limit
339
- // → access → validation → handler).
340
- rateLimit: { per: "user", limit: 5, windowSeconds: 60 },
341
- },
342
- );
332
+ access: { roles: ["Admin"] },
333
+ // Per-user cap: a single admin shouldn't fire more than 5 pay
334
+ // operations per minute. Real production caps would be per
335
+ // tenant + handler ("tenant+handler") to keep one admin from
336
+ // monopolising the tenant's quota — kept simple here for the
337
+ // sample. Demonstrates the fourth dispatcher gate (rate-limit
338
+ // → access → validation → handler).
339
+ rateLimit: { per: "user", limit: 5, windowSeconds: 60 },
340
+ });
343
341
 
344
- r.writeHandler(
345
- "invoice:archive",
346
- z.object({ id: z.uuid() }),
347
- async (event, ctx) => {
342
+ r.writeHandler({
343
+ name: "invoice:archive",
344
+ schema: z.object({ id: z.uuid() }),
345
+ handler: async (event, ctx) => {
348
346
  await ctx.archiveStream(event.payload.id, { aggregateType: "showcase-invoice" });
349
347
  return { isSuccess: true as const, data: { id: event.payload.id } };
350
348
  },
351
- { access: { roles: ["Admin"] } },
352
- );
349
+ access: { roles: ["Admin"] },
350
+ });
353
351
 
354
352
  // Persist a snapshot of the current state. In practice, features schedule
355
353
  // this from a lifecycle hook (every N events, every M minutes) — here it's
356
354
  // an explicit write-handler so the integration test can drive it.
357
- r.writeHandler(
358
- "invoice:take-snapshot",
359
- z.object({ id: z.uuid() }),
360
- async (event, ctx) => {
355
+ r.writeHandler({
356
+ name: "invoice:take-snapshot",
357
+ schema: z.object({ id: z.uuid() }),
358
+ handler: async (event, ctx) => {
361
359
  // Rebuild the state from the current stream so the snapshot reflects
362
360
  // the committed event log, not an in-memory guess. Use the upcaster-
363
361
  // aware ctx.loadAggregate to stay consistent with read-time semantics.
@@ -375,17 +373,17 @@ export const invoiceFeature = defineFeature("showcase", (r) => {
375
373
  });
376
374
  return { isSuccess: true as const, data: { id: event.payload.id, snapshotVersion: version } };
377
375
  },
378
- { access: { roles: ["Admin"] } },
379
- );
376
+ access: { roles: ["Admin"] },
377
+ });
380
378
 
381
379
  // --- Query handlers ---
382
380
 
383
381
  // Live aggregation via ctx.loadAggregate — reduces events into a state
384
382
  // snapshot. Supports asOf for point-in-time reads.
385
- r.queryHandler(
386
- "invoice:state",
387
- z.object({ id: z.uuid(), asOf: z.iso.datetime().optional() }),
388
- async (query, ctx) => {
383
+ r.queryHandler({
384
+ name: "invoice:state",
385
+ schema: z.object({ id: z.uuid(), asOf: z.iso.datetime().optional() }),
386
+ handler: async (query, ctx) => {
389
387
  const events = await ctx.loadAggregate(query.payload.id, {
390
388
  ...(query.payload.asOf ? { asOf: Temporal.Instant.from(query.payload.asOf) } : {}),
391
389
  });
@@ -395,24 +393,22 @@ export const invoiceFeature = defineFeature("showcase", (r) => {
395
393
  }
396
394
  return state;
397
395
  },
398
- {
399
- access: {
400
- openToAll: {
401
- reason:
402
- "demo recipe: any signed-in user may read the event-sourced invoice state; " +
403
- "there is no per-user invoice ownership in this sample",
404
- },
396
+ access: {
397
+ openToAll: {
398
+ reason:
399
+ "demo recipe: any signed-in user may read the event-sourced invoice state; " +
400
+ "there is no per-user invoice ownership in this sample",
405
401
  },
406
402
  },
407
- );
403
+ });
408
404
 
409
405
  // Snapshot-aware fast path. Uses the latest snapshot if available and
410
406
  // only replays delta events past it. Identical state shape to
411
407
  // invoice:state — the two are interchangeable modulo the perf profile.
412
- r.queryHandler(
413
- "invoice:fast-state",
414
- z.object({ id: z.uuid() }),
415
- async (query, ctx) => {
408
+ r.queryHandler({
409
+ name: "invoice:fast-state",
410
+ schema: z.object({ id: z.uuid() }),
411
+ handler: async (query, ctx) => {
416
412
  const result = await ctx.loadAggregateWithSnapshot<InvoiceStateRecord>(
417
413
  query.payload.id,
418
414
  (state, evt) => {
@@ -433,16 +429,14 @@ export const invoiceFeature = defineFeature("showcase", (r) => {
433
429
  snapshotHit: result.snapshotHit,
434
430
  };
435
431
  },
436
- {
437
- access: {
438
- openToAll: {
439
- reason:
440
- "demo recipe: any signed-in user may read the snapshot-accelerated invoice " +
441
- "state; there is no per-user invoice ownership in this sample",
442
- },
432
+ access: {
433
+ openToAll: {
434
+ reason:
435
+ "demo recipe: any signed-in user may read the snapshot-accelerated invoice " +
436
+ "state; there is no per-user invoice ownership in this sample",
443
437
  },
444
438
  },
445
- );
439
+ });
446
440
 
447
441
  // Read-model query via ctx.queryProjection — auto-tenant-scoped. The
448
442
  // helper collapses the zero-argument "return this projection" pattern
@@ -467,10 +461,10 @@ export const invoiceFeature = defineFeature("showcase", (r) => {
467
461
  // bytes nobody will read. The handler checks ctx.signal at every
468
462
  // iteration; on abort, throws and the dispatcher returns an error to
469
463
  // the (already-disconnected) client.
470
- r.queryHandler(
471
- "ops:slow-export",
472
- z.object({ chunks: z.number().int().min(1).max(1000) }),
473
- async (query, ctx) => {
464
+ r.queryHandler({
465
+ name: "ops:slow-export",
466
+ schema: z.object({ chunks: z.number().int().min(1).max(1000) }),
467
+ handler: async (query, ctx) => {
474
468
  const accumulated: number[] = [];
475
469
  for (let i = 0; i < query.payload.chunks; i++) {
476
470
  ctx.signal?.throwIfAborted();
@@ -481,6 +475,6 @@ export const invoiceFeature = defineFeature("showcase", (r) => {
481
475
  }
482
476
  return { processed: accumulated.length };
483
477
  },
484
- { access: { roles: ["Admin"] } },
485
- );
478
+ access: { roles: ["Admin"] },
479
+ });
486
480
  });
@@ -47,11 +47,11 @@ export function createProductFeature(): FeatureDefinition {
47
47
  return defineFeature("product", (r) => {
48
48
  r.systemScope();
49
49
  r.toggleable({ default: true });
50
- r.entity("product", productEntity);
51
- r.writeHandler(
52
- "product:create",
53
- z.object({ name: z.string().min(1).max(100) }),
54
- async (event, ctx) => {
50
+ r.entity({ name: "product", ...productEntity });
51
+ r.writeHandler({
52
+ name: "product:create",
53
+ schema: z.object({ name: z.string().min(1).max(100) }),
54
+ handler: async (event, ctx) => {
55
55
  if (!ctx.systemDb) {
56
56
  throw new InternalError({
57
57
  message: "product:create requires ctx.systemDb — is r.systemScope() still set?",
@@ -62,8 +62,8 @@ export function createProductFeature(): FeatureDefinition {
62
62
  );
63
63
  return productCrud.create(event.payload, event.user, db);
64
64
  },
65
- { access: { roles: ["SystemAdmin"] } },
66
- );
65
+ access: { roles: ["SystemAdmin"] },
66
+ });
67
67
  });
68
68
  }
69
69
 
@@ -92,20 +92,28 @@ export function createProductAuditFeature(): FeatureDefinition {
92
92
  return defineFeature("product-audit", (r) => {
93
93
  r.systemScope();
94
94
  r.toggleable({ default: true });
95
- r.entity("product-audit", productAuditEntity);
95
+ r.entity({ name: "product-audit", ...productAuditEntity });
96
96
 
97
- r.hook("postSave", { allOf: "product" }, async (result, ctx) => {
98
- if (result.kind !== "save" || !result.isNew) return;
99
- if (!ctx.systemDb) return;
100
- const name = (result.changes as Record<string, unknown>)["name"] as string | undefined;
101
- if (!name) return;
102
- const db = ctx.systemDb.acknowledgeCrossTenant(
103
- "audit sink is system-wide, mirrors the system-wide product catalog",
104
- );
105
- // Cross-feature audit sink is itself an r.entity projection — write it via
106
- // the executor (system actor, no human caller) so the row is event-backed
107
- // and survives a rebuild, not a direct method-form insert.
108
- await productAuditCrud.create({ productName: name }, createSystemUser(SYSTEM_TENANT_ID), db);
97
+ r.hook({
98
+ type: "postSave",
99
+ target: { allOf: "product" },
100
+ handler: async (result, ctx) => {
101
+ if (result.kind !== "save" || !result.isNew) return;
102
+ if (!ctx.systemDb) return;
103
+ const name = (result.changes as Record<string, unknown>)["name"] as string | undefined;
104
+ if (!name) return;
105
+ const db = ctx.systemDb.acknowledgeCrossTenant(
106
+ "audit sink is system-wide, mirrors the system-wide product catalog",
107
+ );
108
+ // Cross-feature audit sink is itself an r.entity projection — write it via
109
+ // the executor (system actor, no human caller) so the row is event-backed
110
+ // and survives a rebuild, not a direct method-form insert.
111
+ await productAuditCrud.create(
112
+ { productName: name },
113
+ createSystemUser(SYSTEM_TENANT_ID),
114
+ db,
115
+ );
116
+ },
109
117
  });
110
118
  });
111
119
  }
@@ -59,8 +59,8 @@ export const publishedPhotoIds = new Set<string>();
59
59
  export const coverOrder: string[] = [];
60
60
 
61
61
  export const filesPostProcessingFeature = defineFeature("files-post-processing", (r) => {
62
- r.requires("file-derivatives");
63
- r.entity("photo", photoEntity);
62
+ r.requires({ features: ["file-derivatives"] });
63
+ r.entity({ name: "photo", ...photoEntity });
64
64
 
65
65
  // Uploading a file to a photo's field needs the same rights as saving the
66
66
  // photo, so the photo must exist and the uploader needs update access.
@@ -71,9 +71,9 @@ export const filesPostProcessingFeature = defineFeature("files-post-processing",
71
71
 
72
72
  // Entry point 2 — imperative. A real app would gate this behind real
73
73
  // roles; `openToAll` keeps the recipe's HTTP wiring to one line.
74
- r.writeHandler(
75
- "set-cover",
76
- z.object({
74
+ r.writeHandler({
75
+ name: "set-cover",
76
+ schema: z.object({
77
77
  fileRefId: z.string(),
78
78
  // Relative to the ORIGINAL image's pixels, clamped before resize —
79
79
  // runtime data (e.g. a detected face box), which is exactly why this
@@ -87,7 +87,7 @@ export const filesPostProcessingFeature = defineFeature("files-post-processing",
87
87
  })
88
88
  .optional(),
89
89
  }),
90
- async (event, ctx) => {
90
+ handler: async (event, ctx) => {
91
91
  if (!ctx.derivatives) {
92
92
  return writeFailure(
93
93
  new InternalError({ message: "ctx.derivatives missing — is file-derivatives mounted?" }),
@@ -107,36 +107,32 @@ export const filesPostProcessingFeature = defineFeature("files-post-processing",
107
107
  coverOrder.push(fileRefId);
108
108
  return { isSuccess: true as const, data: { fileRefId, storageKey: result.storageKey } };
109
109
  },
110
- {
111
- access: {
112
- openToAll: {
113
- reason:
114
- "demo recipe: any signed-in user may generate a cover-image derivative for " +
115
- "a file; there is no per-user file ownership check in this sample",
116
- },
110
+ access: {
111
+ openToAll: {
112
+ reason:
113
+ "demo recipe: any signed-in user may generate a cover-image derivative for " +
114
+ "a file; there is no per-user file ownership check in this sample",
117
115
  },
118
116
  },
119
- );
117
+ });
120
118
 
121
119
  // A real app would gate this behind real roles; `openToAll` keeps the
122
120
  // recipe's HTTP wiring to one line.
123
- r.writeHandler(
124
- "publish",
125
- z.object({ entityId: z.string() }),
126
- async (event) => {
121
+ r.writeHandler({
122
+ name: "publish",
123
+ schema: z.object({ entityId: z.string() }),
124
+ handler: async (event) => {
127
125
  publishedPhotoIds.add(`${event.user.tenantId}:${event.payload.entityId}`);
128
126
  return { isSuccess: true as const, data: { entityId: event.payload.entityId } };
129
127
  },
130
- {
131
- access: {
132
- openToAll: {
133
- reason:
134
- "demo recipe: any signed-in user may mark a photo entity published; a real " +
135
- "app would replace this with a role check as noted above",
136
- },
128
+ access: {
129
+ openToAll: {
130
+ reason:
131
+ "demo recipe: any signed-in user may mark a photo entity published; a real " +
132
+ "app would replace this with a role check as noted above",
137
133
  },
138
134
  },
139
- );
135
+ });
140
136
 
141
137
  // Entry point 3 — default-deny. No registration here means every entityId
142
138
  // 404s; a registration that returns false does the same. Only an explicit
@@ -19,32 +19,30 @@ export const orderEntity = createEntity({
19
19
 
20
20
  const orderTable = buildEntityTable("order", orderEntity);
21
21
  export const orderFeature = defineFeature("orders", (r) => {
22
- r.entity("order", orderEntity);
22
+ r.entity({ name: "order", ...orderEntity });
23
23
 
24
24
  const orderExecutor = createEventStoreExecutor(orderTable, orderEntity, { entityName: "order" });
25
25
 
26
- r.writeHandler(
27
- "order:place",
28
- z.object({
26
+ r.writeHandler({
27
+ name: "order:place",
28
+ schema: z.object({
29
29
  customerName: z.string().min(1),
30
30
  product: z.string().min(1),
31
31
  }),
32
- async (event, ctx) =>
32
+ handler: async (event, ctx) =>
33
33
  orderExecutor.create({ ...event.payload, status: "pending" }, event.user, ctx.db),
34
- { access: { roles: ["Admin", "Customer"] } },
35
- );
34
+ access: { roles: ["Admin", "Customer"] },
35
+ });
36
36
 
37
- r.queryHandler(
38
- "order:list",
39
- z.object({ limit: z.number().optional() }),
40
- async (query, ctx) => orderExecutor.list(query.payload, query.user, ctx.db),
41
- {
42
- access: {
43
- openToAll: {
44
- reason:
45
- "any signed-in user may list orders in this recipe; placing an order still requires a role above",
46
- },
37
+ r.queryHandler({
38
+ name: "order:list",
39
+ schema: z.object({ limit: z.number().optional() }),
40
+ handler: async (query, ctx) => orderExecutor.list(query.payload, query.user, ctx.db),
41
+ access: {
42
+ openToAll: {
43
+ reason:
44
+ "any signed-in user may list orders in this recipe; placing an order still requires a role above",
47
45
  },
48
46
  },
49
- );
47
+ });
50
48
  });
@@ -40,7 +40,7 @@ export function createMailTriageFeature(): FeatureDefinition {
40
40
  r.describe(
41
41
  "Sample consumer for inbound-mail-foundation: a worker-lane job triggered by the ingest-message write-handler turns every inbound mail into a triage item.",
42
42
  );
43
- r.requires("inbound-mail-foundation");
43
+ r.requires({ features: ["inbound-mail-foundation"] });
44
44
 
45
45
  // The job receives the dispatcher-validated ingest payload — i.e.
46
46
  // the PLAINTEXT normalized mail (PII encryption happens inside the
@@ -39,18 +39,18 @@ const orderEntity = createEntity({
39
39
 
40
40
  export function createLaneRoutingFeature(): FeatureDefinition {
41
41
  return defineFeature("orders", (r) => {
42
- r.entity("order", orderEntity);
42
+ r.entity({ name: "order", ...orderEntity });
43
43
 
44
44
  // Write handler — runs in the API process (or wherever the command
45
45
  // lands). Event "orders:write:order:create" is the auto-generated
46
46
  // write-event both jobs trigger on.
47
- r.writeHandler(
48
- "order:create",
49
- z.object({
47
+ r.writeHandler({
48
+ name: "order:create",
49
+ schema: z.object({
50
50
  customerName: z.string().min(1),
51
51
  amount: z.number().positive(),
52
52
  }),
53
- async (event) => ({
53
+ handler: async (event) => ({
54
54
  isSuccess: true as const,
55
55
  data: {
56
56
  id: 1,
@@ -58,15 +58,13 @@ export function createLaneRoutingFeature(): FeatureDefinition {
58
58
  amount: event.payload.amount,
59
59
  },
60
60
  }),
61
- {
62
- access: {
63
- openToAll: {
64
- reason:
65
- "demo recipe: any signed-in user may place an order; the handler is a stub with no persistence",
66
- },
61
+ access: {
62
+ openToAll: {
63
+ reason:
64
+ "demo recipe: any signed-in user may place an order; the handler is a stub with no persistence",
67
65
  },
68
66
  },
69
- );
67
+ });
70
68
 
71
69
  // Receipt-rendering: heavy CPU work. Pinned to `worker` so the
72
70
  // BullMQ consumer on the API process never picks it up — even if the
@@ -41,7 +41,7 @@ import {
41
41
  import { createTemplateResolverFeature } from "@cosmicdrift/kumiko-bundled-features/template-resolver";
42
42
 
43
43
  // Beide Features aktivieren — template-resolver ist Foundation, legal-pages
44
- // requires sie. r.requires("template-resolver") greift automatisch im
44
+ // requires sie. r.requires({ features: ["template-resolver"] }) greift automatisch im
45
45
  // legal-pages-Feature.
46
46
  export const templateResolverFeature = createTemplateResolverFeature();
47
47
  export const legalPagesFeature = createLegalPagesFeature();
@@ -12,19 +12,19 @@ back.
12
12
 
13
13
  ## What it shows
14
14
 
15
- - **`r.hook("validation", handlerRef, fn)`** — runs before the
15
+ - **`r.hook({ type: "validation", target: handlerRef, handler })`** — runs before the
16
16
  handler body executes. Returns either `null` (pass) or an array of
17
17
  `{ field, error }` objects (fail). Used here to reject titles
18
18
  containing the word "spam" and titles longer than 200 characters.
19
- - **`r.hook("postSave", { allOf: entityRef }, fn)`** — fires after every
19
+ - **`r.hook({ type: "postSave", target: { allOf: entityRef }, handler })`** — fires after every
20
20
  successful save (create or update) in the same transaction.
21
21
  `result.isNew` distinguishes create from update; `result.changes`
22
22
  carries the changed fields only.
23
- - **`r.hook("preDelete", { allOf: entityRef }, fn)`** — fires inside the
23
+ - **`r.hook({ type: "preDelete", target: { allOf: entityRef }, handler })`** — fires inside the
24
24
  delete transaction. Throws on invariant violations (here: "published
25
25
  articles cannot be deleted") to roll the delete back. The handler
26
26
  receives the full row snapshot — no extra load needed.
27
- - **`r.hook("postDelete", { allOf: entityRef }, fn)`** — fires after commit.
27
+ - **`r.hook({ type: "postDelete", target: { allOf: entityRef }, handler })`** — fires after commit.
28
28
  External side effects (logs, notifications) belong here because
29
29
  failures cannot roll the delete back.
30
30