@cosmicdrift/kumiko-samples 0.365.1 → 0.367.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 (208) 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/changes.json +6 -0
  23. package/packages/bundled-features/src/channel-email/email-channel.ts +12 -2
  24. package/packages/bundled-features/src/channel-email/feature.ts +2 -2
  25. package/packages/bundled-features/src/channel-in-app/feature.ts +1 -1
  26. package/packages/bundled-features/src/channel-push/feature.ts +1 -1
  27. package/packages/bundled-features/src/channel-slack/feature.ts +2 -2
  28. package/packages/bundled-features/src/channel-teams/feature.ts +2 -2
  29. package/packages/bundled-features/src/channel-telegram/feature.ts +2 -2
  30. package/packages/bundled-features/src/compliance-profiles/feature.ts +1 -1
  31. package/packages/bundled-features/src/compliance-profiles-ops/index.ts +3 -3
  32. package/packages/bundled-features/src/config/feature.ts +1 -1
  33. package/packages/bundled-features/src/crypto-shredding/changes.json +6 -0
  34. package/packages/bundled-features/src/custom-fields/feature.ts +2 -2
  35. package/packages/bundled-features/src/custom-fields/wire-for-entity.ts +17 -13
  36. package/packages/bundled-features/src/data-retention/feature.ts +1 -1
  37. package/packages/bundled-features/src/data-retention/presets.ts +4 -4
  38. package/packages/bundled-features/src/delivery/changes.json +18 -0
  39. package/packages/bundled-features/src/delivery/channel-context.ts +1 -0
  40. package/packages/bundled-features/src/delivery/db/queries/address-opt-outs.ts +6 -2
  41. package/packages/bundled-features/src/delivery/db/queries/preferences.ts +4 -4
  42. package/packages/bundled-features/src/delivery/delivery-service.ts +71 -4
  43. package/packages/bundled-features/src/delivery/feature.ts +10 -6
  44. package/packages/bundled-features/src/delivery/handlers/resubscribe-address.write.ts +12 -8
  45. package/packages/bundled-features/src/delivery/handlers/resubscribe-user.write.ts +3 -1
  46. package/packages/bundled-features/src/delivery/handlers/set-preference.write.ts +3 -1
  47. package/packages/bundled-features/src/delivery/handlers/unsubscribe-address.write.ts +13 -6
  48. package/packages/bundled-features/src/delivery/handlers/unsubscribe-user.write.ts +3 -1
  49. package/packages/bundled-features/src/delivery/index.ts +6 -0
  50. package/packages/bundled-features/src/delivery/notification-type-aliases.ts +13 -0
  51. package/packages/bundled-features/src/delivery/testing.ts +2 -0
  52. package/packages/bundled-features/src/delivery/types.ts +4 -0
  53. package/packages/bundled-features/src/delivery/unsubscribe-link.ts +73 -0
  54. package/packages/bundled-features/src/delivery/unsubscribe-page-strings.ts +17 -0
  55. package/packages/bundled-features/src/delivery/unsubscribe.ts +94 -31
  56. package/packages/bundled-features/src/derivatives-sharp/feature.ts +1 -1
  57. package/packages/bundled-features/src/document-ingest-foundation/feature.ts +2 -2
  58. package/packages/bundled-features/src/file-derivatives/feature.ts +3 -3
  59. package/packages/bundled-features/src/file-foundation/feature.ts +1 -1
  60. package/packages/bundled-features/src/file-provider-inmemory/feature.ts +2 -2
  61. package/packages/bundled-features/src/file-provider-s3/feature.ts +3 -3
  62. package/packages/bundled-features/src/file-provider-s3-env/feature.ts +2 -2
  63. package/packages/bundled-features/src/files/README.md +1 -1
  64. package/packages/bundled-features/src/files-tenant-data/index.ts +1 -1
  65. package/packages/bundled-features/src/folders/feature.ts +2 -2
  66. package/packages/bundled-features/src/folders-user-data/index.ts +1 -1
  67. package/packages/bundled-features/src/form-draft/feature.ts +2 -2
  68. package/packages/bundled-features/src/form-draft-user-data/index.ts +1 -1
  69. package/packages/bundled-features/src/inbound-mail-foundation/changes.json +13 -0
  70. package/packages/bundled-features/src/inbound-mail-foundation/connect-routes.ts +9 -1
  71. package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +1 -1
  72. package/packages/bundled-features/src/inbound-mail-foundation/types.ts +19 -7
  73. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +21 -2
  74. package/packages/bundled-features/src/inbound-provider-imap/feature.ts +2 -2
  75. package/packages/bundled-features/src/inbound-provider-inmemory/feature.ts +1 -1
  76. package/packages/bundled-features/src/ledger/feature.ts +5 -5
  77. package/packages/bundled-features/src/legal-pages/feature.ts +1 -1
  78. package/packages/bundled-features/src/mail-foundation/feature.ts +51 -10
  79. package/packages/bundled-features/src/mail-foundation/index.ts +3 -0
  80. package/packages/bundled-features/src/mail-transport-inmemory/feature.ts +2 -2
  81. package/packages/bundled-features/src/mail-transport-smtp/feature.ts +3 -3
  82. package/packages/bundled-features/src/managed-pages/css-gate.ts +1 -1
  83. package/packages/bundled-features/src/managed-pages/feature.ts +2 -2
  84. package/packages/bundled-features/src/metrics/feature.ts +2 -2
  85. package/packages/bundled-features/src/notes-history/feature.ts +2 -2
  86. package/packages/bundled-features/src/notes-history-user-data/index.ts +1 -1
  87. package/packages/bundled-features/src/personal-access-tokens/feature.ts +15 -11
  88. package/packages/bundled-features/src/readiness/feature.ts +2 -2
  89. package/packages/bundled-features/src/renderer-foundation/README.md +1 -1
  90. package/packages/bundled-features/src/renderer-foundation/feature.ts +1 -1
  91. package/packages/bundled-features/src/renderer-pdf/feature.ts +1 -1
  92. package/packages/bundled-features/src/secrets/feature.ts +1 -1
  93. package/packages/bundled-features/src/seo/feature.ts +1 -1
  94. package/packages/bundled-features/src/sessions/feature.ts +17 -13
  95. package/packages/bundled-features/src/step-dispatcher/feature.ts +1 -1
  96. package/packages/bundled-features/src/subscription-mollie/feature.ts +1 -1
  97. package/packages/bundled-features/src/subscription-stripe/feature.ts +3 -3
  98. package/packages/bundled-features/src/tags/feature.ts +2 -2
  99. package/packages/bundled-features/src/template-resolver/changes.json +25 -0
  100. package/packages/bundled-features/src/template-resolver/compile-document-body.ts +61 -9
  101. package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +19 -0
  102. package/packages/bundled-features/src/template-resolver/document-schema.ts +33 -0
  103. package/packages/bundled-features/src/template-resolver/feature.ts +2 -2
  104. package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +9 -1
  105. package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +8 -2
  106. package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +8 -1
  107. package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +6 -1
  108. package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +7 -2
  109. package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +21 -1
  110. package/packages/bundled-features/src/template-resolver/index.ts +4 -0
  111. package/packages/bundled-features/src/template-resolver/qualified-names.ts +1 -0
  112. package/packages/bundled-features/src/template-resolver/seeding.ts +54 -7
  113. package/packages/bundled-features/src/template-resolver/template-preview.ts +2 -0
  114. package/packages/bundled-features/src/template-resolver/template-system-only.ts +21 -0
  115. package/packages/bundled-features/src/template-resolver/template-validation.ts +48 -24
  116. package/packages/bundled-features/src/template-resolver/web/i18n.ts +2 -0
  117. package/packages/bundled-features/src/template-resolver-user-data/index.ts +1 -1
  118. package/packages/bundled-features/src/tenant/feature.ts +4 -4
  119. package/packages/bundled-features/src/tenant/seeding.ts +1 -1
  120. package/packages/bundled-features/src/tenant-lifecycle/feature.ts +2 -3
  121. package/packages/bundled-features/src/tenant-settings/feature.ts +1 -1
  122. package/packages/bundled-features/src/tier-engine/feature.ts +36 -27
  123. package/packages/bundled-features/src/user/feature.ts +1 -1
  124. package/packages/bundled-features/src/user-data-rights/README.md +1 -1
  125. package/packages/bundled-features/src/user-data-rights/changes.json +6 -0
  126. package/packages/bundled-features/src/user-data-rights/feature.ts +4 -4
  127. package/packages/bundled-features/src/user-data-rights-defaults/feature.ts +2 -2
  128. package/packages/bundled-features/src/user-profile/feature.ts +4 -4
  129. package/packages/bundled-features/src/waitlist/feature.ts +2 -2
  130. package/samples/apps/cap-billing-demo/README.md +1 -1
  131. package/samples/apps/cap-billing-demo/src/feature.ts +4 -4
  132. package/samples/apps/cap-billing-demo/src/run-config.ts +1 -1
  133. package/samples/apps/config-demo/src/features/demo/feature.ts +2 -2
  134. package/samples/apps/styleguide/src/features/ai-settings/feature.ts +43 -28
  135. package/samples/apps/styleguide/src/features/demo/feature.ts +18 -18
  136. package/samples/apps/styleguide/src/features/widgets/ai-text-demo.ts +10 -12
  137. package/samples/apps/styleguide/src/features/widgets/feature.ts +90 -108
  138. package/samples/apps/ui-walkthrough/src/app/seed-templates.ts +2 -0
  139. package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +12 -0
  140. package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +7 -0
  141. package/samples/apps/ui-walkthrough/src/features/billing/i18n.ts +2 -0
  142. package/samples/apps/ui-walkthrough/src/features/rental/feature.ts +36 -36
  143. package/samples/apps/ui-walkthrough/src/features/vehicles/feature.ts +6 -6
  144. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +2 -2
  145. package/samples/apps/use-all-bundled/feature-manifest.json +4 -4
  146. package/samples/apps/use-all-bundled/package.json +1 -0
  147. package/samples/apps/use-all-bundled/scripts/gen-sample-index.ts +7 -7
  148. package/samples/apps/use-all-bundled/src/app/notes-feature.ts +3 -3
  149. package/samples/apps/use-all-bundled/src/app/screens-feature.ts +1 -1
  150. package/samples/apps/user-data-rights-demo/src/feature.ts +5 -5
  151. package/samples/recipes/access-control/src/feature.ts +21 -25
  152. package/samples/recipes/anonymous-access/src/feature.ts +36 -43
  153. package/samples/recipes/anonymous-access-multitenant/src/feature.ts +14 -16
  154. package/samples/recipes/auth-foundation-providers/src/feature.ts +1 -1
  155. package/samples/recipes/basic-entity/README.md +1 -1
  156. package/samples/recipes/boot-check/README.md +1 -1
  157. package/samples/recipes/boot-check/src/feature.ts +2 -2
  158. package/samples/recipes/chat-channels/src/feature.ts +1 -1
  159. package/samples/recipes/cross-feature-events/src/feature.ts +7 -7
  160. package/samples/recipes/currencies-global/src/feature.ts +3 -3
  161. package/samples/recipes/currencies-per-tenant/src/feature.ts +2 -2
  162. package/samples/recipes/custom-fields-basic/README.md +1 -1
  163. package/samples/recipes/custom-fields-basic/src/feature.ts +3 -2
  164. package/samples/recipes/custom-handlers/README.md +3 -3
  165. package/samples/recipes/custom-handlers/src/feature.ts +37 -41
  166. package/samples/recipes/dashboard-stats/src/feature.ts +6 -6
  167. package/samples/recipes/delivery-notifications/README.md +1 -1
  168. package/samples/recipes/delivery-notifications/src/feature.ts +8 -8
  169. package/samples/recipes/designer-demo/src/feature.ts +2 -1
  170. package/samples/recipes/embedded/src/feature.ts +1 -1
  171. package/samples/recipes/embedded-entity-form/src/feature.ts +13 -13
  172. package/samples/recipes/encrypted-tenant-config/src/feature.ts +1 -1
  173. package/samples/recipes/error-contract/src/feature.ts +25 -25
  174. package/samples/recipes/event-sourcing/src/feature.ts +70 -76
  175. package/samples/recipes/feature-toggles/src/feature.ts +28 -20
  176. package/samples/recipes/files-post-processing/src/feature.ts +22 -26
  177. package/samples/recipes/idempotency/src/feature.ts +16 -18
  178. package/samples/recipes/inbound-mail-triage/src/feature.ts +1 -1
  179. package/samples/recipes/lane-routing/src/feature.ts +10 -12
  180. package/samples/recipes/legal-pages/src/feature.ts +1 -1
  181. package/samples/recipes/lifecycle-hooks/README.md +4 -4
  182. package/samples/recipes/lifecycle-hooks/src/feature.ts +55 -35
  183. package/samples/recipes/managed-config/src/feature.ts +8 -8
  184. package/samples/recipes/managed-pages/src/feature.ts +1 -1
  185. package/samples/recipes/notes-history-basic/README.md +1 -1
  186. package/samples/recipes/notes-history-basic/src/feature.ts +2 -2
  187. package/samples/recipes/pdf-audit-export/src/feature.ts +7 -7
  188. package/samples/recipes/pipeline-basics/README.md +1 -1
  189. package/samples/recipes/pipeline-basics/src/feature.ts +7 -7
  190. package/samples/recipes/public-share-token/src/feature.ts +1 -1
  191. package/samples/recipes/rate-limiting/src/feature.ts +7 -9
  192. package/samples/recipes/record-detail-layout/src/feature.ts +18 -18
  193. package/samples/recipes/reference-data/src/feature.ts +1 -1
  194. package/samples/recipes/relations/src/feature.ts +2 -2
  195. package/samples/recipes/screens-nav/src/feature.ts +1 -1
  196. package/samples/recipes/search/src/feature.ts +17 -19
  197. package/samples/recipes/security-baseline/README.md +1 -1
  198. package/samples/recipes/state-machine/src/feature.ts +1 -1
  199. package/samples/recipes/tags-basic/README.md +1 -1
  200. package/samples/recipes/tags-basic/src/feature.ts +2 -2
  201. package/samples/recipes/tenant-settings/README.md +1 -1
  202. package/samples/recipes/tenant-settings/src/feature.ts +1 -1
  203. package/samples/recipes/timezones/src/feature.ts +12 -12
  204. package/samples/recipes/user-data-rights/README.md +2 -2
  205. package/samples/recipes/user-data-rights/src/feature.ts +1 -1
  206. package/samples/recipes/user-profile/src/feature.ts +1 -1
  207. package/samples/recipes/webhook-step/src/feature.ts +1 -1
  208. package/samples/recipes/writeform-section/src/feature.ts +12 -12
@@ -46,10 +46,10 @@ function cannedText(payload: RunInput): string {
46
46
  }
47
47
 
48
48
  export const aiTextDemoFeature = defineFeature("ai-text", (r) => {
49
- r.queryHandler(
50
- "run",
51
- runInput,
52
- async ({ payload }) => {
49
+ r.queryHandler({
50
+ name: "run",
51
+ schema: runInput,
52
+ handler: async ({ payload }) => {
53
53
  const text = cannedText(payload);
54
54
  return {
55
55
  type: "text" as const,
@@ -57,14 +57,12 @@ export const aiTextDemoFeature = defineFeature("ai-text", (r) => {
57
57
  usage: { inputTokens: payload.text.length, outputTokens: text.length },
58
58
  };
59
59
  },
60
- {
61
- access: {
62
- openToAll: {
63
- reason:
64
- "demo stand-in for a real LLM call: returns canned text transforms with no " +
65
- "side effects or per-user data; any signed-in user may query it",
66
- },
60
+ access: {
61
+ openToAll: {
62
+ reason:
63
+ "demo stand-in for a real LLM call: returns canned text transforms with no " +
64
+ "side effects or per-user data; any signed-in user may query it",
67
65
  },
68
66
  },
69
- );
67
+ });
70
68
  });
@@ -198,10 +198,10 @@ export const widgetsFeature = defineFeature("widgets", (r) => {
198
198
  ],
199
199
  });
200
200
 
201
- r.queryHandler(
202
- "metrics:portfolio-stat",
203
- z.object({ region: z.string().optional() }),
204
- async ({ payload: { region } }, ctx) => ({
201
+ r.queryHandler({
202
+ name: "metrics:portfolio-stat",
203
+ schema: z.object({ region: z.string().optional() }),
204
+ handler: async ({ payload: { region } }, ctx) => ({
205
205
  value:
206
206
  region === "us"
207
207
  ? demoMoney(38120, "USD", ctx.locale)
@@ -212,86 +212,76 @@ export const widgetsFeature = defineFeature("widgets", (r) => {
212
212
  deltaDirection: "up",
213
213
  deltaTone: "positive",
214
214
  }),
215
- {
216
- access: {
217
- openToAll: {
218
- reason:
219
- "demo dashboard widget: returns static canned data with no per-user or " +
220
- "tenant scoping; any signed-in user may query it",
221
- },
215
+ access: {
216
+ openToAll: {
217
+ reason:
218
+ "demo dashboard widget: returns static canned data with no per-user or " +
219
+ "tenant scoping; any signed-in user may query it",
222
220
  },
223
221
  },
224
- );
225
- r.queryHandler(
226
- "metrics:net-worth-assets",
227
- z.object({ region: z.string().optional() }),
228
- async (_query, ctx) => ({ value: demoMoney(120000, "EUR", ctx.locale) }),
229
- {
230
- access: {
231
- openToAll: {
232
- reason:
233
- "demo dashboard widget: returns static canned data with no per-user or " +
234
- "tenant scoping; any signed-in user may query it",
235
- },
222
+ });
223
+ r.queryHandler({
224
+ name: "metrics:net-worth-assets",
225
+ schema: z.object({ region: z.string().optional() }),
226
+ handler: async (_query, ctx) => ({ value: demoMoney(120000, "EUR", ctx.locale) }),
227
+ access: {
228
+ openToAll: {
229
+ reason:
230
+ "demo dashboard widget: returns static canned data with no per-user or " +
231
+ "tenant scoping; any signed-in user may query it",
236
232
  },
237
233
  },
238
- );
239
- r.queryHandler(
240
- "metrics:net-worth-debts",
241
- z.object({ region: z.string().optional() }),
242
- async (_query, ctx) => ({ value: demoMoney(65370, "EUR", ctx.locale) }),
243
- {
244
- access: {
245
- openToAll: {
246
- reason:
247
- "demo dashboard widget: returns static canned data with no per-user or " +
248
- "tenant scoping; any signed-in user may query it",
249
- },
234
+ });
235
+ r.queryHandler({
236
+ name: "metrics:net-worth-debts",
237
+ schema: z.object({ region: z.string().optional() }),
238
+ handler: async (_query, ctx) => ({ value: demoMoney(65370, "EUR", ctx.locale) }),
239
+ access: {
240
+ openToAll: {
241
+ reason:
242
+ "demo dashboard widget: returns static canned data with no per-user or " +
243
+ "tenant scoping; any signed-in user may query it",
250
244
  },
251
245
  },
252
- );
253
- r.queryHandler(
254
- "metrics:response-times",
255
- z.object({}),
256
- async () => ({
246
+ });
247
+ r.queryHandler({
248
+ name: "metrics:response-times",
249
+ schema: z.object({}),
250
+ handler: async () => ({
257
251
  points: RESPONSE_POINTS,
258
252
  windowStartMs: 0,
259
253
  windowEndMs: 24 * 60 * 60 * 1000,
260
254
  }),
261
- {
262
- access: {
263
- openToAll: {
264
- reason:
265
- "demo dashboard widget: returns static canned data with no per-user or " +
266
- "tenant scoping; any signed-in user may query it",
267
- },
255
+ access: {
256
+ openToAll: {
257
+ reason:
258
+ "demo dashboard widget: returns static canned data with no per-user or " +
259
+ "tenant scoping; any signed-in user may query it",
268
260
  },
269
261
  },
270
- );
271
- r.queryHandler(
272
- "metrics:loan",
273
- z.object({}),
274
- async () => ({
262
+ });
263
+ r.queryHandler({
264
+ name: "metrics:loan",
265
+ schema: z.object({}),
266
+ handler: async () => ({
275
267
  series: LOAN_SERIES,
276
268
  markers: LOAN_MARKERS,
277
269
  windowStartMs: LOAN_START_MS,
278
270
  windowEndMs: loanMonthMs(LOAN_MONTHS),
279
271
  todayMs: LOAN_TODAY_MS,
280
272
  }),
281
- {
282
- access: {
283
- openToAll: {
284
- reason:
285
- "demo dashboard widget: returns static canned data with no per-user or " +
286
- "tenant scoping; any signed-in user may query it",
287
- },
273
+ access: {
274
+ openToAll: {
275
+ reason:
276
+ "demo dashboard widget: returns static canned data with no per-user or " +
277
+ "tenant scoping; any signed-in user may query it",
288
278
  },
289
279
  },
290
- );
291
- r.queryHandler(
292
- "metrics:latest-items",
293
- z.object({}),
294
- async (_query, ctx) => ({
280
+ });
281
+ r.queryHandler({
282
+ name: "metrics:latest-items",
283
+ schema: z.object({}),
284
+ handler: async (_query, ctx) => ({
295
285
  rows: [
296
286
  {
297
287
  id: "i1",
@@ -306,25 +296,23 @@ export const widgetsFeature = defineFeature("widgets", (r) => {
306
296
  ],
307
297
  nextCursor: null,
308
298
  }),
309
- {
310
- access: {
311
- openToAll: {
312
- reason:
313
- "demo dashboard widget: returns static canned data with no per-user or " +
314
- "tenant scoping; any signed-in user may query it",
315
- },
299
+ access: {
300
+ openToAll: {
301
+ reason:
302
+ "demo dashboard widget: returns static canned data with no per-user or " +
303
+ "tenant scoping; any signed-in user may query it",
316
304
  },
317
305
  },
318
- );
319
- r.queryHandler(
320
- "metrics:inbox-messages",
321
- z.object({
306
+ });
307
+ r.queryHandler({
308
+ name: "metrics:inbox-messages",
309
+ schema: z.object({
322
310
  cursor: z.coerce.number().int().min(0).optional(),
323
311
  limit: z.number().int().min(1).max(100).optional(),
324
312
  unreadOnly: z.boolean().optional(),
325
313
  search: z.string().optional(),
326
314
  }),
327
- async ({ payload: { cursor, limit, unreadOnly, search } }) => {
315
+ handler: async ({ payload: { cursor, limit, unreadOnly, search } }) => {
328
316
  const term = search?.trim().toLowerCase() ?? "";
329
317
  const filtered = INBOX_MESSAGES.filter(
330
318
  (m) =>
@@ -339,20 +327,18 @@ export const widgetsFeature = defineFeature("widgets", (r) => {
339
327
  const nextCursor = start + pageSize < filtered.length ? String(start + pageSize) : null;
340
328
  return { rows, nextCursor };
341
329
  },
342
- {
343
- access: {
344
- openToAll: {
345
- reason:
346
- "demo dashboard widget: returns static canned data with no per-user or " +
347
- "tenant scoping; any signed-in user may query it",
348
- },
330
+ access: {
331
+ openToAll: {
332
+ reason:
333
+ "demo dashboard widget: returns static canned data with no per-user or " +
334
+ "tenant scoping; any signed-in user may query it",
349
335
  },
350
336
  },
351
- );
352
- r.queryHandler(
353
- "metrics:upcoming-events",
354
- z.object({}),
355
- async (_query, ctx) => ({
337
+ });
338
+ r.queryHandler({
339
+ name: "metrics:upcoming-events",
340
+ schema: z.object({}),
341
+ handler: async (_query, ctx) => ({
356
342
  rows: [
357
343
  {
358
344
  primary: demoText("widgets:dashboard:demo-event-rate-adjustment", ctx.locale),
@@ -364,20 +350,18 @@ export const widgetsFeature = defineFeature("widgets", (r) => {
364
350
  },
365
351
  ],
366
352
  }),
367
- {
368
- access: {
369
- openToAll: {
370
- reason:
371
- "demo dashboard widget: returns static canned data with no per-user or " +
372
- "tenant scoping; any signed-in user may query it",
373
- },
353
+ access: {
354
+ openToAll: {
355
+ reason:
356
+ "demo dashboard widget: returns static canned data with no per-user or " +
357
+ "tenant scoping; any signed-in user may query it",
374
358
  },
375
359
  },
376
- );
377
- r.queryHandler(
378
- "metrics:goal-progress",
379
- z.object({}),
380
- async (_query, ctx) => ({
360
+ });
361
+ r.queryHandler({
362
+ name: "metrics:goal-progress",
363
+ schema: z.object({}),
364
+ handler: async (_query, ctx) => ({
381
365
  rows: [
382
366
  {
383
367
  label: demoText("widgets:dashboard:demo-goal-mortgage", ctx.locale),
@@ -391,16 +375,14 @@ export const widgetsFeature = defineFeature("widgets", (r) => {
391
375
  },
392
376
  ],
393
377
  }),
394
- {
395
- access: {
396
- openToAll: {
397
- reason:
398
- "demo dashboard widget: returns static canned data with no per-user or " +
399
- "tenant scoping; any signed-in user may query it",
400
- },
378
+ access: {
379
+ openToAll: {
380
+ reason:
381
+ "demo dashboard widget: returns static canned data with no per-user or " +
382
+ "tenant scoping; any signed-in user may query it",
401
383
  },
402
384
  },
403
- );
385
+ });
404
386
 
405
387
  r.translations({ keys: WIDGETS_I18N });
406
388
 
@@ -32,6 +32,7 @@ import {
32
32
  SYSTEM_INVOICE_ISSUED,
33
33
  SYSTEM_PDF_LAYOUT,
34
34
  SYSTEM_TENANCY_CERTIFICATE,
35
+ SYSTEM_TERMS_CHANGED,
35
36
  TENANT_INVOICE_OVERRIDES,
36
37
  TENANT_MAIL_LAYOUT_VERSIONS,
37
38
  TENANT_UTILITY_STATEMENT,
@@ -165,6 +166,7 @@ export const seedTemplates: SeedFn = async (stack) => {
165
166
  await upsertSystem(SYSTEM_PDF_LAYOUT);
166
167
  for (const mail of SYSTEM_AUTH_MAILS) await upsertSystem(mail);
167
168
  await upsertSystem(SYSTEM_INVOICE_ISSUED);
169
+ await upsertSystem(SYSTEM_TERMS_CHANGED);
168
170
  for (const document of SYSTEM_INVOICE_DOCUMENTS) await upsertSystem(document);
169
171
  const certificate = await upsertSystem(SYSTEM_TENANCY_CERTIFICATE);
170
172
  await stack.http.writeOk(
@@ -276,6 +276,18 @@ export const INVOICE_CORRECTED_DRAFT: SeedTemplate = {
276
276
  meta: invoiceIssuedMeta("Ihre Rechnung {{invoiceNumber}} von {{tenantName}}"),
277
277
  };
278
278
 
279
+ export const SYSTEM_TERMS_CHANGED: SeedTemplate = {
280
+ slug: "billing:notify:terms-changed",
281
+ kind: "mail",
282
+ locale: "de",
283
+ title: "AGB-Änderung",
284
+ content: [
285
+ `<p>Guten Tag ${v("customerName")},</p>`,
286
+ `<p>zum ${v("effectiveDate")} ändern sich die Allgemeinen Geschäftsbedingungen des Mieterportals.</p>`,
287
+ ].join(""),
288
+ meta: { subject: "Änderung der Geschäftsbedingungen" },
289
+ };
290
+
279
291
  export const PAYMENT_REMINDER_TEMPLATES: readonly SeedTemplate[] = [
280
292
  {
281
293
  slug: "billing:notify:payment-reminder",
@@ -63,10 +63,17 @@ const PAYMENT_REMINDER_VARIABLES = {
63
63
  invoiceUrl: templateVariable.link({ required: true }),
64
64
  } as const;
65
65
 
66
+ const TERMS_CHANGED_VARIABLES = {
67
+ customerName: templateVariable.text({ required: true, example: "Familie Yilmaz" }),
68
+ effectiveDate: templateVariable.date({ required: true, example: "2026-12-01" }),
69
+ } as const;
70
+
66
71
  export const billingFeature = defineFeature("billing", (r) => {
67
72
  r.translations({ keys: toKeyFirst(billingTranslations) });
68
73
 
69
74
  r.notification("invoice-issued", { variables: INVOICE_ISSUED_VARIABLES });
70
75
  r.notification("invoice-corrected", { variables: INVOICE_ISSUED_VARIABLES });
71
76
  r.notification("payment-reminder", { variables: PAYMENT_REMINDER_VARIABLES });
77
+ // Legal wording the platform owns: tenant admins neither see nor override it.
78
+ r.notification("terms-changed", { variables: TERMS_CHANGED_VARIABLES, systemOnly: true });
72
79
  });
@@ -8,11 +8,13 @@ export const billingTranslations: TranslationsByLocale = {
8
8
  "billing:notify.invoice-issued.label": "Rechnung gestellt",
9
9
  "billing:notify.invoice-corrected.label": "Rechnung korrigiert",
10
10
  "billing:notify.payment-reminder.label": "Mahnlauf",
11
+ "billing:notify.terms-changed.label": "AGB-Änderung",
11
12
  },
12
13
  en: {
13
14
  "billing:feature.label": "Invoices",
14
15
  "billing:notify.invoice-issued.label": "Invoice issued",
15
16
  "billing:notify.invoice-corrected.label": "Invoice corrected",
16
17
  "billing:notify.payment-reminder.label": "Dunning run",
18
+ "billing:notify.terms-changed.label": "Terms update",
17
19
  },
18
20
  };
@@ -71,10 +71,10 @@ export const rentalFeature = defineFeature("rental", (r) => {
71
71
  r.crud("leasePosition", leasePositionEntity, { write: open, read: open });
72
72
  r.crud("leaseParty", leasePartyEntity, { write: open, read: open });
73
73
 
74
- r.queryHandler(
75
- "lease:akte",
76
- idPayloadSchema,
77
- async (query, ctx) => {
74
+ r.queryHandler({
75
+ name: "lease:akte",
76
+ schema: idPayloadSchema,
77
+ handler: async (query, ctx) => {
78
78
  const row = await leaseExecutor.detail({ id: query.payload.id }, query.user, ctx.db);
79
79
  if (!row) return failNotFound("lease", query.payload.id);
80
80
  const lease = parseLeaseRow(row);
@@ -104,13 +104,13 @@ export const rentalFeature = defineFeature("rental", (r) => {
104
104
  positionCount: await countChildren(positionExecutor),
105
105
  };
106
106
  },
107
- open,
108
- );
107
+ ...open,
108
+ });
109
109
 
110
- r.queryHandler(
111
- "lease:positions",
112
- idPayloadSchema,
113
- async (query, ctx) => {
110
+ r.queryHandler({
111
+ name: "lease:positions",
112
+ schema: idPayloadSchema,
113
+ handler: async (query, ctx) => {
114
114
  const page = await positionExecutor.list(
115
115
  { filter: { field: "lease", op: "eq", value: query.payload.id }, limit: 100 },
116
116
  query.user,
@@ -132,13 +132,13 @@ export const rentalFeature = defineFeature("rental", (r) => {
132
132
  }),
133
133
  };
134
134
  },
135
- open,
136
- );
135
+ ...open,
136
+ });
137
137
 
138
- r.queryHandler(
139
- "lease:kennzahlen",
140
- z.object({}),
141
- async () => ({
138
+ r.queryHandler({
139
+ name: "lease:kennzahlen",
140
+ schema: z.object({}),
141
+ handler: async () => ({
142
142
  debt: "123.456 €",
143
143
  debtSub: "nach 10 Jahren",
144
144
  debtDelta: "15 %",
@@ -146,13 +146,13 @@ export const rentalFeature = defineFeature("rental", (r) => {
146
146
  rent: "48.200 €",
147
147
  rentSub: "319 Verträge",
148
148
  }),
149
- open,
150
- );
149
+ ...open,
150
+ });
151
151
 
152
- r.writeHandler(
153
- "lease:terminate",
154
- idPayloadSchema,
155
- async (event, ctx) =>
152
+ r.writeHandler({
153
+ name: "lease:terminate",
154
+ schema: idPayloadSchema,
155
+ handler: async (event, ctx) =>
156
156
  leaseExecutor.update(
157
157
  {
158
158
  id: event.payload.id,
@@ -165,13 +165,13 @@ export const rentalFeature = defineFeature("rental", (r) => {
165
165
  ctx.db,
166
166
  { skipOptimisticLock: true },
167
167
  ),
168
- open,
169
- );
168
+ ...open,
169
+ });
170
170
 
171
- r.writeHandler(
172
- "lease:record-positions",
173
- recordPositionsPayloadSchema,
174
- async (event, ctx) => {
171
+ r.writeHandler({
172
+ name: "lease:record-positions",
173
+ schema: recordPositionsPayloadSchema,
174
+ handler: async (event, ctx) => {
175
175
  let created: Awaited<ReturnType<typeof positionExecutor.create>> | undefined;
176
176
  const unitLabels = new Map<string, string | null | undefined>();
177
177
  for (const unitId of new Set(event.payload.items.map((item) => item.unit))) {
@@ -189,13 +189,13 @@ export const rentalFeature = defineFeature("rental", (r) => {
189
189
  }
190
190
  return created ?? failNotFound("leasePosition", event.payload.lease);
191
191
  },
192
- open,
193
- );
192
+ ...open,
193
+ });
194
194
 
195
- r.writeHandler(
196
- "rent:adjust",
197
- rentAdjustPayloadSchema,
198
- async (event, ctx) => {
195
+ r.writeHandler({
196
+ name: "rent:adjust",
197
+ schema: rentAdjustPayloadSchema,
198
+ handler: async (event, ctx) => {
199
199
  const { positionId, wirksamAb, neuerBetrag } = event.payload;
200
200
  const current = await positionExecutor.detail({ id: positionId }, event.user, ctx.db);
201
201
  if (!current) return failNotFound("leasePosition", positionId);
@@ -229,8 +229,8 @@ export const rentalFeature = defineFeature("rental", (r) => {
229
229
  { skipOptimisticLock: true },
230
230
  );
231
231
  },
232
- open,
233
- );
232
+ ...open,
233
+ });
234
234
 
235
235
  r.screen(leaseListScreen);
236
236
  r.screen(leaseListShortScreen);
@@ -32,10 +32,10 @@ export const vehiclesFeature = defineFeature("vehicles", (r) => {
32
32
  r.crud("campaign", campaignEntity, { write: open, read: open });
33
33
  r.crud("campaignPost", campaignPostEntity, { write: open, read: open });
34
34
 
35
- r.writeHandler(
36
- "campaign-post:mark-posted",
37
- markPostedPayloadSchema,
38
- async (event, ctx) => {
35
+ r.writeHandler({
36
+ name: "campaign-post:mark-posted",
37
+ schema: markPostedPayloadSchema,
38
+ handler: async (event, ctx) => {
39
39
  const current = await campaignPostExecutor.detail(
40
40
  { id: event.payload.id },
41
41
  event.user,
@@ -62,8 +62,8 @@ export const vehiclesFeature = defineFeature("vehicles", (r) => {
62
62
  { skipOptimisticLock: true },
63
63
  );
64
64
  },
65
- open,
66
- );
65
+ ...open,
66
+ });
67
67
 
68
68
  r.screen(vehicleListScreen);
69
69
  r.screen(vehicleEditScreen);
@@ -117,14 +117,14 @@ export const galleryProviderFeature: FeatureDefinition = defineFeature(
117
117
  "gallery-mock-provider",
118
118
  (r) => {
119
119
  r.describe("E2E-only mock subscription provider for the consent/termination gallery.");
120
- r.requires("billing-foundation");
120
+ r.requires({ features: ["billing-foundation"] });
121
121
  r.useExtension("subscriptionProvider", GALLERY_PROVIDER, galleryProviderPlugin);
122
122
  },
123
123
  );
124
124
 
125
125
  export const galleryScreensFeature: FeatureDefinition = defineFeature("gallery-screens", (r) => {
126
126
  r.describe("E2E-only nav entry for the billing plans screen plus plan labels.");
127
- r.requires("billing-foundation", "admin-shell");
127
+ r.requires({ features: ["billing-foundation", "admin-shell"] });
128
128
  r.nav({
129
129
  id: "billing",
130
130
  label: "gallery:nav.billing",
@@ -407,7 +407,7 @@
407
407
  },
408
408
  {
409
409
  "name": "channel-email",
410
- "description": "Wires an `EmailTransport` (typically `mail-transport-smtp` in production, `createInMemoryTransport()` in tests) into the delivery system as the `email` channel. Requires `delivery`; pass an `EmailChannelOptions` with a `transport` and a `resolveEmail` function that maps a user ID to their email address. Mails render from stored templates (`renderer-mail-html`): a notification routed to this channel needs `template` + `meta` on `r.notification`, otherwise its delivery fails.",
410
+ "description": "Wires an `EmailTransport` (typically `mail-transport-smtp` in production, `createInMemoryTransport()` in tests) into the delivery system as the `email` channel. Requires `delivery`; pass an `EmailChannelOptions` with a `transport` and a `resolveEmail` function that maps a user ID to their email address. Mails render from stored templates (`renderer-mail-html`): a notification routed to this channel needs `template` + `meta` on `r.notification`, otherwise its delivery fails. Pass `tenantTransport: tenantMailTransport` (from `mail-foundation`) to send each tenant's mail through its own configured provider: a broken provider fails the send, a tenant without one uses the app `transport`. Notifications declared with `unsubscribe: true` get a signed `data.unsubscribeUrl` from delivery, which turns into the `List-Unsubscribe` headers.",
411
411
  "toggleableDefault": null,
412
412
  "requires": [
413
413
  "delivery"
@@ -725,7 +725,7 @@
725
725
  },
726
726
  {
727
727
  "name": "delivery",
728
- "description": "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). Inside a write handler the delivery runs after the write has committed (the call returns `{ kind: \"deferred\" }`, a rolled-back write sends nothing); `immediate: true` delivers synchronously and returns `{ kind: \"dispatched\", deliveries }`. It stores per-user channel preferences in the `notification-preference` entity, opt-outs for no-account recipient addresses in `notification-address-opt-out` (keyed by a blind-index hash, never the plaintext address), logs every attempt to `store_delivery_attempts`, and enforces idempotency and rate-limiting — add `channel-email`, `channel-in-app`, or `channel-push` on top to actually send anything. Unsubscribe/resubscribe links are served by `createUnsubscribeRoutes({ secret })` mounted via the app's `extraRoutes` at `/api/delivery/unsubscribe` and `/api/delivery/resubscribe`: `GET /unsubscribe?token=` renders a confirmation page (no write), `POST /unsubscribe` performs the opt-out (RFC 8058 one-click, token from the form body or query), `POST /resubscribe` undoes it (JSON `{token}` or form body only, never the query — reachable by a mail-client link prefetcher) — sign links with `signUnsubscribeToken` / `signAddressUnsubscribeToken` using the same secret. `channel-email` sets `List-Unsubscribe` / `List-Unsubscribe-Post` automatically when a message's `data.unsubscribeUrl` points at the unsubscribe route.",
728
+ "description": "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). Inside a write handler the delivery runs after the write has committed (the call returns `{ kind: \"deferred\" }`, a rolled-back write sends nothing); `immediate: true` delivers synchronously and returns `{ kind: \"dispatched\", deliveries }`. It stores per-user channel preferences in the `notification-preference` entity, opt-outs for no-account recipient addresses in `notification-address-opt-out` (keyed by a blind-index hash, never the plaintext address), logs every attempt to `store_delivery_attempts`, and enforces idempotency and rate-limiting — add `channel-email`, `channel-in-app`, or `channel-push` on top to actually send anything. Unsubscribe/resubscribe links are served by `createUnsubscribeRoutes({ secret })` mounted via the app's `extraRoutes` at `/api/delivery/unsubscribe` and `/api/delivery/resubscribe`: `GET /unsubscribe?token=` renders a confirmation page (no write), `POST /unsubscribe` performs the opt-out (RFC 8058 one-click, token from the form body or query), `POST /resubscribe` undoes it (JSON `{token}` or form body only, never the query — reachable by a mail-client link prefetcher) — sign links with `signUnsubscribeToken` / `signAddressUnsubscribeToken` using the same secret. `channel-email` sets `List-Unsubscribe` / `List-Unsubscribe-Post` automatically when a message's `data.unsubscribeUrl` points at the unsubscribe route. A notification declared with `unsubscribe: true` gets that link from delivery itself when the service has `unsubscribeLinks` (`runProdApp` option `unsubscribe: { secret }` mounts the routes and wires it; base URL from `auth.publicOrigin`, `auth.mail.baseUrl` or `APP_BASE_URL`): user recipients get a user token, `route` recipients an address token (needs a blind-index key, otherwise no link); a `data.unsubscribeUrl` from the caller wins. `unsubscribe.tenantOrigin: (ctx) => origin | null` points the link at the tenant's own host (subdomain or custom domain, must serve this app's `/api`); null, a throw or a non-https value fall back to the app origin. The token carries the mail's locale, so the confirmation and result pages render in the mail's language. To react to a no-account opt-out (e.g. remove the app's own subscriber row), declare `r.job(name, { trigger: { on: DeliveryHandlers.unsubscribeAddress } }, handler)` on a lane the app consumes: it runs after each committed opt-out, also an idempotent repeat, with `unsubscribeAddressPayloadSchema` (`addressHash`, `notificationType`, `channel`) as payload and the tenant in `ctx.triggeredBy.tenantId`; match the subscriber via `hashUnsubscribeAddress(email)`. Declare `unsubscribeUrl` as an optional template variable if the mail body shows it.",
729
729
  "toggleableDefault": null,
730
730
  "requires": [
731
731
  "tenant"
@@ -1507,7 +1507,7 @@
1507
1507
  },
1508
1508
  {
1509
1509
  "name": "mail-foundation",
1510
- "description": "Defines the `mailTransport` extension point and a per-tenant `provider` config key that selects which registered transport plugin to use at runtime. Call `createTransportForTenant(ctx, tenantId)` to get an `EmailTransport` ready for sending — use this feature together with at least one `mail-transport-*` feature; use `delivery` + `channel-email` instead when you need the full notification pipeline with delivery attempts and user preferences.",
1510
+ "description": "Defines the `mailTransport` extension point and a per-tenant `provider` config key that selects which registered transport plugin to use at runtime. Call `createTransportForTenant(ctx, tenantId)` to get an `EmailTransport` ready for sending — use this feature together with at least one `mail-transport-*` feature; use `delivery` + `channel-email` instead when you need the full notification pipeline with delivery attempts and user preferences. Pass `tenantMailTransport` as `tenantTransport` to `createChannelEmailFeature` so a tenant with a provider configured sends delivery mails through its own transport and other tenants through the app transport.",
1511
1511
  "toggleableDefault": null,
1512
1512
  "requires": [
1513
1513
  "config"
@@ -2122,7 +2122,7 @@
2122
2122
  },
2123
2123
  {
2124
2124
  "name": "renderer-mail-html",
2125
- "description": "A `renderer-foundation` plugin that handles `RenderRequest { kind: \"mail\" }` and returns `{ subject, html, text, previewText? }` suitable for sending via `channel-email`. The mail template (`kind: \"mail\"`, document HTML, subject in `meta`) resolves through `template-resolver`'s DB fallback, its body is compiled to Handlebars by template-resolver and rendered with escaping, then placed into an MJML layout (`kind: \"mail-layout\"`, slug `default` unless `meta.layoutSlug` says otherwise, an in-code structured layout with the tenant branding logo and accent color as last resort) that `mjml()` compiles to email-safe table HTML, with `html-to-text` producing the plain-text alternative. An optional `footer` resolver adds the platform imprint, privacy link and footer text below every structured layout, and an optional `brandSuffix` resolver adds a word after the tenant name in the brand row of the in-code layout. Mail meta `headerChip`, `tone` and `footerNote` add a chip beside the brand, the tone of the top line and chip, and a note above the site and unsubscribe links of the card. Seed an editable system layout with `seedSystemMailLayout(db)`. Requires `renderer-foundation` + `template-resolver`.",
2125
+ "description": "A `renderer-foundation` plugin that handles `RenderRequest { kind: \"mail\" }` and returns `{ subject, html, text, previewText? }` suitable for sending via `channel-email`. The mail template (`kind: \"mail\"`, document HTML, subject in `meta`) resolves through `template-resolver`'s DB fallback, its body is compiled to Handlebars by template-resolver and rendered with escaping, then placed into an MJML layout (`kind: \"mail-layout\"`, slug `default` unless `meta.layoutSlug` says otherwise, an in-code structured layout with the tenant branding logo and accent color as last resort) that `mjml()` compiles to email-safe table HTML, with `html-to-text` producing the plain-text alternative. An optional `footer` resolver adds the platform imprint, privacy link and footer text below every structured layout, an optional `brandSuffix` resolver adds a word after the tenant name in the brand row of the in-code layout, and an optional `siteUrl` resolver supplies the site link of the card when the tenant branding has no site URL (without either, the link is left out). The brand row and `data-resource=\"logo\"` images use the branding logo upload, or the branding logo https address when no file is uploaded. Mail meta `headerChip`, `tone` and `footerNote` add a chip beside the brand, the tone of the top line and chip, and a note above the site and unsubscribe links of the card. Seed an editable system layout with `seedSystemMailLayout(db)`. Requires `renderer-foundation` + `template-resolver`.",
2126
2126
  "toggleableDefault": null,
2127
2127
  "requires": [
2128
2128
  "renderer-foundation",
@@ -21,6 +21,7 @@
21
21
  "@cosmicdrift/kumiko-document-designer": "workspace:*",
22
22
  "@cosmicdrift/kumiko-framework": "workspace:*",
23
23
  "@cosmicdrift/kumiko-headless": "workspace:*",
24
+ "@cosmicdrift/kumiko-renderer-mail-html": "workspace:*",
24
25
  "@cosmicdrift/kumiko-renderer-puppeteer-client": "workspace:*",
25
26
  "@cosmicdrift/kumiko-renderer-web": "workspace:*",
26
27
  "@cosmicdrift/kumiko-server-runtime": "workspace:*",
@@ -89,13 +89,13 @@ export function extractFeaturesFromText(rawText: string): Set<string> {
89
89
  const name = m[1];
90
90
  if (name) feats.add(name);
91
91
  }
92
- for (const m of text.matchAll(/r\.requires\(\s*["']([a-z0-9-]+)["']/g)) {
93
- const name = m[1];
94
- if (name) feats.add(name);
95
- }
96
- for (const m of text.matchAll(/r\.optionalRequires\(\s*["']([a-z0-9-]+)["']/g)) {
97
- const name = m[1];
98
- if (name) feats.add(name);
92
+ for (const m of text.matchAll(
93
+ /r\.(?:requires|optionalRequires)\(\s*\{\s*features:\s*\[([^\]]*)\]/g,
94
+ )) {
95
+ for (const quoted of (m[1] ?? "").matchAll(/["']([a-z0-9-]+)["']/g)) {
96
+ const name = quoted[1];
97
+ if (name) feats.add(name);
98
+ }
99
99
  }
100
100
  return feats;
101
101
  }
@@ -120,9 +120,9 @@ export const notesFeature: FeatureDefinition = defineFeature("notes-demo", (r) =
120
120
  r.describe(
121
121
  "Dev-only host entity demonstrating tags, custom-fields, and folders on a real entityEdit screen.",
122
122
  );
123
- r.requires("tags");
124
- r.requires("custom-fields");
125
- r.requires("folders");
123
+ r.requires({ features: ["tags"] });
124
+ r.requires({ features: ["custom-fields"] });
125
+ r.requires({ features: ["folders"] });
126
126
 
127
127
  wireCustomFieldsFor(r, "note", noteTable);
128
128
 
@@ -9,7 +9,7 @@ import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-frame
9
9
 
10
10
  export const appScreensFeature: FeatureDefinition = defineFeature("app-screens", (r) => {
11
11
  r.describe("App-side nav placement for the user-profile self-service page.");
12
- r.requires("user-profile");
12
+ r.requires({ features: ["user-profile"] });
13
13
  r.nav({
14
14
  id: "profile",
15
15
  label: "screen:profile.title",