@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
@@ -1,4 +1,17 @@
1
1
  [
2
+ {
3
+ "version": "0.366.0",
4
+ "type": "improvement",
5
+ "title": "Inbound mail fetch can skip already-seen message ids",
6
+ "detail": "InboundMailProviderPlugin.fetch receives an optional filterUnknownMessageIds(providerMessageIds) hook. The watch supervisor implements it with a tenant- and account-scoped lookup of already-seen provider message ids and returns the ids not seen yet, so a plugin can avoid downloading messages it has already processed."
7
+ },
8
+ {
9
+ "version": "0.366.0",
10
+ "type": "breaking",
11
+ "title": "InboundMailContext exposes metricsFor to provider plugins",
12
+ "detail": "InboundMailContext gains an optional metricsFor(featureName) returning a MetricsHandle. runProdApp, the worker app and the dev server wire it from the observability meter, so provider plugins can emit their own metrics through the same pipeline.",
13
+ "migration": "Code that builds SystemWireDeps itself adds metricsFor (createMetricsForFactory(meter))."
14
+ },
2
15
  {
3
16
  "version": "0.364.0",
4
17
  "type": "fix",
@@ -30,6 +30,7 @@ import {
30
30
  } from "./oauth-access-token.js";
31
31
  import { type OAuthStatePayload, signOAuthState, verifyOAuthState } from "./oauth-state.js";
32
32
  import { resolveInboundProviderForKey } from "./provider-factory.js";
33
+ import type { InboundMailContext } from "./types.js";
33
34
 
34
35
  const DEFAULT_STATE_TTL_MINUTES = 15;
35
36
  const DEFAULT_CONNECT_PATH = "/api/inbound-mail/connect";
@@ -56,6 +57,8 @@ export type InboundMailConnectRoutesOptions = {
56
57
  * lock and primes the access-token cache from the code exchange. Omitted
57
58
  * → plain secret write, no cache priming. */
58
59
  readonly oauthTokens?: OAuthAccessTokenManager;
60
+ /** Handed to the provider plugin's context (`InboundMailContext.metricsFor`). */
61
+ readonly metricsFor?: InboundMailContext["metricsFor"];
59
62
  };
60
63
 
61
64
  function errorJson(c: Context, status: 400 | 401 | 404 | 500 | 502, code: string, message: string) {
@@ -172,7 +175,12 @@ export function createInboundMailConnectRoutes(
172
175
  `${INBOUND_MAIL_FOUNDATION_FEATURE}: no secrets context wired for the oauth callback route — the refresh token cannot be persisted`,
173
176
  );
174
177
  }
175
- const providerCtx = { registry: deps.registry, secrets: deps.secrets, config: undefined };
178
+ const providerCtx: InboundMailContext = {
179
+ registry: deps.registry,
180
+ secrets: deps.secrets,
181
+ config: undefined,
182
+ ...(options.metricsFor && { metricsFor: options.metricsFor }),
183
+ };
176
184
 
177
185
  let plugin: ReturnType<typeof resolveInboundProviderForKey>;
178
186
  try {
@@ -107,7 +107,7 @@ export const inboundMailFoundationFeature = defineFeature(INBOUND_MAIL_FOUNDATIO
107
107
  recommended: false,
108
108
  });
109
109
  // EXT_TENANT_DATA-Hook braucht den tenant-lifecycle-Host.
110
- r.requires("tenant-lifecycle");
110
+ r.requires({ features: ["tenant-lifecycle"] });
111
111
 
112
112
  // 5 fine-grained domain-events (Payload-Schemas: events.ts).
113
113
  // piiFields: "none" — address/subject/snippet are already ciphertext under the tenant subject key before append (see events.ts).
@@ -11,23 +11,28 @@
11
11
  // (sichtbarer breaking change) statt ctx-cast.
12
12
 
13
13
  import type { ConfigAccessor, Registry } from "@cosmicdrift/kumiko-framework/engine";
14
+ import type { MetricsHandle } from "@cosmicdrift/kumiko-framework/observability";
14
15
  import { INBOUND_MAIL_PROVIDER_EXTENSION, type InboundMailAccountStatus } from "./constants.js";
15
16
 
16
17
  /**
17
- * Slim-Context für Provider-Plugins.
18
+ * Slim context for provider plugins.
18
19
  *
19
- * **Felder:**
20
- * config — tenant-config-reads (host/port/... der Plugins)
21
- * registry — extension-Lookup in der Factory (nicht plugin-intern)
22
- * secrets — tenant-secret-reads (IMAP-Passwort, OAuth-Refresh-Token)
23
- * _userId — Audit-Identity für secret-reads. Handler-Pfad: dispatcher
24
- * setzt Caller-User-ID; Worker-Pfad: System-Identity.
20
+ * **Fields:**
21
+ * config — tenant config reads (host/port/... of the plugins)
22
+ * registry — extension lookup in the factory (not plugin-internal)
23
+ * secrets — tenant secret reads (IMAP password, OAuth refresh token)
24
+ * _userId — audit identity for secret reads. Handler path: the dispatcher
25
+ * sets the caller user ID; worker path: system identity.
26
+ * metricsFor — metrics handle of a feature by name, same behavior as
27
+ * HandlerContext.metricsFor: unregistered metrics are a silent
28
+ * no-op instead of a throw.
25
29
  */
26
30
  export type InboundMailContext = {
27
31
  readonly config?: ConfigAccessor;
28
32
  readonly registry?: Registry;
29
33
  readonly secrets?: import("@cosmicdrift/kumiko-framework/secrets").SecretsContext;
30
34
  readonly _userId?: string | undefined;
35
+ readonly metricsFor?: (featureName: string) => MetricsHandle;
31
36
  };
32
37
 
33
38
  /**
@@ -167,6 +172,13 @@ export type InboundMailProviderPlugin = {
167
172
  * whose provider declares `oauth` (authMethod oauth/xoauth2). Absent
168
173
  * for password accounts and providers without an oauth flow. */
169
174
  readonly accessToken?: string;
175
+ /** Batch lookup of the provider message ids not yet ingested for this
176
+ * account (committed seen anchors, one query per call). Call it after
177
+ * the listing/delta page and download MIME only for the returned ids.
178
+ * Ingest dedup stays the correctness anchor; this only saves downloads. */
179
+ readonly filterUnknownMessageIds?: (
180
+ providerMessageIds: readonly string[],
181
+ ) => Promise<ReadonlySet<string>>;
170
182
  },
171
183
  ) => Promise<InboundFetchResult>;
172
184
  /** Nur OAuth-Provider. */
@@ -14,7 +14,7 @@
14
14
  // app owner still wires the supervisor in bin/server.ts:
15
15
  //
16
16
  // const supervisor = createInboundMailSupervisor({
17
- // providerCtx: { registry: deps.registry, secrets },
17
+ // providerCtx: { registry: deps.registry, secrets, metricsFor: deps.metricsFor },
18
18
  // db,
19
19
  // dispatchWrite: ({ handlerQn, payload, tenantId }) =>
20
20
  // deps.dispatchSystemWrite({ handlerQn, payload, tenantId: tenantId as TenantId }),
@@ -54,7 +54,7 @@ import type { DistributedLock } from "@cosmicdrift/kumiko-framework/pipeline";
54
54
  import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
55
55
  import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
56
56
  import { InboundMailAccountStatuses, InboundMailFoundationHandlers } from "./constants.js";
57
- import { MAIL_ACCOUNT_PII_FIELDS, syncCursorTable } from "./entities.js";
57
+ import { MAIL_ACCOUNT_PII_FIELDS, seenMessageTable, syncCursorTable } from "./entities.js";
58
58
  import {
59
59
  createOAuthAccessTokenManager,
60
60
  type OAuthAccessTokenManager,
@@ -398,6 +398,24 @@ export function createInboundMailSupervisor(
398
398
  // ---------------------------------------------------------------
399
399
  // Poll (Reconciliation).
400
400
  // ---------------------------------------------------------------
401
+ function unknownMessageIdsFilterFor(account: MailAccountRecord) {
402
+ return async (providerMessageIds: readonly string[]): Promise<ReadonlySet<string>> => {
403
+ if (providerMessageIds.length === 0) return new Set();
404
+ const seen = await selectMany<{ providerMessageId: string }>(
405
+ deps.db,
406
+ seenMessageTable,
407
+ {
408
+ tenantId: account.tenantId,
409
+ accountId: account.id,
410
+ providerMessageId: { in: providerMessageIds },
411
+ },
412
+ { fields: ["providerMessageId"] },
413
+ );
414
+ const seenIds = new Set(seen.map((row) => row.providerMessageId));
415
+ return new Set(providerMessageIds.filter((id) => !seenIds.has(id)));
416
+ };
417
+ }
418
+
401
419
  async function pollAccount(
402
420
  account: MailAccountRecord,
403
421
  plugin: InboundMailProviderPlugin,
@@ -411,6 +429,7 @@ export function createInboundMailSupervisor(
411
429
  const result = await plugin.fetch(deps.providerCtx, account, cursor, {
412
430
  backfillWindowDays,
413
431
  maxMessages: budget,
432
+ filterUnknownMessageIds: unknownMessageIdsFilterFor(account),
414
433
  ...(accessToken !== undefined && { accessToken }),
415
434
  });
416
435
  await ingestBatch(account, result.messages, JSON.stringify(result.nextCursor));
@@ -281,8 +281,8 @@ export const inboundProviderImapFeature = defineFeature(FEATURE_NAME, (r) => {
281
281
  category: "notifications",
282
282
  recommended: false,
283
283
  });
284
- r.requires("inbound-mail-foundation");
285
- r.requires("secrets");
284
+ r.requires({ features: ["inbound-mail-foundation"] });
285
+ r.requires({ features: ["secrets"] });
286
286
  r.envSchema(mailAllowedPrivateHostsEnvSchema);
287
287
  r.useExtension(INBOUND_MAIL_PROVIDER_EXTENSION, IMAP_PROVIDER_KEY, imapInboundMailPlugin);
288
288
  });
@@ -149,6 +149,6 @@ export const inboundProviderInMemoryFeature = defineFeature(FEATURE_NAME, (r) =>
149
149
  category: "notifications",
150
150
  recommended: false,
151
151
  });
152
- r.requires("inbound-mail-foundation");
152
+ r.requires({ features: ["inbound-mail-foundation"] });
153
153
  r.useExtension("inboundMailProvider", INMEMORY_PROVIDER_KEY, inMemoryInboundMailPlugin);
154
154
  });
@@ -54,9 +54,9 @@ function registerLedger(
54
54
 
55
55
  if (toggleable !== undefined) r.toggleable(toggleable);
56
56
 
57
- r.entity("account", accountEntity);
58
- r.entity("transaction", transactionEntity);
59
- r.entity("schedule", scheduleEntity);
57
+ r.entity({ name: "account", ...accountEntity });
58
+ r.entity({ name: "transaction", ...transactionEntity });
59
+ r.entity({ name: "schedule", ...scheduleEntity });
60
60
 
61
61
  // Chart of accounts — plain CRUD, no custom logic. No delete in v1: removing an
62
62
  // account that has postings would orphan them; a posting-aware guard lands with
@@ -126,8 +126,8 @@ function registerLedger(
126
126
  "Changes a recurring booking template's description, accounts, amount, interval or date window; use it when a standing order's terms change, already confirmed periods stay booked as they were.",
127
127
  }),
128
128
  );
129
- r.hook("preSave", "schedule:create", requireScheduleSubjectPair);
130
- r.hook("preSave", "schedule:update", requireScheduleSubjectPair);
129
+ r.hook({ type: "preSave", target: "schedule:create", handler: requireScheduleSubjectPair });
130
+ r.hook({ type: "preSave", target: "schedule:update", handler: requireScheduleSubjectPair });
131
131
  r.writeHandler(createConfirmSchedulePeriodHandler(access));
132
132
  r.queryHandler(
133
133
  defineEntityListHandler("schedule", scheduleEntity, {
@@ -110,7 +110,7 @@ export function createLegalPagesFeature(opts: LegalPagesOptions = {}): FeatureDe
110
110
  category: "content",
111
111
  recommended: false,
112
112
  });
113
- r.requires("template-resolver");
113
+ r.requires({ features: ["template-resolver"] });
114
114
 
115
115
  // Public HTML routes (default: 4 DACH routes, configurable via opts.routes)
116
116
  for (const route of routes) {
@@ -128,7 +128,7 @@ export const mailFoundationFeature = defineFeature(FEATURE_NAME, (r) => {
128
128
  category: "notifications",
129
129
  recommended: false,
130
130
  });
131
- r.requires("config");
131
+ r.requires({ features: ["config"] });
132
132
 
133
133
  // Plugin extension-point. Provider-features register here. The
134
134
  // entityName at registration time becomes the value tenants pick in
@@ -80,11 +80,11 @@ export const mailTransportInMemoryFeature = defineFeature(FEATURE_NAME, (r) => {
80
80
  r.describe(
81
81
  'Registers an in-process `"inmemory"` provider for `mail-foundation` that buffers sent mails per tenant instead of contacting an SMTP server. Use `getInbox(tenantId)` and `clearInbox(tenantId)` in demo apps and tests; not for production (buffer is process-memory, lost on restart).',
82
82
  );
83
- // Kein r.requires("config") + kein r.requires("secrets") — der
83
+ // Kein requires auf "config" oder "secrets" — der
84
84
  // In-Memory-Transport hat keine Config (nichts zu konfigurieren) und
85
85
  // kein Secret. Der einzige hard-require ist mail-foundation, das den
86
86
  // extension-point "mailTransport" definiert.
87
- r.requires("mail-foundation");
87
+ r.requires({ features: ["mail-foundation"] });
88
88
 
89
89
  const plugin: MailTransportPlugin = {
90
90
  build: async (_ctx: MailTransportContext, tenantId: string): Promise<EmailTransport> => {
@@ -85,9 +85,9 @@ export const mailTransportSmtpFeature = defineFeature(FEATURE_NAME, (r) => {
85
85
  category: "notifications",
86
86
  recommended: false,
87
87
  });
88
- r.requires("config");
89
- r.requires("secrets");
90
- r.requires("mail-foundation");
88
+ r.requires({ features: ["config"] });
89
+ r.requires({ features: ["secrets"] });
90
+ r.requires({ features: ["mail-foundation"] });
91
91
  r.envSchema(mailTransportSmtpEnvSchema);
92
92
 
93
93
  // Provider-secret. Sensitive: redact-helper for admin-UI display.
@@ -18,7 +18,7 @@ export function createManagedPagesCssFeature(): FeatureDefinition {
18
18
  r.describe(
19
19
  'Per-tenant toggle gate for the managed-pages custom-CSS (raw tenant CSS-inject) capability. Handler-less: composing it makes `managed-pages-css` a toggleable feature defaulting OFF, so an operator/tier can grant raw CSS-inject per tenant. managed-pages reads `ctx.hasFeature("managed-pages-css")` in its branding query and emits tenant CSS only when this toggle is on AND the app passed `allowCustomCss: true`. The tenant CSS is allowlist-sanitized + scoped at render regardless; this gate is the commercial/operator control, not the safety boundary.',
20
20
  );
21
- r.requires("managed-pages");
21
+ r.requires({ features: ["managed-pages"] });
22
22
  r.toggleable({ default: false });
23
23
  });
24
24
  }
@@ -162,8 +162,8 @@ export function createManagedPagesFeature(opts: ManagedPagesOptions): FeatureDef
162
162
  category: "content",
163
163
  recommended: false,
164
164
  });
165
- r.requires("config");
166
- r.entity("page", pageEntity);
165
+ r.requires({ features: ["config"] });
166
+ r.entity({ name: "page", ...pageEntity });
167
167
 
168
168
  // Per-tenant branding config keys (scope: tenant). Write-validated via
169
169
  // keyDef.pattern (hex / https) — see branding.ts. read:all so the
@@ -146,7 +146,7 @@ export function createMetricsFeature(options: MetricsFeatureOptions): FeatureDef
146
146
  category: "operations",
147
147
  recommended: false,
148
148
  });
149
- for (const feature of requiredFeatures(metrics)) r.requires(feature);
149
+ for (const feature of requiredFeatures(metrics)) r.requires({ features: [feature] });
150
150
  for (const metric of metrics) r.queryHandler(createTenantMetricQuery(metric, now));
151
151
  r.translations({ keys: METRICS_I18N });
152
152
  });
@@ -167,7 +167,7 @@ export function createSystemMetricsFeature(options: MetricsFeatureOptions): Feat
167
167
  recommended: false,
168
168
  });
169
169
  r.systemScope();
170
- for (const feature of requiredFeatures(metrics)) r.requires(feature);
170
+ for (const feature of requiredFeatures(metrics)) r.requires({ features: [feature] });
171
171
  for (const metric of metrics) r.queryHandler(createSystemMetricQuery(metric, now));
172
172
  r.translations({ keys: METRICS_I18N });
173
173
  });
@@ -43,13 +43,13 @@ function registerNotesHistory(
43
43
  });
44
44
 
45
45
  const entity = createNoteEntryEntity(ownership, parents);
46
- r.entity("note-entry", entity);
46
+ r.entity({ name: "note-entry", ...entity });
47
47
  // No write/query handler of its own — populated only as a side effect of
48
48
  // add-note (see handlers/add-note.write.ts), looked up by
49
49
  // notes-history-user-data's forget cascade. Registering the entity (not
50
50
  // just building it inline in the handler) is what makes it visible to the
51
51
  // registry-wide GDPR boot guards and to executor.ts's table/projection setup.
52
- r.entity("note-mention", noteMentionEntity);
52
+ r.entity({ name: "note-mention", ...noteMentionEntity });
53
53
 
54
54
  r.writeHandler(createAddNoteHandler(access, createNoteEntryExecutor(ownership)));
55
55
  r.queryHandler(
@@ -23,7 +23,7 @@ export const notesHistoryUserDataFeature = defineFeature("notes-history-user-dat
23
23
  // is OPTIONAL: if it's mounted toggleable(default=false), a hard r.requires
24
24
  // would throw an "effectively disabled" boot warning even though the
25
25
  // note-entry entity exists and the hooks work fine.
26
- r.requires("user-data-rights");
26
+ r.requires({ features: ["user-data-rights"] });
27
27
  r.optionalRequires("notes-history");
28
28
  r.useExtension(EXT_USER_DATA, "note-entry", {
29
29
  export: noteEntryExportHook,
@@ -106,7 +106,7 @@ export function createPersonalAccessTokensFeature(
106
106
  // roles — make both boot-time deps so a mis-wiring fails validateBoot.
107
107
  // auth-foundation owns EXT_TOKEN_VERIFIER, which the useExtension below
108
108
  // registers against.
109
- r.requires("user", "tenant", "auth-foundation");
109
+ r.requires({ features: ["user", "tenant", "auth-foundation"] });
110
110
  // Self-registers as a bearer-auth provider instead of the app wiring a
111
111
  // dedicated patResolver callback — the middleware finds it generically
112
112
  // via resolveTokenVerifier(), shape-matched by the PAT token prefix.
@@ -142,16 +142,20 @@ export function createPersonalAccessTokensFeature(
142
142
  // #497), so a postSave hook on "user" gets a poisoned ctx.db here. The
143
143
  // concrete revoker is bound once run{Prod,Dev}App has a real db handle.
144
144
  let autoRevokeOnPasswordChange = options.autoRevokeOnPasswordChange;
145
- r.hook("postSave", { allOf: "user" }, async (result) => {
146
- // skip: nothing bound — same late-bind pattern as sessions/feature.ts
147
- if (!autoRevokeOnPasswordChange) return;
148
- // skip: brand-new user, no PATs can exist yet
149
- if (result.isNew) return;
150
- // skip: handler didn't touch any revoke-triggering field
151
- if (!PAT_REVOKE_TRIGGERING_FIELDS.some((field) => result.changes[field] !== undefined)) {
152
- return;
153
- }
154
- await autoRevokeOnPasswordChange(String(result.id));
145
+ r.hook({
146
+ type: "postSave",
147
+ target: { allOf: "user" },
148
+ handler: async (result) => {
149
+ // skip: nothing bound — same late-bind pattern as sessions/feature.ts
150
+ if (!autoRevokeOnPasswordChange) return;
151
+ // skip: brand-new user, no PATs can exist yet
152
+ if (result.isNew) return;
153
+ // skip: handler didn't touch any revoke-triggering field
154
+ if (!PAT_REVOKE_TRIGGERING_FIELDS.some((field) => result.changes[field] !== undefined)) {
155
+ return;
156
+ }
157
+ await autoRevokeOnPasswordChange(String(result.id));
158
+ },
155
159
  });
156
160
  const bindAutoRevokeOnPasswordChange: BindPatAutoRevokeOnPasswordChange = (revoker) => {
157
161
  // explicit constructor option wins over the runtime binding
@@ -20,8 +20,8 @@ export const readinessFeature = defineFeature("readiness", (r) => {
20
20
  category: "operations",
21
21
  recommended: false,
22
22
  });
23
- r.requires("config");
24
- r.requires("secrets");
23
+ r.requires({ features: ["config"] });
24
+ r.requires({ features: ["secrets"] });
25
25
 
26
26
  const queries = {
27
27
  status: r.queryHandler(statusQuery),
@@ -66,7 +66,7 @@ Mails einer `r.notification` rendert die Delivery selbst: `template` + `meta` an
66
66
  import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
67
67
 
68
68
  export const myRendererFeature = defineFeature("renderer-myown", (r) => {
69
- r.requires("renderer-foundation");
69
+ r.requires({ features: ["renderer-foundation"] });
70
70
  r.useExtension("renderer", "myown", {
71
71
  kinds: ["document-pdf"],
72
72
  // ctx ist optional — nur nehmen wenn Service-Access nötig
@@ -20,7 +20,7 @@ export function createRendererFoundationFeature() {
20
20
  category: "notifications",
21
21
  recommended: false,
22
22
  });
23
- r.requires("template-resolver");
23
+ r.requires({ features: ["template-resolver"] });
24
24
 
25
25
  r.extendsRegistrar(RENDERER_EXTENSION, {
26
26
  onRegister: () => {
@@ -44,7 +44,7 @@ export function createRendererPdfFeature(): FeatureDefinition {
44
44
  category: "notifications",
45
45
  recommended: false,
46
46
  });
47
- r.requires("renderer-foundation");
47
+ r.requires({ features: ["renderer-foundation"] });
48
48
 
49
49
  r.useExtension(RENDERER_EXTENSION, "pdf", {
50
50
  kinds: ["document-pdf"] as const,
@@ -159,7 +159,7 @@ export function createSecretsFeature(opts: SecretsFeatureOptions = {}): FeatureD
159
159
  // uniqueIndex are not expressible via the field-DSL (jsonb-without-default,
160
160
  // now()-default), so the physical table is the DDL truth. Without this the
161
161
  // generated migration omitted those columns → prod-500 (publicstatus#116).
162
- r.entity("tenant-secret", tenantSecretEntity, { table: tenantSecretsTable });
162
+ r.entity({ name: "tenant-secret", ...tenantSecretEntity, backingTable: tenantSecretsTable });
163
163
 
164
164
  // Read-audit domain-event. Registered here so ops tools + MSPs can
165
165
  // discover the type; secrets-context.get parses payloads against
@@ -179,7 +179,7 @@ export function createSeoFeature(opts: SeoOptions): FeatureDefinition {
179
179
  // config keys need the config feature's write/read machinery to actually
180
180
  // function (same hard dependency managed-pages has) — only legal-pages/
181
181
  // managed-pages are genuinely optional (merged only when opted in).
182
- r.requires("config");
182
+ r.requires({ features: ["config"] });
183
183
  r.optionalRequires("legal-pages", "managed-pages");
184
184
 
185
185
  r.config({ keys: SEO_CONFIG_KEYS });
@@ -113,7 +113,7 @@ export function createSessionsFeature(options?: SessionsFeatureOptions): Feature
113
113
  // sessions-without-user wiring fails validateBoot instead of 500ing live.
114
114
  // auth-foundation owns EXT_SESSION_STORE, which the useExtension below
115
115
  // registers against.
116
- r.requires("user", "auth-foundation");
116
+ r.requires({ features: ["user", "auth-foundation"] });
117
117
  // store_user_sessions is a hot-path direct-write store: sessionCreator
118
118
  // inserts and the revoke handlers update rows WITHOUT emitting lifecycle
119
119
  // events (the row columns ARE the audit trail). Registering it as
@@ -212,18 +212,22 @@ export function createSessionsFeature(options?: SessionsFeatureOptions): Feature
212
212
  // regardless.
213
213
  const REVOKE_TRIGGERING_FIELDS = ["passwordHash"] as const;
214
214
  let autoRevoke = options?.autoRevokeOnPasswordChange;
215
- r.hook("postSave", { allOf: "user" }, async (ctx) => {
216
- // skip: nothing bound — stateless-JWT deployments without a runtime
217
- // that calls bindAutoRevokeOnPasswordChange keep the old behavior.
218
- if (!autoRevoke) return;
219
- // skip: brand-new user, no sessions can possibly exist yet. The
220
- // initial passwordHash on a user:create would trip the second guard
221
- // otherwise — every registration would do a mass-revoke roundtrip
222
- // for a user who literally has no rows in user_sessions.
223
- if (ctx.isNew) return;
224
- // skip: handler didn't touch any revoke-triggering field
225
- if (!REVOKE_TRIGGERING_FIELDS.some((field) => ctx.changes[field] !== undefined)) return;
226
- await autoRevoke(String(ctx.id));
215
+ r.hook({
216
+ type: "postSave",
217
+ target: { allOf: "user" },
218
+ handler: async (ctx) => {
219
+ // skip: nothing bound — stateless-JWT deployments without a runtime
220
+ // that calls bindAutoRevokeOnPasswordChange keep the old behavior.
221
+ if (!autoRevoke) return;
222
+ // skip: brand-new user, no sessions can possibly exist yet. The
223
+ // initial passwordHash on a user:create would trip the second guard
224
+ // otherwise — every registration would do a mass-revoke roundtrip
225
+ // for a user who literally has no rows in user_sessions.
226
+ if (ctx.isNew) return;
227
+ // skip: handler didn't touch any revoke-triggering field
228
+ if (!REVOKE_TRIGGERING_FIELDS.some((field) => ctx.changes[field] !== undefined)) return;
229
+ await autoRevoke(String(ctx.id));
230
+ },
227
231
  });
228
232
 
229
233
  const bindAutoRevokeOnPasswordChange: BindAutoRevokeOnPasswordChange = (revoker) => {
@@ -64,7 +64,7 @@ export function createStepDispatcherFeature(): FeatureDefinition {
64
64
  recommended: false,
65
65
  });
66
66
  r.envSchema(stepDispatcherEnvSchema);
67
- r.requires("secrets");
67
+ r.requires({ features: ["secrets"] });
68
68
 
69
69
  // Literal name: the few-shot corpus parser only reads string-literal job names.
70
70
  // StepDispatcherJobNames.deliver mirrors it; feature.boot.test.ts pins the two together.
@@ -154,7 +154,7 @@ export function createSubscriptionMollieFeature(
154
154
  category: "billing",
155
155
  recommended: false,
156
156
  });
157
- r.requires("billing-foundation");
157
+ r.requires({ features: ["billing-foundation"] });
158
158
  r.envSchema(subscriptionMollieEnvSchema);
159
159
 
160
160
  const plugin: SubscriptionProviderPlugin = {
@@ -155,9 +155,9 @@ export function createSubscriptionStripeFeature(
155
155
  // Hard-deps: billing-foundation (plugin-host) + config (billing-live +
156
156
  // backing:"secrets" credentials) + secrets (the store the backing:secrets
157
157
  // dispatch reads/writes + its tenant_secrets table).
158
- r.requires("billing-foundation");
159
- r.requires("config");
160
- r.requires("secrets");
158
+ r.requires({ features: ["billing-foundation"] });
159
+ r.requires({ features: ["config"] });
160
+ r.requires({ features: ["secrets"] });
161
161
  r.envSchema(subscriptionStripeEnvSchema);
162
162
 
163
163
  // Runtime config. api-key + webhook-secret declare backing:"secrets" —
@@ -68,8 +68,8 @@ function registerTags(
68
68
  if (toggleable !== undefined) r.toggleable(toggleable);
69
69
 
70
70
  const tagAssignmentEntity = createTagAssignmentEntity(ownership, parents);
71
- r.entity("tag", tagEntity);
72
- r.entity("tag-assignment", tagAssignmentEntity);
71
+ r.entity({ name: "tag", ...tagEntity });
72
+ r.entity({ name: "tag-assignment", ...tagAssignmentEntity });
73
73
 
74
74
  r.writeHandler(createCreateTagHandler(access));
75
75
  r.writeHandler(createUpdateTagHandler(access));
@@ -99,12 +99,12 @@ export function createTemplateResolverFeature(opts: TemplateResolverOptions = {}
99
99
  resourceMaxBytes: createSystemConfig("number", { default: 2_097_152, bounds: { min: 1 } }),
100
100
  },
101
101
  });
102
- r.entity("template-resource", templateResourceEntity);
102
+ r.entity({ name: "template-resource", ...templateResourceEntity });
103
103
  // Only when the app actually mounts a user-owned collection: the entity's
104
104
  // `content` is `userOwned`, which makes it subject data — the boot guard
105
105
  // then requires an EXT_USER_DATA hook (mount `template-resolver-user-data`).
106
106
  // Registering it unconditionally would impose that on every app.
107
- if (hasUserOwned) r.entity("user-content-entry", userContentEntryEntity);
107
+ if (hasUserOwned) r.entity({ name: "user-content-entry", ...userContentEntryEntity });
108
108
 
109
109
  const handlers = {
110
110
  upsertSystem: r.writeHandler(upsertSystemWrite),
@@ -16,7 +16,7 @@ export const templateResolverUserDataFeature = defineFeature("template-resolver-
16
16
  // template-resolver is optional for the same reason as in
17
17
  // notes-history-user-data: a toggleable mount would otherwise trip the
18
18
  // "effectively disabled" boot warning even though the entity is there.
19
- r.requires("user-data-rights");
19
+ r.requires({ features: ["user-data-rights"] });
20
20
  r.optionalRequires("template-resolver");
21
21
  r.useExtension(EXT_USER_DATA, "user-content-entry", {
22
22
  export: userContentExportHook,
@@ -80,7 +80,7 @@ export function createTenantFeature(options?: TenantFeatureOptions): FeatureDefi
80
80
  recommended: true,
81
81
  });
82
82
  r.systemScope();
83
- r.requires("config");
83
+ r.requires({ features: ["config"] });
84
84
  r.extendsRegistrar(EXT_ASSIGNABLE_ROLE, {});
85
85
  // The elevation guards read the registry, the screens only these options: a role the
86
86
  // server accepts but the screen omits is silently stripped on the next member-roles save.
@@ -92,9 +92,9 @@ export function createTenantFeature(options?: TenantFeatureOptions): FeatureDefi
92
92
  );
93
93
  }
94
94
  });
95
- r.entity("tenant", tenantEntity);
96
- r.entity("tenant-membership", tenantMembershipEntity);
97
- r.entity("tenant-invitation", tenantInvitationEntity);
95
+ r.entity({ name: "tenant", ...tenantEntity });
96
+ r.entity({ name: "tenant-membership", ...tenantMembershipEntity });
97
+ r.entity({ name: "tenant-invitation", ...tenantInvitationEntity });
98
98
 
99
99
  r.config({
100
100
  keys: {
@@ -292,7 +292,7 @@ export async function seedTenant(
292
292
 
293
293
  // "tenant:seed" matches no handler-scoped hook, only entity-scoped ones
294
294
  // (keyed by result.entityName === "tenant") — that's exactly what
295
- // tier-engine's `r.hook("postSave", { allOf: "tenant" }, ...)` needs.
295
+ // tier-engine's `r.hook({ type: "postSave", target: { allOf: "tenant" }, ... })` needs.
296
296
  await fireEntityPostSave(hooks, "tenant:seed", result.data, { tenantId: options.id, db });
297
297
 
298
298
  return { id: options.id };
@@ -63,7 +63,7 @@ export function createTenantLifecycleFeature(): FeatureDefinition {
63
63
  category: "compliance",
64
64
  recommended: false,
65
65
  });
66
- r.requires("tenant", "compliance-profiles");
66
+ r.requires({ features: ["tenant", "compliance-profiles"] });
67
67
  r.optionalRequires("sessions");
68
68
  r.usesApi("compliance.forTenant");
69
69
 
@@ -140,8 +140,7 @@ export function createTenantLifecycleFeature(): FeatureDefinition {
140
140
  if (!ctx.registry) {
141
141
  throw new Error("run-tenant-destruction: ctx.registry required (JobContext incomplete)");
142
142
  }
143
- const T = (await import("@cosmicdrift/kumiko-framework/time")).getTemporal();
144
- const now = T.Now.instant();
143
+ const now = ctx.tz.now();
145
144
  // Without crossTenantReads (undeclared hatch) fall through to the sweep.
146
145
  if (ctx.crossTenantReads && !(await hasTenantDestructionWork(ctx.crossTenantReads, now))) {
147
146
  // skip: no tenant is due, so the sweep would not touch anything
@@ -22,7 +22,7 @@ export function createTenantSettingsFeature(
22
22
  opts: TenantSettingsFeatureOptions = {},
23
23
  ): FeatureDefinition {
24
24
  return defineFeature(TENANT_SETTINGS_FEATURE_NAME, (r) => {
25
- r.requires("config");
25
+ r.requires({ features: ["config"] });
26
26
  r.describe(
27
27
  "Per-tenant Currency + Locale defaults, exposed as two config keys (tenant-settings:config:currency, tenant-settings:config:locale) that surface in the self-populating Settings-Hub via `mask`. Pair with defineCreateWithTenantDefaults to auto-fill a money field's currency or a locale field on create instead of hard-coding a literal per entity.",
28
28
  );