@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
@@ -21,8 +21,8 @@ export function createChannelSlackFeature(options: SlackChannelOptions = {}): Fe
21
21
  category: "notifications",
22
22
  recommended: false,
23
23
  });
24
- r.requires("delivery");
25
- r.requires("secrets");
24
+ r.requires({ features: ["delivery"] });
25
+ r.requires({ features: ["secrets"] });
26
26
 
27
27
  const webhookTarget = resolveChatWebhookTarget(
28
28
  { defaultAllowedHosts: SLACK_DEFAULT_ALLOWED_HOSTS },
@@ -21,8 +21,8 @@ export function createChannelTeamsFeature(options: TeamsChannelOptions = {}): Fe
21
21
  category: "notifications",
22
22
  recommended: false,
23
23
  });
24
- r.requires("delivery");
25
- r.requires("secrets");
24
+ r.requires({ features: ["delivery"] });
25
+ r.requires({ features: ["secrets"] });
26
26
 
27
27
  const webhookTarget = resolveChatWebhookTarget(
28
28
  { defaultAllowedHosts: TEAMS_DEFAULT_ALLOWED_HOSTS },
@@ -19,8 +19,8 @@ export function createChannelTelegramFeature(
19
19
  category: "notifications",
20
20
  recommended: false,
21
21
  });
22
- r.requires("delivery");
23
- r.requires("secrets");
22
+ r.requires({ features: ["delivery"] });
23
+ r.requires({ features: ["secrets"] });
24
24
 
25
25
  const botToken = r.secret("botToken", {
26
26
  label: { en: "Telegram bot token" },
@@ -63,7 +63,7 @@ export function createComplianceProfilesFeature(
63
63
  // Context, Profile-Selection ist eigene Entity, sub-processor-Liste
64
64
  // sind Constants. Wenn S1.4+ Cross-Feature-Reads dazukommen, kommt
65
65
  // r.requires hier rein.
66
- r.entity("tenant-compliance-profile", tenantComplianceProfileEntity);
66
+ r.entity({ name: "tenant-compliance-profile", ...tenantComplianceProfileEntity });
67
67
 
68
68
  r.exposesApi("compliance.forTenant");
69
69
 
@@ -7,7 +7,7 @@
7
7
  // a separate feature (mirrors folders-user-data/notes-history-user-data)
8
8
  // rather than folded into compliance-profiles: this is the one genuinely
9
9
  // cross-tenant capability in the picker's orbit, and it alone needs
10
- // r.systemScope() + a hard r.requires("tenant") — apps that only want the
10
+ // r.systemScope() + a hard r.requires({ features: ["tenant"] }) — apps that only want the
11
11
  // per-tenant picker (the vast majority) stay unaffected.
12
12
 
13
13
  import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
@@ -23,8 +23,8 @@ export const complianceProfilesOpsFeature = defineFeature("compliance-profiles-o
23
23
  recommended: false,
24
24
  });
25
25
  r.systemScope();
26
- r.requires("compliance-profiles");
27
- r.requires("tenant");
26
+ r.requires({ features: ["compliance-profiles"] });
27
+ r.requires({ features: ["tenant"] });
28
28
 
29
29
  const queries = {
30
30
  tenantsMissingProfile: r.queryHandler(tenantsMissingProfileQuery),
@@ -45,7 +45,7 @@ export function createConfigFeature(): FeatureDefinition {
45
45
  // projection write in one TX. Subscribers that need config-change
46
46
  // semantics listen to those auto-events via r.multiStreamProjection
47
47
  // (see docs/plans/architecture/event-sourcing-pivot.md §4.7).
48
- r.entity("config-value", configValueEntity);
48
+ r.entity({ name: "config-value", ...configValueEntity });
49
49
 
50
50
  const handlers = {
51
51
  set: r.writeHandler(setWrite),
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.366.0",
4
+ "type": "fix",
5
+ "title": "forget-subject is covered for users stored before the KMS and users without an event stream",
6
+ "detail": "Tests now cover forget-subject for users stored as plaintext before the KMS was configured and for users without an event stream. Both succeed since the KMS tombstone fix, close the login door and write the audit event."
7
+ },
2
8
  {
3
9
  "version": "0.365.1",
4
10
  "type": "fix",
@@ -10,7 +10,7 @@
10
10
  // (crypto-shredding, event ciphertext, forget pipeline).
11
11
  //
12
12
  // **Was diese Feature liefert (B1 + B2, 2026-05-23):**
13
- // 1. r.entity("field-definition") — Definition-Storage (event-sourced).
13
+ // 1. r.entity({ name: "field-definition" }) — Definition-Storage (event-sourced).
14
14
  // 2. define-tenant-field / define-system-field — RBAC write-handlers für
15
15
  // Definition-CRUD.
16
16
  // 3. delete-tenant-field / delete-system-field — RBAC write-handlers.
@@ -123,7 +123,7 @@ function registerCustomFields(
123
123
  category: "data",
124
124
  recommended: false,
125
125
  });
126
- r.entity("field-definition", fieldDefinitionEntity);
126
+ r.entity({ name: "field-definition", ...fieldDefinitionEntity });
127
127
 
128
128
  // Event-types — qualified als "custom-fields:event:<short-name>".
129
129
  // Returned EventDefs liefern .name als compile-time literal-typed string,
@@ -44,8 +44,8 @@ export function customFieldsField(): JsonbFieldDef {
44
44
  // Consumer-side:
45
45
  //
46
46
  // defineFeature("property-mgmt", (r) => {
47
- // r.entity("property", propertyEntity); // muss customFieldsField() haben
48
- // r.requires(CUSTOM_FIELDS_FEATURE_NAME);
47
+ // r.entity({ name: "property", ...propertyEntity }); // muss customFieldsField() haben
48
+ // r.requires({ features: [CUSTOM_FIELDS_FEATURE_NAME] });
49
49
  // wireCustomFieldsFor(r, "property", propertyTable);
50
50
  // });
51
51
  //
@@ -198,18 +198,22 @@ export function wireCustomFieldsFor<TReg extends FeatureRegistrar<string>>(
198
198
 
199
199
  // postQuery-hook: flatten row.customFields jsonb auf root-level der
200
200
  // API-response. Spec-Promise Z.4 "indistinguishable von Stammfeldern".
201
- r.hook("postQuery", { allOf: entityName }, async ({ rows }) => ({
202
- rows: rows.map((row) => {
203
- const customFields = row["customFields"];
204
- if (customFields && typeof customFields === "object" && !Array.isArray(customFields)) {
205
- return {
206
- ...(customFields as Record<string, unknown>), // @cast-boundary db-row jsonb runtime-untyped
207
- ...row, // base fields win: a custom fieldKey named `id`/`name` must not shadow the real column
208
- };
209
- }
210
- return row;
201
+ r.hook({
202
+ type: "postQuery",
203
+ target: { allOf: entityName },
204
+ handler: async ({ rows }) => ({
205
+ rows: rows.map((row) => {
206
+ const customFields = row["customFields"];
207
+ if (customFields && typeof customFields === "object" && !Array.isArray(customFields)) {
208
+ return {
209
+ ...(customFields as Record<string, unknown>), // @cast-boundary db-row jsonb runtime-untyped
210
+ ...row, // base fields win: a custom fieldKey named `id`/`name` must not shadow the real column
211
+ };
212
+ }
213
+ return row;
214
+ }),
211
215
  }),
212
- }));
216
+ });
213
217
 
214
218
  // Search-Payload-Extension: customFields-keys flach ins Index-Doc.
215
219
  // Anders als postQuery-hook (der ALLE keys merged) trägt der Search-
@@ -65,7 +65,7 @@ export function createDataRetentionFeature(): FeatureDefinition {
65
65
  category: "compliance",
66
66
  recommended: false,
67
67
  });
68
- r.entity("tenant-retention-override", tenantRetentionOverrideEntity);
68
+ r.entity({ name: "tenant-retention-override", ...tenantRetentionOverrideEntity });
69
69
 
70
70
  // S2.D3: Cross-Feature-API fuer Forget-Flow + Cleanup-Job
71
71
  r.exposesApi("retention.policyFor");
@@ -13,10 +13,10 @@
13
13
  import type { RetentionDef } from "@cosmicdrift/kumiko-framework/engine";
14
14
 
15
15
  /**
16
- * Pro Entity-Name gemappte Retention-Policy. Entity-Namen sind der
17
- * String aus r.entity("name", ...) — kebab-case oder lowercase.
18
- * Cleanup-Job iteriert alle bekannten Entities; was nicht im Preset
19
- * steht, fällt auf Entity-Default (Layer 1) zurück.
16
+ * Retention policy keyed by entity name, i.e. the `name` from
17
+ * r.entity({ name, ... }) (kebab-case or lowercase). The cleanup job walks
18
+ * all known entities; anything missing from the preset falls back to the
19
+ * entity default (layer 1).
20
20
  */
21
21
  export type RetentionPreset = Readonly<Record<string, RetentionDef>>;
22
22
 
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.366.0",
4
+ "type": "improvement",
5
+ "title": "Renaming a notification type keeps user preferences and address opt-outs",
6
+ "detail": "r.notification accepts renamedFrom, a list of former qualified names. Preferences and address opt-outs stored under a former name keep applying to the renamed notification, and a stored choice under the current name wins over one under a former name. New writes via setPreference and the unsubscribe and resubscribe handlers are stored under the current name, resubscribeAddress also lifts opt-outs under former names. Boot fails if an alias is a current notification name, is the wildcard, or is claimed by two notifications."
7
+ },
2
8
  {
3
9
  "version": "0.365.0",
4
10
  "type": "fix",
@@ -16,14 +16,18 @@ export function addressOptOutKey(addressHash: string, channel: string): AddressO
16
16
  export async function selectOptedOutAddresses(
17
17
  db: DbConnection,
18
18
  tenantId: TenantId,
19
- notificationType: string,
19
+ notificationTypeNames: readonly string[],
20
20
  addressHashes: readonly string[],
21
21
  ): Promise<ReadonlySet<AddressOptOutKey>> {
22
22
  if (addressHashes.length === 0) return new Set();
23
23
  const rows = await selectMany<{ readonly addressHash: string; readonly channel: string }>(
24
24
  db,
25
25
  notificationAddressOptOutsTable,
26
- { tenantId, notificationType, addressHash: { in: addressHashes } },
26
+ {
27
+ tenantId,
28
+ notificationType: { in: notificationTypeNames },
29
+ addressHash: { in: addressHashes },
30
+ },
27
31
  );
28
32
  return new Set(rows.map((row) => addressOptOutKey(row.addressHash, row.channel)));
29
33
  }
@@ -13,7 +13,7 @@ export async function selectNotificationPreferences(
13
13
  db: DbConnection,
14
14
  tenantId: TenantId,
15
15
  userId: string,
16
- notificationType: string,
16
+ notificationTypeNames: readonly string[],
17
17
  channelName: string,
18
18
  ): Promise<readonly NotificationPreferenceRow[]> {
19
19
  return unsafeReadRetrying<NotificationPreferenceRow>(
@@ -23,10 +23,10 @@ export async function selectNotificationPreferences(
23
23
  WHERE tenant_id = $1
24
24
  AND user_id = $2
25
25
  AND (
26
- (notification_type = $3 AND channel = $4)
26
+ (notification_type = ANY($3::text[]) AND channel = $4)
27
27
  OR (notification_type = '*' AND channel = $4)
28
- OR (notification_type = $3 AND channel = '*')
28
+ OR (notification_type = ANY($3::text[]) AND channel = '*')
29
29
  )`,
30
- [tenantId, userId, notificationType, channelName],
30
+ [tenantId, userId, notificationTypeNames, channelName],
31
31
  );
32
32
  }
@@ -44,6 +44,7 @@ import {
44
44
  selectOptedOutAddresses,
45
45
  } from "./db/queries/address-opt-outs.js";
46
46
  import { selectNotificationPreferences } from "./db/queries/preferences.js";
47
+ import { notificationTypeNames } from "./notification-type-aliases.js";
47
48
  import {
48
49
  type ChannelContext,
49
50
  type ChannelMessage,
@@ -503,7 +504,12 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
503
504
  const addressHash = hashUnsubscribeAddress(address);
504
505
  if (addressHash !== undefined) addressHashes.add(addressHash);
505
506
  }
506
- return selectOptedOutAddresses(run.db, tenantId, notificationType, [...addressHashes]);
507
+ return selectOptedOutAddresses(
508
+ run.db,
509
+ tenantId,
510
+ notificationTypeNames(registry, notificationType),
511
+ [...addressHashes],
512
+ );
507
513
  }
508
514
 
509
515
  function isAddressSuppressed(
@@ -617,7 +623,7 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
617
623
  run.db,
618
624
  tenantId,
619
625
  userId,
620
- notificationType,
626
+ notificationTypeNames(registry, notificationType),
621
627
  channelName,
622
628
  );
623
629
 
@@ -629,6 +635,15 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
629
635
  );
630
636
  if (exact) return exact.enabled;
631
637
 
638
+ // Rows stored under a former name: the user's choice from before the rename
639
+ const aliasExact = prefs.filter(
640
+ (p) =>
641
+ p.notificationType !== "*" &&
642
+ p.notificationType !== notificationType &&
643
+ p.channel === channelName,
644
+ );
645
+ if (aliasExact.length > 0) return !aliasExact.some((p) => p.enabled === false);
646
+
632
647
  // Only wildcards matched: any disabled entry disables delivery (deterministic
633
648
  // and conservative — DB ordering no longer decides the outcome).
634
649
  return !prefs.some((p) => p.enabled === false);
@@ -70,18 +70,22 @@ export function createDeliveryFeature(options?: DeliveryFeatureOptions): Feature
70
70
  });
71
71
  r.systemScope();
72
72
  // delivery-log's tenantId column has refEntity: "tenant:tenant" (boot-checked).
73
- r.requires("tenant");
73
+ r.requires({ features: ["tenant"] });
74
74
  // Backing table: the (tenant,user,type,channel) uniqueIndex lives only on
75
75
  // the physical table, not on the entity fields, so the generator would
76
76
  // otherwise omit it → duplicate preference rows on concurrent upserts.
77
- r.entity("notification-preference", notificationPreferenceEntity, {
78
- table: notificationPreferencesTable,
77
+ r.entity({
78
+ name: "notification-preference",
79
+ ...notificationPreferenceEntity,
80
+ backingTable: notificationPreferencesTable,
79
81
  });
80
82
  // Same ride-along reason as notification-preference above: the
81
83
  // (tenant,addressHash,type,channel) uniqueIndex lives only on the
82
84
  // physical table.
83
- r.entity("notification-address-opt-out", notificationAddressOptOutEntity, {
84
- table: notificationAddressOptOutsTable,
85
+ r.entity({
86
+ name: "notification-address-opt-out",
87
+ ...notificationAddressOptOutEntity,
88
+ backingTable: notificationAddressOptOutsTable,
85
89
  });
86
90
  r.storeTable(deliveryAttemptsTableMeta, {
87
91
  reason: "read_side.delivery_attempt_log",
@@ -2,6 +2,7 @@ import { access, defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine
2
2
  import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
3
3
  import * as z from "zod";
4
4
  import { removeAddressOptOut } from "../address-opt-out.js";
5
+ import { currentNotificationType, notificationTypeNames } from "../notification-type-aliases.js";
5
6
 
6
7
  export const resubscribeAddressWrite = defineWriteHandler({
7
8
  name: "resubscribeAddress",
@@ -20,7 +21,8 @@ export const resubscribeAddressWrite = defineWriteHandler({
20
21
  description:
21
22
  "Removes a no-account address opt-out for one notificationType/channel combination; dispatched by the signed resubscribe route, not meant for UI callers.",
22
23
  handler: async (event, ctx) => {
23
- const { addressHash, notificationType, channel } = event.payload;
24
+ const { addressHash, channel } = event.payload;
25
+ const notificationType = currentNotificationType(ctx.registry, event.payload.notificationType);
24
26
  const { tenantId } = event.user;
25
27
 
26
28
  if (!ctx.systemDb) {
@@ -30,13 +32,15 @@ export const resubscribeAddressWrite = defineWriteHandler({
30
32
  }
31
33
  const db = ctx.systemDb.assertTenantMatch(tenantId);
32
34
 
33
- const result = await removeAddressOptOut(db, event.user, {
34
- tenantId,
35
- addressHash,
36
- notificationType,
37
- channel,
38
- });
39
- if (!result.isSuccess) return result;
35
+ for (const name of notificationTypeNames(ctx.registry, notificationType)) {
36
+ const result = await removeAddressOptOut(db, event.user, {
37
+ tenantId,
38
+ addressHash,
39
+ notificationType: name,
40
+ channel,
41
+ });
42
+ if (!result.isSuccess) return result;
43
+ }
40
44
  return { isSuccess: true, data: { notificationType, channel } };
41
45
  },
42
46
  });
@@ -1,6 +1,7 @@
1
1
  import { access, defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
3
3
  import * as z from "zod";
4
+ import { currentNotificationType } from "../notification-type-aliases.js";
4
5
  import { upsertPreference } from "../upsert-preference.js";
5
6
 
6
7
  export const resubscribeUserWrite = defineWriteHandler({
@@ -18,7 +19,8 @@ export const resubscribeUserWrite = defineWriteHandler({
18
19
  description:
19
20
  "Re-enables one notification type and channel combination for the userId carried in a verified unsubscribe token; dispatched by the signed resubscribe route, not meant for UI callers.",
20
21
  handler: async (event, ctx) => {
21
- const { userId, notificationType, channel } = event.payload;
22
+ const { userId, channel } = event.payload;
23
+ const notificationType = currentNotificationType(ctx.registry, event.payload.notificationType);
22
24
  const { tenantId } = event.user;
23
25
 
24
26
  if (!ctx.systemDb) {
@@ -1,6 +1,7 @@
1
1
  import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
3
3
  import * as z from "zod";
4
+ import { currentNotificationType } from "../notification-type-aliases.js";
4
5
  import { upsertPreference } from "../upsert-preference.js";
5
6
 
6
7
  export const setPreferenceWrite = defineWriteHandler({
@@ -21,7 +22,8 @@ export const setPreferenceWrite = defineWriteHandler({
21
22
  description:
22
23
  "Enables or disables one notification type and channel combination for the calling user, where either side may be the wildcard * as a catch-all; use it when a user changes their own notification settings.",
23
24
  handler: async (event, ctx) => {
24
- const { notificationType, channel, enabled } = event.payload;
25
+ const { channel, enabled } = event.payload;
26
+ const notificationType = currentNotificationType(ctx.registry, event.payload.notificationType);
25
27
  const { id: userId, tenantId } = event.user;
26
28
 
27
29
  // delivery runs in system-scope (feature.ts r.systemScope()) — ctx.systemDb
@@ -2,6 +2,7 @@ import { access, defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine
2
2
  import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
3
3
  import * as z from "zod";
4
4
  import { upsertAddressOptOut } from "../address-opt-out.js";
5
+ import { currentNotificationType } from "../notification-type-aliases.js";
5
6
 
6
7
  export const unsubscribeAddressWrite = defineWriteHandler({
7
8
  name: "unsubscribeAddress",
@@ -18,7 +19,8 @@ export const unsubscribeAddressWrite = defineWriteHandler({
18
19
  description:
19
20
  "Records a no-account address opt-out for one notificationType/channel combination; dispatched by the signed unsubscribe route, not meant for UI callers.",
20
21
  handler: async (event, ctx) => {
21
- const { addressHash, notificationType, channel } = event.payload;
22
+ const { addressHash, channel } = event.payload;
23
+ const notificationType = currentNotificationType(ctx.registry, event.payload.notificationType);
22
24
  const { tenantId } = event.user;
23
25
 
24
26
  if (!ctx.systemDb) {
@@ -1,6 +1,7 @@
1
1
  import { access, defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
3
3
  import * as z from "zod";
4
+ import { currentNotificationType } from "../notification-type-aliases.js";
4
5
  import { upsertPreference } from "../upsert-preference.js";
5
6
 
6
7
  export const unsubscribeUserWrite = defineWriteHandler({
@@ -20,7 +21,8 @@ export const unsubscribeUserWrite = defineWriteHandler({
20
21
  handler: async (event, ctx) => {
21
22
  // userId comes from the payload, not event.user.id — the caller here is
22
23
  // the system user dispatchSystemWrite runs as, not the user unsubscribing.
23
- const { userId, notificationType, channel } = event.payload;
24
+ const { userId, channel } = event.payload;
25
+ const notificationType = currentNotificationType(ctx.registry, event.payload.notificationType);
24
26
  const { tenantId } = event.user;
25
27
 
26
28
  if (!ctx.systemDb) {
@@ -0,0 +1,13 @@
1
+ import type { Registry } from "@cosmicdrift/kumiko-framework/engine";
2
+
3
+ export function notificationTypeNames(registry: Registry, type: string): readonly string[] {
4
+ const renamedFrom = registry.getAllNotifications().get(type)?.renamedFrom ?? [];
5
+ return [type, ...renamedFrom];
6
+ }
7
+
8
+ export function currentNotificationType(registry: Registry, type: string): string {
9
+ for (const [name, definition] of registry.getAllNotifications()) {
10
+ if (definition.renamedFrom?.includes(type)) return name;
11
+ }
12
+ return type;
13
+ }
@@ -15,7 +15,7 @@ export const derivativesSharpFeature = defineFeature(FEATURE_NAME, (r) => {
15
15
  category: "storage",
16
16
  recommended: false,
17
17
  });
18
- r.requires("file-derivatives");
18
+ r.requires({ features: ["file-derivatives"] });
19
19
 
20
20
  const plugin: DerivativeRendererPlugin = { render: renderImage };
21
21
  r.useExtension(EXT_DERIVATIVE_RENDERER, "image/*", plugin);
@@ -132,9 +132,9 @@ export const documentIngestFoundationFeature = defineFeature(FEATURE_NAME, (r) =
132
132
  });
133
133
  // tenant-lifecycle hosts EXT_TENANT_DATA — documentExtract.pages is
134
134
  // tenant-subject ciphertext and needs the destroy hook below.
135
- r.requires("config", "tenant-lifecycle");
135
+ r.requires({ features: ["config", "tenant-lifecycle"] });
136
136
 
137
- r.entity("documentExtract", documentExtractEntity);
137
+ r.entity({ name: "documentExtract", ...documentExtractEntity });
138
138
  r.useExtension(EXT_TENANT_DATA, "documentExtract", {
139
139
  destroy: documentExtractTenantDestroyHook,
140
140
  });
@@ -111,12 +111,12 @@ export function createFileDerivativesFeature(opts: FileDerivativesOptions = {}):
111
111
  });
112
112
  // Needs a storage provider to read the original + write the variant —
113
113
  // without file-foundation there's nothing to derive from or write to.
114
- r.requires("file-foundation");
114
+ r.requires({ features: ["file-foundation"] });
115
115
  // variant() reads fileRefsTable, which "files" (not file-foundation)
116
116
  // owns — without this, mounting file-derivatives + file-foundation
117
117
  // alone boots fine but fails at request-time with a SQL error the first
118
118
  // time variant() runs.
119
- r.requires("files");
119
+ r.requires({ features: ["files"] });
120
120
 
121
121
  r.extendsRegistrar(EXT_DERIVATIVE_RENDERER, {
122
122
  onRegister: () => {
@@ -164,7 +164,7 @@ export function createFileDerivativesFeature(opts: FileDerivativesOptions = {}):
164
164
  // tenant's public variants from staying reachable — require the
165
165
  // dependency so a missing mount fails boot, not the tenant check
166
166
  // silently no-op-ing at request time.
167
- r.requires("tenant");
167
+ r.requires({ features: ["tenant"] });
168
168
  r.queryHandler(publicVariantByFileRefQuery);
169
169
  }
170
170
 
@@ -46,7 +46,7 @@ export const fileFoundationFeature = defineFeature(FEATURE_NAME, (r) => {
46
46
  category: "storage",
47
47
  recommended: false,
48
48
  });
49
- r.requires("config");
49
+ r.requires({ features: ["config"] });
50
50
 
51
51
  r.extendsRegistrar(EXT_FILE_PROVIDER, {
52
52
  onRegister: () => {
@@ -68,10 +68,10 @@ export const fileProviderInMemoryFeature = defineFeature(FEATURE_NAME, (r) => {
68
68
  category: "storage",
69
69
  recommended: false,
70
70
  });
71
- // Kein r.requires("config") + kein r.requires("secrets") — der
71
+ // Kein requires auf "config" oder "secrets" — der
72
72
  // In-Memory-Provider hat keine Config + kein Secret. Nur die
73
73
  // file-foundation muss da sein (Plugin-extension-point).
74
- r.requires("file-foundation");
74
+ r.requires({ features: ["file-foundation"] });
75
75
 
76
76
  const plugin: FileProviderPlugin = {
77
77
  build: async (_ctx: FileProviderContext, tenantId: string): Promise<FileStorageProvider> => {
@@ -47,9 +47,9 @@ export const fileProviderS3Feature = defineFeature(FEATURE_NAME, (r) => {
47
47
  category: "storage",
48
48
  recommended: false,
49
49
  });
50
- r.requires("config");
51
- r.requires("secrets");
52
- r.requires("file-foundation");
50
+ r.requires({ features: ["config"] });
51
+ r.requires({ features: ["secrets"] });
52
+ r.requires({ features: ["file-foundation"] });
53
53
 
54
54
  const secretAccessKey = r.secret("s3.secretAccessKey", {
55
55
  label: { en: "S3 secret access key" },
@@ -42,10 +42,10 @@ export const fileProviderS3EnvFeature = defineFeature(FEATURE_NAME, (r) => {
42
42
  category: "storage",
43
43
  recommended: false,
44
44
  });
45
- // No r.requires("config") / r.requires("secrets") — credentials come from
45
+ // No requires on "config" or "secrets" — credentials come from
46
46
  // env, not the per-tenant config + secrets store. Only the file-foundation
47
47
  // plugin-extension-point must be mounted.
48
- r.requires("file-foundation");
48
+ r.requires({ features: ["file-foundation"] });
49
49
 
50
50
  const plugin: FileProviderPlugin = {
51
51
  // tenantId ignored: one shared bucket serves all tenants (isolation via
@@ -5,7 +5,7 @@ Sprint-1.5 Refactor (Pre-Sprint-2).
5
5
 
6
6
  ## Was es macht
7
7
 
8
- Deklariert `r.entity("fileRef", ...)` für die DB-Tabelle die das
8
+ Deklariert `r.entity({ name: "fileRef", ... })` für die DB-Tabelle die das
9
9
  Framework via `createFileRoutes` (multipart-Upload + binary-Download)
10
10
  bewirtschaftet. Das öffnet die Tür für Cross-Feature-Hooks:
11
11
 
@@ -39,7 +39,7 @@ export function createFilesTenantDataFeature(): FeatureDefinition {
39
39
  category: "compliance",
40
40
  recommended: false,
41
41
  });
42
- r.requires("tenant-lifecycle");
42
+ r.requires({ features: ["tenant-lifecycle"] });
43
43
  r.optionalRequires("files");
44
44
 
45
45
  r.useExtension(EXT_TENANT_DATA, "fileRef", { destroy: fileRefTenantDestroyHook });
@@ -58,8 +58,8 @@ function registerFolders(
58
58
  if (toggleable !== undefined) r.toggleable(toggleable);
59
59
 
60
60
  const folderAssignmentEntity = createFolderAssignmentEntity(ownership, parents);
61
- r.entity("folder", folderEntity);
62
- r.entity("folder-assignment", folderAssignmentEntity);
61
+ r.entity({ name: "folder", ...folderEntity });
62
+ r.entity({ name: "folder-assignment", ...folderAssignmentEntity });
63
63
 
64
64
  // Folder catalog — plain CRUD, no custom logic. update is rename (and, in a
65
65
  // later stage, reparent: it accepts changes.parentId, optimistic-locked).
@@ -20,7 +20,7 @@ export const foldersUserDataFeature = defineFeature("folders-user-data", (r) =>
20
20
  // OPTIONAL: if it's mounted toggleable(default=false) (e.g. per-tenant via
21
21
  // tier), a hard r.requires would throw an "effectively disabled" boot
22
22
  // warning even though the folder entities exist and the hooks work fine.
23
- r.requires("user-data-rights");
23
+ r.requires({ features: ["user-data-rights"] });
24
24
  r.optionalRequires("folders");
25
25
  r.useExtension(EXT_USER_DATA, "folder", {
26
26
  export: folderExportHook,
@@ -30,11 +30,11 @@ function registerFormDraft(r: FeatureRegistrar<typeof FORM_DRAFT_FEATURE_NAME>):
30
30
  // config is optional: the retention-days key only resolves when the
31
31
  // config feature is mounted (ctx.configResolver present) — the cleanup
32
32
  // job already falls back to FORM_DRAFT_DEFAULT_RETENTION_DAYS otherwise
33
- // (see handlers/cleanup.job.ts). A hard r.requires("config") would break
33
+ // (see handlers/cleanup.job.ts). A hard r.requires({ features: ["config"] }) would break
34
34
  // every app that mounts form-draft without config, which is a breaking
35
35
  // change no minor/patch release should introduce silently.
36
36
  r.optionalRequires("config");
37
- r.entity("form-draft", formDraftEntity);
37
+ r.entity({ name: "form-draft", ...formDraftEntity });
38
38
 
39
39
  r.writeHandler(saveDraftWrite);
40
40
  r.writeHandler(discardDraftWrite);
@@ -12,7 +12,7 @@ export const formDraftUserDataFeature = defineFeature("form-draft-user-data", (r
12
12
  r.describe(
13
13
  "GDPR (Art. 20 export / Art. 17 erasure) coverage for the `form-draft` feature's `form-draft` entity. Mounts the EXT_USER_DATA export hook so a user's saved drafts are included in the user-data export bundle, and a delete hook that physically removes the user's own draft rows on forget. Kept separate from `form-draft` so form-draft consumers without the user-data-rights pipeline don't pull a hard dependency — requires `user-data-rights`, optionalRequires `form-draft`.",
14
14
  );
15
- r.requires("user-data-rights");
15
+ r.requires({ features: ["user-data-rights"] });
16
16
  r.optionalRequires("form-draft");
17
17
  r.useExtension(EXT_USER_DATA, "form-draft", {
18
18
  export: formDraftExportHook,