@cosmicdrift/kumiko-bundled-features 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 (104) hide show
  1. package/dist/admin-shell/feature.js +9 -9
  2. package/dist/agent-tools/tool-dispatch.d.ts +9 -1
  3. package/dist/agent-tools/tool-dispatch.js +55 -5
  4. package/dist/audit/feature.js +1 -1
  5. package/dist/auth-email-password/constants.d.ts +0 -1
  6. package/dist/auth-email-password/constants.js +0 -2
  7. package/dist/auth-email-password/feature.js +3 -3
  8. package/dist/auth-email-password/handlers/invite-create.write.js +1 -0
  9. package/dist/auth-mfa/feature.js +4 -4
  10. package/dist/auth-mfa-user-data/index.js +1 -1
  11. package/dist/billing-foundation/consumer-protection/register-consent.js +1 -1
  12. package/dist/billing-foundation/feature.js +1 -1
  13. package/dist/cap-counter/cap-reservation.js +73 -25
  14. package/dist/cap-counter/constants.d.ts +1 -0
  15. package/dist/cap-counter/constants.js +2 -0
  16. package/dist/cap-counter/feature.js +2 -2
  17. package/dist/cap-counter/tables.js +5 -2
  18. package/dist/cap-overview/feature.js +3 -3
  19. package/dist/channel-discord/feature.js +2 -2
  20. package/dist/channel-email/feature.js +1 -1
  21. package/dist/channel-in-app/feature.js +1 -1
  22. package/dist/channel-push/feature.js +1 -1
  23. package/dist/channel-slack/feature.js +2 -2
  24. package/dist/channel-teams/feature.js +2 -2
  25. package/dist/channel-telegram/feature.js +2 -2
  26. package/dist/compliance-profiles/feature.js +1 -1
  27. package/dist/compliance-profiles-ops/index.js +3 -3
  28. package/dist/config/feature.js +1 -1
  29. package/dist/custom-fields/feature.js +2 -2
  30. package/dist/custom-fields/wire-for-entity.js +17 -13
  31. package/dist/data-retention/feature.js +1 -1
  32. package/dist/data-retention/presets.d.ts +4 -4
  33. package/dist/delivery/db/queries/address-opt-outs.d.ts +1 -1
  34. package/dist/delivery/db/queries/address-opt-outs.js +6 -2
  35. package/dist/delivery/db/queries/preferences.d.ts +1 -1
  36. package/dist/delivery/db/queries/preferences.js +4 -4
  37. package/dist/delivery/delivery-service.js +9 -2
  38. package/dist/delivery/feature.js +9 -5
  39. package/dist/delivery/handlers/resubscribe-address.write.js +13 -9
  40. package/dist/delivery/handlers/resubscribe-user.write.js +3 -1
  41. package/dist/delivery/handlers/set-preference.write.js +3 -1
  42. package/dist/delivery/handlers/unsubscribe-address.write.js +3 -1
  43. package/dist/delivery/handlers/unsubscribe-user.write.js +3 -1
  44. package/dist/delivery/notification-type-aliases.d.ts +3 -0
  45. package/dist/delivery/notification-type-aliases.js +11 -0
  46. package/dist/derivatives-sharp/feature.js +1 -1
  47. package/dist/document-ingest-foundation/feature.js +2 -2
  48. package/dist/file-derivatives/feature.js +3 -3
  49. package/dist/file-foundation/feature.js +1 -1
  50. package/dist/file-provider-inmemory/feature.js +2 -2
  51. package/dist/file-provider-s3/feature.js +3 -3
  52. package/dist/file-provider-s3-env/feature.js +2 -2
  53. package/dist/files-tenant-data/index.js +1 -1
  54. package/dist/folders/feature.js +2 -2
  55. package/dist/folders-user-data/index.js +1 -1
  56. package/dist/form-draft/feature.js +2 -2
  57. package/dist/form-draft-user-data/index.js +1 -1
  58. package/dist/inbound-mail-foundation/connect-routes.d.ts +3 -0
  59. package/dist/inbound-mail-foundation/connect-routes.js +6 -1
  60. package/dist/inbound-mail-foundation/feature.js +1 -1
  61. package/dist/inbound-mail-foundation/types.d.ts +17 -7
  62. package/dist/inbound-mail-foundation/watch-supervisor.js +16 -2
  63. package/dist/inbound-provider-imap/feature.js +2 -2
  64. package/dist/inbound-provider-inmemory/feature.js +1 -1
  65. package/dist/ledger/feature.js +5 -5
  66. package/dist/legal-pages/feature.js +1 -1
  67. package/dist/mail-foundation/feature.js +1 -1
  68. package/dist/mail-transport-inmemory/feature.js +2 -2
  69. package/dist/mail-transport-smtp/feature.js +3 -3
  70. package/dist/managed-pages/css-gate.js +1 -1
  71. package/dist/managed-pages/feature.js +2 -2
  72. package/dist/metrics/feature.js +2 -2
  73. package/dist/notes-history/feature.js +2 -2
  74. package/dist/notes-history-user-data/index.js +1 -1
  75. package/dist/personal-access-tokens/feature.js +17 -13
  76. package/dist/readiness/feature.js +2 -2
  77. package/dist/renderer-foundation/feature.js +1 -1
  78. package/dist/renderer-pdf/feature.js +1 -1
  79. package/dist/secrets/feature.js +1 -1
  80. package/dist/seo/feature.js +1 -1
  81. package/dist/sessions/feature.js +20 -16
  82. package/dist/step-dispatcher/feature.js +1 -1
  83. package/dist/subscription-mollie/feature.js +1 -1
  84. package/dist/subscription-stripe/feature.js +3 -3
  85. package/dist/tags/feature.js +2 -2
  86. package/dist/template-resolver/feature.js +2 -2
  87. package/dist/template-resolver-user-data/index.js +1 -1
  88. package/dist/tenant/feature.js +4 -4
  89. package/dist/tenant/seeding.js +1 -1
  90. package/dist/tenant-lifecycle/feature.js +2 -3
  91. package/dist/tenant-settings/feature.js +1 -1
  92. package/dist/tier-engine/feature.js +86 -72
  93. package/dist/user/feature.js +1 -1
  94. package/dist/user-data-rights/feature.js +4 -4
  95. package/dist/user-data-rights-defaults/feature.js +2 -2
  96. package/dist/user-profile/feature.js +4 -4
  97. package/dist/waitlist/feature.js +2 -2
  98. package/package.json +10 -10
  99. package/src/agent-tools/changes.json +6 -0
  100. package/src/auth-email-password/changes.json +13 -0
  101. package/src/cap-counter/changes.json +7 -0
  102. package/src/crypto-shredding/changes.json +6 -0
  103. package/src/delivery/changes.json +6 -0
  104. package/src/inbound-mail-foundation/changes.json +13 -0
@@ -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 }),
@@ -48,7 +48,7 @@ import { configuredPiiSubjectKms, decryptPiiFieldValues, } from "@cosmicdrift/ku
48
48
  import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
49
49
  import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
50
50
  import { InboundMailAccountStatuses, InboundMailFoundationHandlers } from "./constants.js";
51
- import { MAIL_ACCOUNT_PII_FIELDS, syncCursorTable } from "./entities.js";
51
+ import { MAIL_ACCOUNT_PII_FIELDS, seenMessageTable, syncCursorTable } from "./entities.js";
52
52
  import { createOAuthAccessTokenManager, usesFoundationManagedOAuth, } from "./oauth-access-token.js";
53
53
  import { mailAccountsProjectionTable } from "./projection.js";
54
54
  import { resolveInboundProviderForAccount } from "./provider-factory.js";
@@ -255,6 +255,19 @@ export function createInboundMailSupervisor(deps) {
255
255
  // ---------------------------------------------------------------
256
256
  // Poll (Reconciliation).
257
257
  // ---------------------------------------------------------------
258
+ function unknownMessageIdsFilterFor(account) {
259
+ return async (providerMessageIds) => {
260
+ if (providerMessageIds.length === 0)
261
+ return new Set();
262
+ const seen = await selectMany(deps.db, seenMessageTable, {
263
+ tenantId: account.tenantId,
264
+ accountId: account.id,
265
+ providerMessageId: { in: providerMessageIds },
266
+ }, { fields: ["providerMessageId"] });
267
+ const seenIds = new Set(seen.map((row) => row.providerMessageId));
268
+ return new Set(providerMessageIds.filter((id) => !seenIds.has(id)));
269
+ };
270
+ }
258
271
  async function pollAccount(account, plugin) {
259
272
  let cursor = await loadCursor(account.id);
260
273
  let budget = maxMessagesPerPoll;
@@ -265,6 +278,7 @@ export function createInboundMailSupervisor(deps) {
265
278
  const result = await plugin.fetch(deps.providerCtx, account, cursor, {
266
279
  backfillWindowDays,
267
280
  maxMessages: budget,
281
+ filterUnknownMessageIds: unknownMessageIdsFilterFor(account),
268
282
  ...(accessToken !== undefined && { accessToken }),
269
283
  });
270
284
  await ingestBatch(account, result.messages, JSON.stringify(result.nextCursor));
@@ -224,8 +224,8 @@ export const inboundProviderImapFeature = defineFeature(FEATURE_NAME, (r) => {
224
224
  category: "notifications",
225
225
  recommended: false,
226
226
  });
227
- r.requires("inbound-mail-foundation");
228
- r.requires("secrets");
227
+ r.requires({ features: ["inbound-mail-foundation"] });
228
+ r.requires({ features: ["secrets"] });
229
229
  r.envSchema(mailAllowedPrivateHostsEnvSchema);
230
230
  r.useExtension(INBOUND_MAIL_PROVIDER_EXTENSION, IMAP_PROVIDER_KEY, imapInboundMailPlugin);
231
231
  });
@@ -110,6 +110,6 @@ export const inboundProviderInMemoryFeature = defineFeature(FEATURE_NAME, (r) =>
110
110
  category: "notifications",
111
111
  recommended: false,
112
112
  });
113
- r.requires("inbound-mail-foundation");
113
+ r.requires({ features: ["inbound-mail-foundation"] });
114
114
  r.useExtension("inboundMailProvider", INMEMORY_PROVIDER_KEY, inMemoryInboundMailPlugin);
115
115
  });
@@ -28,9 +28,9 @@ function registerLedger(r, access, toggleable) {
28
28
  });
29
29
  if (toggleable !== undefined)
30
30
  r.toggleable(toggleable);
31
- r.entity("account", accountEntity);
32
- r.entity("transaction", transactionEntity);
33
- r.entity("schedule", scheduleEntity);
31
+ r.entity({ name: "account", ...accountEntity });
32
+ r.entity({ name: "transaction", ...transactionEntity });
33
+ r.entity({ name: "schedule", ...scheduleEntity });
34
34
  // Chart of accounts — plain CRUD, no custom logic. No delete in v1: removing an
35
35
  // account that has postings would orphan them; a posting-aware guard lands with
36
36
  // the postings projection (Phase 1).
@@ -73,8 +73,8 @@ function registerLedger(r, access, toggleable) {
73
73
  access,
74
74
  description: "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.",
75
75
  }));
76
- r.hook("preSave", "schedule:create", requireScheduleSubjectPair);
77
- r.hook("preSave", "schedule:update", requireScheduleSubjectPair);
76
+ r.hook({ type: "preSave", target: "schedule:create", handler: requireScheduleSubjectPair });
77
+ r.hook({ type: "preSave", target: "schedule:update", handler: requireScheduleSubjectPair });
78
78
  r.writeHandler(createConfirmSchedulePeriodHandler(access));
79
79
  r.queryHandler(defineEntityListHandler("schedule", scheduleEntity, {
80
80
  access,
@@ -35,7 +35,7 @@ export function createLegalPagesFeature(opts = {}) {
35
35
  category: "content",
36
36
  recommended: false,
37
37
  });
38
- r.requires("template-resolver");
38
+ r.requires({ features: ["template-resolver"] });
39
39
  // Public HTML routes (default: 4 DACH routes, configurable via opts.routes)
40
40
  for (const route of routes) {
41
41
  r.httpRoute({
@@ -52,7 +52,7 @@ export const mailFoundationFeature = defineFeature(FEATURE_NAME, (r) => {
52
52
  category: "notifications",
53
53
  recommended: false,
54
54
  });
55
- r.requires("config");
55
+ r.requires({ features: ["config"] });
56
56
  // Plugin extension-point. Provider-features register here. The
57
57
  // entityName at registration time becomes the value tenants pick in
58
58
  // `provider` config-key (e.g. "smtp", "brevo-api").
@@ -62,11 +62,11 @@ export function clearInbox(tenantId) {
62
62
  // =============================================================================
63
63
  export const mailTransportInMemoryFeature = defineFeature(FEATURE_NAME, (r) => {
64
64
  r.describe('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).');
65
- // Kein r.requires("config") + kein r.requires("secrets") — der
65
+ // Kein requires auf "config" oder "secrets" — der
66
66
  // In-Memory-Transport hat keine Config (nichts zu konfigurieren) und
67
67
  // kein Secret. Der einzige hard-require ist mail-foundation, das den
68
68
  // extension-point "mailTransport" definiert.
69
- r.requires("mail-foundation");
69
+ r.requires({ features: ["mail-foundation"] });
70
70
  const plugin = {
71
71
  build: async (_ctx, tenantId) => {
72
72
  // Returnt den per-tenant Buffer. Identitätsstabil zwischen calls
@@ -57,9 +57,9 @@ export const mailTransportSmtpFeature = defineFeature(FEATURE_NAME, (r) => {
57
57
  category: "notifications",
58
58
  recommended: false,
59
59
  });
60
- r.requires("config");
61
- r.requires("secrets");
62
- r.requires("mail-foundation");
60
+ r.requires({ features: ["config"] });
61
+ r.requires({ features: ["secrets"] });
62
+ r.requires({ features: ["mail-foundation"] });
63
63
  r.envSchema(mailTransportSmtpEnvSchema);
64
64
  // Provider-secret. Sensitive: redact-helper for admin-UI display.
65
65
  const password = r.secret("smtp.password", {
@@ -15,7 +15,7 @@ import { MANAGED_PAGES_CSS_FEATURE } from "./branding.js";
15
15
  export function createManagedPagesCssFeature() {
16
16
  return defineFeature(MANAGED_PAGES_CSS_FEATURE, (r) => {
17
17
  r.describe('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.');
18
- r.requires("managed-pages");
18
+ r.requires({ features: ["managed-pages"] });
19
19
  r.toggleable({ default: false });
20
20
  });
21
21
  }
@@ -86,8 +86,8 @@ export function createManagedPagesFeature(opts) {
86
86
  category: "content",
87
87
  recommended: false,
88
88
  });
89
- r.requires("config");
90
- r.entity("page", pageEntity);
89
+ r.requires({ features: ["config"] });
90
+ r.entity({ name: "page", ...pageEntity });
91
91
  // Per-tenant branding config keys (scope: tenant). Write-validated via
92
92
  // keyDef.pattern (hex / https) — see branding.ts. read:all so the
93
93
  // anonymous render path may resolve them. The raw-CSS key is added only
@@ -103,7 +103,7 @@ export function createMetricsFeature(options) {
103
103
  recommended: false,
104
104
  });
105
105
  for (const feature of requiredFeatures(metrics))
106
- r.requires(feature);
106
+ r.requires({ features: [feature] });
107
107
  for (const metric of metrics)
108
108
  r.queryHandler(createTenantMetricQuery(metric, now));
109
109
  r.translations({ keys: METRICS_I18N });
@@ -122,7 +122,7 @@ export function createSystemMetricsFeature(options) {
122
122
  });
123
123
  r.systemScope();
124
124
  for (const feature of requiredFeatures(metrics))
125
- r.requires(feature);
125
+ r.requires({ features: [feature] });
126
126
  for (const metric of metrics)
127
127
  r.queryHandler(createSystemMetricQuery(metric, now));
128
128
  r.translations({ keys: METRICS_I18N });
@@ -27,13 +27,13 @@ function registerNotesHistory(r, access, ownership, parents) {
27
27
  recommended: false,
28
28
  });
29
29
  const entity = createNoteEntryEntity(ownership, parents);
30
- r.entity("note-entry", entity);
30
+ r.entity({ name: "note-entry", ...entity });
31
31
  // No write/query handler of its own — populated only as a side effect of
32
32
  // add-note (see handlers/add-note.write.ts), looked up by
33
33
  // notes-history-user-data's forget cascade. Registering the entity (not
34
34
  // just building it inline in the handler) is what makes it visible to the
35
35
  // registry-wide GDPR boot guards and to executor.ts's table/projection setup.
36
- r.entity("note-mention", noteMentionEntity);
36
+ r.entity({ name: "note-mention", ...noteMentionEntity });
37
37
  r.writeHandler(createAddNoteHandler(access, createNoteEntryExecutor(ownership)));
38
38
  r.queryHandler(defineEntityListHandler("note-entry", entity, {
39
39
  access,
@@ -13,7 +13,7 @@ export const notesHistoryUserDataFeature = defineFeature("notes-history-user-dat
13
13
  // is OPTIONAL: if it's mounted toggleable(default=false), a hard r.requires
14
14
  // would throw an "effectively disabled" boot warning even though the
15
15
  // note-entry entity exists and the hooks work fine.
16
- r.requires("user-data-rights");
16
+ r.requires({ features: ["user-data-rights"] });
17
17
  r.optionalRequires("notes-history");
18
18
  r.useExtension(EXT_USER_DATA, "note-entry", {
19
19
  export: noteEntryExportHook,
@@ -52,7 +52,7 @@ export function createPersonalAccessTokensFeature(options) {
52
52
  // roles — make both boot-time deps so a mis-wiring fails validateBoot.
53
53
  // auth-foundation owns EXT_TOKEN_VERIFIER, which the useExtension below
54
54
  // registers against.
55
- r.requires("user", "tenant", "auth-foundation");
55
+ r.requires({ features: ["user", "tenant", "auth-foundation"] });
56
56
  // Self-registers as a bearer-auth provider instead of the app wiring a
57
57
  // dedicated patResolver callback — the middleware finds it generically
58
58
  // via resolveTokenVerifier(), shape-matched by the PAT token prefix.
@@ -83,18 +83,22 @@ export function createPersonalAccessTokensFeature(options) {
83
83
  // #497), so a postSave hook on "user" gets a poisoned ctx.db here. The
84
84
  // concrete revoker is bound once run{Prod,Dev}App has a real db handle.
85
85
  let autoRevokeOnPasswordChange = options.autoRevokeOnPasswordChange;
86
- r.hook("postSave", { allOf: "user" }, async (result) => {
87
- // skip: nothing bound — same late-bind pattern as sessions/feature.ts
88
- if (!autoRevokeOnPasswordChange)
89
- return;
90
- // skip: brand-new user, no PATs can exist yet
91
- if (result.isNew)
92
- return;
93
- // skip: handler didn't touch any revoke-triggering field
94
- if (!PAT_REVOKE_TRIGGERING_FIELDS.some((field) => result.changes[field] !== undefined)) {
95
- return;
96
- }
97
- await autoRevokeOnPasswordChange(String(result.id));
86
+ r.hook({
87
+ type: "postSave",
88
+ target: { allOf: "user" },
89
+ handler: async (result) => {
90
+ // skip: nothing bound — same late-bind pattern as sessions/feature.ts
91
+ if (!autoRevokeOnPasswordChange)
92
+ return;
93
+ // skip: brand-new user, no PATs can exist yet
94
+ if (result.isNew)
95
+ return;
96
+ // skip: handler didn't touch any revoke-triggering field
97
+ if (!PAT_REVOKE_TRIGGERING_FIELDS.some((field) => result.changes[field] !== undefined)) {
98
+ return;
99
+ }
100
+ await autoRevokeOnPasswordChange(String(result.id));
101
+ },
98
102
  });
99
103
  const bindAutoRevokeOnPasswordChange = (revoker) => {
100
104
  // explicit constructor option wins over the runtime binding
@@ -16,8 +16,8 @@ export const readinessFeature = defineFeature("readiness", (r) => {
16
16
  category: "operations",
17
17
  recommended: false,
18
18
  });
19
- r.requires("config");
20
- r.requires("secrets");
19
+ r.requires({ features: ["config"] });
20
+ r.requires({ features: ["secrets"] });
21
21
  const queries = {
22
22
  status: r.queryHandler(statusQuery),
23
23
  };
@@ -17,7 +17,7 @@ export function createRendererFoundationFeature() {
17
17
  category: "notifications",
18
18
  recommended: false,
19
19
  });
20
- r.requires("template-resolver");
20
+ r.requires({ features: ["template-resolver"] });
21
21
  r.extendsRegistrar(RENDERER_EXTENSION, {
22
22
  onRegister: () => {
23
23
  // Plugin-Konformitäts-Check könnte hier: shape-validation der
@@ -29,7 +29,7 @@ export function createRendererPdfFeature() {
29
29
  category: "notifications",
30
30
  recommended: false,
31
31
  });
32
- r.requires("renderer-foundation");
32
+ r.requires({ features: ["renderer-foundation"] });
33
33
  r.useExtension(RENDERER_EXTENSION, "pdf", {
34
34
  kinds: ["document-pdf"],
35
35
  render: (req) => renderTablePdfRequest(req),
@@ -116,7 +116,7 @@ export function createSecretsFeature(opts = {}) {
116
116
  // uniqueIndex are not expressible via the field-DSL (jsonb-without-default,
117
117
  // now()-default), so the physical table is the DDL truth. Without this the
118
118
  // generated migration omitted those columns → prod-500 (publicstatus#116).
119
- r.entity("tenant-secret", tenantSecretEntity, { table: tenantSecretsTable });
119
+ r.entity({ name: "tenant-secret", ...tenantSecretEntity, backingTable: tenantSecretsTable });
120
120
  // Read-audit domain-event. Registered here so ops tools + MSPs can
121
121
  // discover the type; secrets-context.get parses payloads against
122
122
  // `secretReadSchema` at write time because the low-level append() path
@@ -108,7 +108,7 @@ export function createSeoFeature(opts) {
108
108
  // config keys need the config feature's write/read machinery to actually
109
109
  // function (same hard dependency managed-pages has) — only legal-pages/
110
110
  // managed-pages are genuinely optional (merged only when opted in).
111
- r.requires("config");
111
+ r.requires({ features: ["config"] });
112
112
  r.optionalRequires("legal-pages", "managed-pages");
113
113
  r.config({ keys: SEO_CONFIG_KEYS });
114
114
  r.queryHandler(seoConfigQuery);
@@ -59,7 +59,7 @@ export function createSessionsFeature(options) {
59
59
  // sessions-without-user wiring fails validateBoot instead of 500ing live.
60
60
  // auth-foundation owns EXT_SESSION_STORE, which the useExtension below
61
61
  // registers against.
62
- r.requires("user", "auth-foundation");
62
+ r.requires({ features: ["user", "auth-foundation"] });
63
63
  // store_user_sessions is a hot-path direct-write store: sessionCreator
64
64
  // inserts and the revoke handlers update rows WITHOUT emitting lifecycle
65
65
  // events (the row columns ARE the audit trail). Registering it as
@@ -144,21 +144,25 @@ export function createSessionsFeature(options) {
144
144
  // regardless.
145
145
  const REVOKE_TRIGGERING_FIELDS = ["passwordHash"];
146
146
  let autoRevoke = options?.autoRevokeOnPasswordChange;
147
- r.hook("postSave", { allOf: "user" }, async (ctx) => {
148
- // skip: nothing bound — stateless-JWT deployments without a runtime
149
- // that calls bindAutoRevokeOnPasswordChange keep the old behavior.
150
- if (!autoRevoke)
151
- return;
152
- // skip: brand-new user, no sessions can possibly exist yet. The
153
- // initial passwordHash on a user:create would trip the second guard
154
- // otherwise — every registration would do a mass-revoke roundtrip
155
- // for a user who literally has no rows in user_sessions.
156
- if (ctx.isNew)
157
- return;
158
- // skip: handler didn't touch any revoke-triggering field
159
- if (!REVOKE_TRIGGERING_FIELDS.some((field) => ctx.changes[field] !== undefined))
160
- return;
161
- await autoRevoke(String(ctx.id));
147
+ r.hook({
148
+ type: "postSave",
149
+ target: { allOf: "user" },
150
+ handler: async (ctx) => {
151
+ // skip: nothing bound — stateless-JWT deployments without a runtime
152
+ // that calls bindAutoRevokeOnPasswordChange keep the old behavior.
153
+ if (!autoRevoke)
154
+ return;
155
+ // skip: brand-new user, no sessions can possibly exist yet. The
156
+ // initial passwordHash on a user:create would trip the second guard
157
+ // otherwise — every registration would do a mass-revoke roundtrip
158
+ // for a user who literally has no rows in user_sessions.
159
+ if (ctx.isNew)
160
+ return;
161
+ // skip: handler didn't touch any revoke-triggering field
162
+ if (!REVOKE_TRIGGERING_FIELDS.some((field) => ctx.changes[field] !== undefined))
163
+ return;
164
+ await autoRevoke(String(ctx.id));
165
+ },
162
166
  });
163
167
  const bindAutoRevokeOnPasswordChange = (revoker) => {
164
168
  // explicit constructor option wins over the runtime binding
@@ -34,7 +34,7 @@ export function createStepDispatcherFeature() {
34
34
  recommended: false,
35
35
  });
36
36
  r.envSchema(stepDispatcherEnvSchema);
37
- r.requires("secrets");
37
+ r.requires({ features: ["secrets"] });
38
38
  // Literal name: the few-shot corpus parser only reads string-literal job names.
39
39
  // StepDispatcherJobNames.deliver mirrors it; feature.boot.test.ts pins the two together.
40
40
  r.job("deliver", {
@@ -102,7 +102,7 @@ export function createSubscriptionMollieFeature(options) {
102
102
  category: "billing",
103
103
  recommended: false,
104
104
  });
105
- r.requires("billing-foundation");
105
+ r.requires({ features: ["billing-foundation"] });
106
106
  r.envSchema(subscriptionMollieEnvSchema);
107
107
  const plugin = {
108
108
  verifyAndParseWebhook: verifyAndParse,
@@ -93,9 +93,9 @@ export function createSubscriptionStripeFeature(options = {}) {
93
93
  // Hard-deps: billing-foundation (plugin-host) + config (billing-live +
94
94
  // backing:"secrets" credentials) + secrets (the store the backing:secrets
95
95
  // dispatch reads/writes + its tenant_secrets table).
96
- r.requires("billing-foundation");
97
- r.requires("config");
98
- r.requires("secrets");
96
+ r.requires({ features: ["billing-foundation"] });
97
+ r.requires({ features: ["config"] });
98
+ r.requires({ features: ["secrets"] });
99
99
  r.envSchema(subscriptionStripeEnvSchema);
100
100
  // Runtime config. api-key + webhook-secret declare backing:"secrets" —
101
101
  // the value lives envelope-encrypted in the secrets store under
@@ -41,8 +41,8 @@ function registerTags(r, access, toggleable, ownership, parents) {
41
41
  if (toggleable !== undefined)
42
42
  r.toggleable(toggleable);
43
43
  const tagAssignmentEntity = createTagAssignmentEntity(ownership, parents);
44
- r.entity("tag", tagEntity);
45
- r.entity("tag-assignment", tagAssignmentEntity);
44
+ r.entity({ name: "tag", ...tagEntity });
45
+ r.entity({ name: "tag-assignment", ...tagAssignmentEntity });
46
46
  r.writeHandler(createCreateTagHandler(access));
47
47
  r.writeHandler(createUpdateTagHandler(access));
48
48
  r.writeHandler(createDeleteTagHandler(access));
@@ -63,13 +63,13 @@ export function createTemplateResolverFeature(opts = {}) {
63
63
  resourceMaxBytes: createSystemConfig("number", { default: 2_097_152, bounds: { min: 1 } }),
64
64
  },
65
65
  });
66
- r.entity("template-resource", templateResourceEntity);
66
+ r.entity({ name: "template-resource", ...templateResourceEntity });
67
67
  // Only when the app actually mounts a user-owned collection: the entity's
68
68
  // `content` is `userOwned`, which makes it subject data — the boot guard
69
69
  // then requires an EXT_USER_DATA hook (mount `template-resolver-user-data`).
70
70
  // Registering it unconditionally would impose that on every app.
71
71
  if (hasUserOwned)
72
- r.entity("user-content-entry", userContentEntryEntity);
72
+ r.entity({ name: "user-content-entry", ...userContentEntryEntity });
73
73
  const handlers = {
74
74
  upsertSystem: r.writeHandler(upsertSystemWrite),
75
75
  upsertTenant: r.writeHandler(upsertTenantWrite),
@@ -12,7 +12,7 @@ export const templateResolverUserDataFeature = defineFeature("template-resolver-
12
12
  // template-resolver is optional for the same reason as in
13
13
  // notes-history-user-data: a toggleable mount would otherwise trip the
14
14
  // "effectively disabled" boot warning even though the entity is there.
15
- r.requires("user-data-rights");
15
+ r.requires({ features: ["user-data-rights"] });
16
16
  r.optionalRequires("template-resolver");
17
17
  r.useExtension(EXT_USER_DATA, "user-content-entry", {
18
18
  export: userContentExportHook,
@@ -38,7 +38,7 @@ export function createTenantFeature(options) {
38
38
  recommended: true,
39
39
  });
40
40
  r.systemScope();
41
- r.requires("config");
41
+ r.requires({ features: ["config"] });
42
42
  r.extendsRegistrar(EXT_ASSIGNABLE_ROLE, {});
43
43
  // The elevation guards read the registry, the screens only these options: a role the
44
44
  // server accepts but the screen omits is silently stripped on the next member-roles save.
@@ -49,9 +49,9 @@ export function createTenantFeature(options) {
49
49
  throw new Error(`[tenant] assignable app role "${role}" (assignableFrom "${assignableFrom}") is declared but missing from the tenant screens' options; pass assignableAppRoles: collectAssignableAppRoles(appFeatures) to createTenantFeature.`);
50
50
  }
51
51
  });
52
- r.entity("tenant", tenantEntity);
53
- r.entity("tenant-membership", tenantMembershipEntity);
54
- r.entity("tenant-invitation", tenantInvitationEntity);
52
+ r.entity({ name: "tenant", ...tenantEntity });
53
+ r.entity({ name: "tenant-membership", ...tenantMembershipEntity });
54
+ r.entity({ name: "tenant-invitation", ...tenantInvitationEntity });
55
55
  r.config({
56
56
  keys: {
57
57
  // Stammdaten-Settings — Tenant-Admin darf ändern, alle dürfen lesen.
@@ -185,7 +185,7 @@ export async function seedTenant(db, options, hooks) {
185
185
  }
186
186
  // "tenant:seed" matches no handler-scoped hook, only entity-scoped ones
187
187
  // (keyed by result.entityName === "tenant") — that's exactly what
188
- // tier-engine's `r.hook("postSave", { allOf: "tenant" }, ...)` needs.
188
+ // tier-engine's `r.hook({ type: "postSave", target: { allOf: "tenant" }, ... })` needs.
189
189
  await fireEntityPostSave(hooks, "tenant:seed", result.data, { tenantId: options.id, db });
190
190
  return { id: options.id };
191
191
  }
@@ -19,7 +19,7 @@ export function createTenantLifecycleFeature() {
19
19
  category: "compliance",
20
20
  recommended: false,
21
21
  });
22
- r.requires("tenant", "compliance-profiles");
22
+ r.requires({ features: ["tenant", "compliance-profiles"] });
23
23
  r.optionalRequires("sessions");
24
24
  r.usesApi("compliance.forTenant");
25
25
  r.extendsRegistrar(EXT_TENANT_DATA, {});
@@ -74,8 +74,7 @@ export function createTenantLifecycleFeature() {
74
74
  if (!ctx.registry) {
75
75
  throw new Error("run-tenant-destruction: ctx.registry required (JobContext incomplete)");
76
76
  }
77
- const T = (await import("@cosmicdrift/kumiko-framework/time")).getTemporal();
78
- const now = T.Now.instant();
77
+ const now = ctx.tz.now();
79
78
  // Without crossTenantReads (undeclared hatch) fall through to the sweep.
80
79
  if (ctx.crossTenantReads && !(await hasTenantDestructionWork(ctx.crossTenantReads, now))) {
81
80
  // skip: no tenant is due, so the sweep would not touch anything
@@ -16,7 +16,7 @@ import { buildTenantSettingsKeys } from "./config.js";
16
16
  import { TENANT_SETTINGS_FEATURE_NAME } from "./constants.js";
17
17
  export function createTenantSettingsFeature(opts = {}) {
18
18
  return defineFeature(TENANT_SETTINGS_FEATURE_NAME, (r) => {
19
- r.requires("config");
19
+ r.requires({ features: ["config"] });
20
20
  r.describe("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.");
21
21
  r.config({ keys: buildTenantSettingsKeys(opts) });
22
22
  r.translations({