@cosmicdrift/kumiko-bundled-features 0.352.0 → 0.354.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 (259) hide show
  1. package/dist/agent-tools/agent-doc-lint.d.ts +2 -0
  2. package/dist/agent-tools/agent-doc-lint.js +61 -1
  3. package/dist/agent-tools/agent-manifest.d.ts +1 -0
  4. package/dist/agent-tools/agent-manifest.js +17 -5
  5. package/dist/agent-tools/feature.js +7 -2
  6. package/dist/agent-tools/index.d.ts +1 -1
  7. package/dist/agent-tools/index.js +1 -1
  8. package/dist/agent-tools/manifest-deny-qns.d.ts +3 -0
  9. package/dist/agent-tools/manifest-deny-qns.js +17 -0
  10. package/dist/agent-tools/tool-catalog.d.ts +2 -1
  11. package/dist/agent-tools/tool-catalog.js +4 -1
  12. package/dist/agent-tools/tool-dispatch.js +1 -1
  13. package/dist/audit/handlers/list.query.js +13 -8
  14. package/dist/auth-email-password/handlers/change-password.write.js +1 -0
  15. package/dist/auth-email-password/handlers/invite-accept-with-login.write.js +2 -2
  16. package/dist/auth-email-password/handlers/invite-accept.write.js +2 -2
  17. package/dist/auth-email-password/handlers/invite-create.write.js +17 -0
  18. package/dist/auth-email-password/handlers/invite-signup-complete.write.js +4 -1
  19. package/dist/auth-email-password/handlers/login.write.js +1 -0
  20. package/dist/auth-email-password/handlers/reset-password.write.js +1 -0
  21. package/dist/auth-email-password/handlers/signup-confirm.write.js +6 -4
  22. package/dist/auth-email-password/handlers/signup-request.write.js +51 -77
  23. package/dist/auth-email-password/handlers/token-request-handler.js +1 -0
  24. package/dist/auth-email-password/handlers/verify-email.write.js +1 -0
  25. package/dist/auth-email-password/index.d.ts +2 -0
  26. package/dist/auth-email-password/index.js +2 -0
  27. package/dist/auth-email-password/seeding.d.ts +2 -0
  28. package/dist/auth-email-password/seeding.js +30 -8
  29. package/dist/auth-email-password/signup-activation.d.ts +24 -0
  30. package/dist/auth-email-password/signup-activation.js +41 -0
  31. package/dist/auth-email-password/signup-token-store.d.ts +5 -0
  32. package/dist/auth-email-password/signup-token-store.js +8 -3
  33. package/dist/auth-email-password/web/auth-gate.d.ts +2 -2
  34. package/dist/auth-email-password/web/auth-gate.js +5 -5
  35. package/dist/auth-email-password/web/auth-redirect.d.ts +1 -0
  36. package/dist/auth-email-password/web/auth-redirect.js +14 -0
  37. package/dist/auth-email-password/web/login-screen.js +2 -2
  38. package/dist/auth-email-password/web/user-menu.js +1 -1
  39. package/dist/auth-mfa/feature.js +5 -0
  40. package/dist/auth-mfa/handlers/enable-confirm-preauth.write.js +1 -0
  41. package/dist/auth-mfa/handlers/verify.write.js +1 -0
  42. package/dist/auth-mfa/schema/user-mfa.d.ts +6 -0
  43. package/dist/auth-mfa/screens.js +2 -2
  44. package/dist/auth-mfa/web/mfa-recovery-codes-reveal.js +1 -1
  45. package/dist/auth-mfa/web/mfa-setup-preauth-screen.d.ts +1 -1
  46. package/dist/auth-mfa/web/mfa-setup-preauth-screen.js +3 -3
  47. package/dist/auth-mfa/web/mfa-verify-screen.d.ts +1 -1
  48. package/dist/auth-mfa/web/mfa-verify-screen.js +1 -1
  49. package/dist/billing-foundation/billing-info-query.d.ts +5 -2
  50. package/dist/billing-foundation/checkout-core.d.ts +7 -0
  51. package/dist/billing-foundation/checkout-core.js +7 -2
  52. package/dist/billing-foundation/entities.d.ts +8 -0
  53. package/dist/billing-foundation/handlers/start-plan-checkout.write.js +7 -1
  54. package/dist/billing-foundation/projection.d.ts +8 -0
  55. package/dist/cap-counter/entity.d.ts +1 -0
  56. package/dist/cap-overview/access.js +2 -9
  57. package/dist/cap-overview/handlers/tenant-caps-list.query.js +2 -2
  58. package/dist/cap-overview/handlers/tenant-options.query.js +4 -5
  59. package/dist/channel-in-app/in-app-channel.js +1 -1
  60. package/dist/channel-in-app/tables.js +4 -2
  61. package/dist/compliance-profiles/schema/profile-selection.d.ts +10 -8
  62. package/dist/compliance-profiles-ops/handlers/tenants-missing-profile.query.js +2 -4
  63. package/dist/config/handlers/values.query.d.ts +3 -1
  64. package/dist/config/handlers/values.query.js +7 -2
  65. package/dist/config/table.d.ts +6 -0
  66. package/dist/crypto-shredding/handlers/forget-subject.write.d.ts +1 -0
  67. package/dist/crypto-shredding/handlers/forget-subject.write.js +110 -61
  68. package/dist/custom-fields/entity.d.ts +4 -0
  69. package/dist/data-retention/schema/tenant-retention-override.d.ts +4 -0
  70. package/dist/delivery/db/queries/address-opt-outs.d.ts +3 -1
  71. package/dist/delivery/db/queries/address-opt-outs.js +12 -7
  72. package/dist/delivery/delivery-service.js +60 -12
  73. package/dist/delivery/public-names.js +1 -1
  74. package/dist/delivery/tables.d.ts +6 -0
  75. package/dist/delivery/unsubscribe.js +66 -14
  76. package/dist/document-ingest-foundation/entity.d.ts +4 -0
  77. package/dist/feature-toggles/feature.js +2 -3
  78. package/dist/feature-toggles/toggle-runtime.d.ts +5 -1
  79. package/dist/feature-toggles/toggle-runtime.js +30 -6
  80. package/dist/feature-toggles/toggle-sync-signal.d.ts +3 -2
  81. package/dist/feature-toggles/toggle-sync-signal.js +29 -10
  82. package/dist/file-derivatives/handlers/public-variant.query.d.ts +1 -0
  83. package/dist/file-derivatives/handlers/public-variant.query.js +7 -1
  84. package/dist/files-tenant-data/handlers/sweep-orphaned-derivatives.job.js +5 -0
  85. package/dist/files-tenant-data/hooks.js +61 -24
  86. package/dist/folders/entity.d.ts +8 -0
  87. package/dist/folders/feature.js +2 -10
  88. package/dist/folders/handlers/delete-folder.write.js +31 -4
  89. package/dist/form-draft/db/queries/cleanup.d.ts +1 -1
  90. package/dist/form-draft/db/queries/cleanup.js +4 -3
  91. package/dist/form-draft/entity.d.ts +2 -0
  92. package/dist/form-draft/handlers/cleanup.job.d.ts +5 -1
  93. package/dist/form-draft/handlers/cleanup.job.js +11 -5
  94. package/dist/foundation-shared/index.d.ts +1 -1
  95. package/dist/foundation-shared/index.js +1 -1
  96. package/dist/foundation-shared/mail-host-policy.d.ts +4 -0
  97. package/dist/foundation-shared/mail-host-policy.js +13 -4
  98. package/dist/inbound-mail-foundation/entities.d.ts +25 -0
  99. package/dist/inbound-mail-foundation/projection.d.ts +20 -0
  100. package/dist/inbound-provider-imap/feature.js +2 -0
  101. package/dist/inbound-provider-imap/imap-client.js +3 -4
  102. package/dist/jobs/constants.d.ts +1 -0
  103. package/dist/jobs/constants.js +1 -0
  104. package/dist/jobs/db/queries/retention.d.ts +1 -0
  105. package/dist/jobs/db/queries/retention.js +5 -0
  106. package/dist/jobs/db/queries/stale-run-sweep.d.ts +2 -0
  107. package/dist/jobs/db/queries/stale-run-sweep.js +13 -2
  108. package/dist/jobs/feature.js +7 -1
  109. package/dist/jobs/handlers/list.query.d.ts +2 -1
  110. package/dist/jobs/handlers/list.query.js +40 -15
  111. package/dist/jobs/handlers/tenant-failures.query.js +1 -1
  112. package/dist/jobs/handlers/tenant-runs.query.d.ts +17 -0
  113. package/dist/jobs/handlers/tenant-runs.query.js +72 -0
  114. package/dist/jobs/i18n.js +1 -0
  115. package/dist/jobs/index.d.ts +2 -0
  116. package/dist/jobs/index.js +1 -0
  117. package/dist/jobs/job-run-logger.d.ts +2 -2
  118. package/dist/jobs/job-run-logger.js +98 -9
  119. package/dist/jobs/job-run-table.d.ts +6 -0
  120. package/dist/jobs/tenant-job-failure-table.js +15 -2
  121. package/dist/jobs/tenant-job-run-table.d.ts +4 -0
  122. package/dist/jobs/tenant-job-run-table.js +34 -0
  123. package/dist/ledger/entity.d.ts +24 -8
  124. package/dist/ledger/entity.js +17 -5
  125. package/dist/ledger/feature.js +3 -0
  126. package/dist/ledger/handlers/confirm-schedule-period.write.js +9 -3
  127. package/dist/ledger/handlers/create-transaction.write.js +1 -0
  128. package/dist/ledger/handlers/reverse-transaction.write.js +1 -0
  129. package/dist/ledger/schedule-subject-pair.d.ts +2 -0
  130. package/dist/ledger/schedule-subject-pair.js +24 -0
  131. package/dist/ledger/schemas.js +8 -2
  132. package/dist/mail-transport-smtp/feature.d.ts +3 -4
  133. package/dist/mail-transport-smtp/feature.js +4 -13
  134. package/dist/managed-pages/feature.js +28 -10
  135. package/dist/managed-pages/table.d.ts +12 -0
  136. package/dist/notes-history/entity.d.ts +12 -0
  137. package/dist/notes-history/entity.js +4 -0
  138. package/dist/notes-history/feature.js +2 -10
  139. package/dist/personal-access-tokens/handlers/create.write.js +1 -0
  140. package/dist/personal-access-tokens/schema/api-token.d.ts +6 -0
  141. package/dist/personal-access-tokens/screens.js +14 -3
  142. package/dist/secrets/table.d.ts +1 -0
  143. package/dist/seo/feature.js +28 -6
  144. package/dist/sessions/feature.d.ts +1 -0
  145. package/dist/sessions/feature.js +9 -8
  146. package/dist/sessions/handlers/detail.query.d.ts +1 -1
  147. package/dist/sessions/handlers/detail.query.js +2 -2
  148. package/dist/sessions/handlers/list.query.d.ts +1 -1
  149. package/dist/sessions/handlers/list.query.js +5 -3
  150. package/dist/sessions/schema/user-session.d.ts +4 -0
  151. package/dist/sessions/screens.d.ts +2 -2
  152. package/dist/sessions/screens.js +6 -7
  153. package/dist/shared/assert-no-where-rule-in-ownership-write.d.ts +2 -0
  154. package/dist/shared/assert-no-where-rule-in-ownership-write.js +18 -0
  155. package/dist/shared/encrypt-for-direct-write.js +10 -2
  156. package/dist/shared/index.d.ts +4 -3
  157. package/dist/shared/index.js +4 -3
  158. package/dist/shared/parent-visibility.d.ts +2 -1
  159. package/dist/shared/parent-visibility.js +21 -0
  160. package/dist/shared/rate-limited-text-response.d.ts +2 -0
  161. package/dist/shared/rate-limited-text-response.js +9 -0
  162. package/dist/shared/row-bound-grant.js +16 -2
  163. package/dist/shared/session-locale-field.d.ts +1 -0
  164. package/dist/shared/session-locale-field.js +6 -0
  165. package/dist/tags/entity.d.ts +9 -0
  166. package/dist/tags/feature.js +2 -10
  167. package/dist/template-resolver/api.d.ts +2 -2
  168. package/dist/template-resolver/handlers/list.query.d.ts +2 -0
  169. package/dist/template-resolver/handlers/list.query.js +2 -2
  170. package/dist/template-resolver/table.d.ts +48 -30
  171. package/dist/template-resolver/user-content-table.d.ts +34 -20
  172. package/dist/tenant/decrypt-tenant-names.d.ts +3 -0
  173. package/dist/tenant/decrypt-tenant-names.js +10 -0
  174. package/dist/tenant/feature.js +7 -1
  175. package/dist/tenant/handlers/me.query.js +5 -1
  176. package/dist/tenant/handlers/memberships.query.js +3 -2
  177. package/dist/tenant/handlers/tenant-directory.query.js +9 -3
  178. package/dist/tenant/handlers/toggle-enabled.write.js +2 -0
  179. package/dist/tenant/handlers/update-member-roles.write.js +1 -0
  180. package/dist/tenant/index.d.ts +1 -0
  181. package/dist/tenant/index.js +1 -0
  182. package/dist/tenant/invitation-table.d.ts +54 -8
  183. package/dist/tenant/invitation-table.js +6 -1
  184. package/dist/tenant/invited-membership.d.ts +8 -6
  185. package/dist/tenant/invited-membership.js +28 -7
  186. package/dist/tenant/membership-table.d.ts +4 -0
  187. package/dist/tenant/schema/tenant.d.ts +18 -10
  188. package/dist/tenant/schema/tenant.js +4 -6
  189. package/dist/tenant/screens.js +1 -1
  190. package/dist/tenant-handover/move-entity-graph.js +11 -18
  191. package/dist/tenant-handover/transfer-graph.d.ts +2 -21
  192. package/dist/tenant-handover/transfer-graph.js +8 -75
  193. package/dist/tenant-lifecycle/boot-checks.d.ts +1 -0
  194. package/dist/tenant-lifecycle/boot-checks.js +13 -0
  195. package/dist/tenant-lifecycle/constants.d.ts +3 -0
  196. package/dist/tenant-lifecycle/constants.js +5 -0
  197. package/dist/tenant-lifecycle/events.d.ts +5 -0
  198. package/dist/tenant-lifecycle/events.js +5 -0
  199. package/dist/tenant-lifecycle/feature.js +6 -3
  200. package/dist/tenant-lifecycle/run-tenant-destroy.d.ts +10 -5
  201. package/dist/tenant-lifecycle/run-tenant-destroy.js +59 -17
  202. package/dist/tenant-lifecycle/stages.d.ts +8 -1
  203. package/dist/tenant-lifecycle/stages.js +33 -4
  204. package/dist/tenant-lifecycle/testing.d.ts +8 -0
  205. package/dist/tenant-lifecycle/testing.js +42 -0
  206. package/dist/tier-engine/entity.d.ts +2 -0
  207. package/dist/tier-engine/handlers/set-tenant-tier.write.js +1 -0
  208. package/dist/tier-engine/tier-resolver.d.ts +2 -0
  209. package/dist/user/handlers/detail.query.js +2 -0
  210. package/dist/user/handlers/list.query.js +6 -2
  211. package/dist/user/handlers/update.write.js +1 -0
  212. package/dist/user/schema/user.d.ts +22 -8
  213. package/dist/user/seeding.d.ts +1 -0
  214. package/dist/user/seeding.js +20 -0
  215. package/dist/user-data-rights/boot-checks.js +5 -0
  216. package/dist/user-data-rights/handlers/confirm-deletion-by-token.write.js +2 -2
  217. package/dist/user-data-rights/handlers/request-deletion.write.js +1 -0
  218. package/dist/user-data-rights/index.d.ts +1 -0
  219. package/dist/user-data-rights/index.js +1 -0
  220. package/dist/user-data-rights/schema/download-attempt.d.ts +14 -0
  221. package/dist/user-data-rights/schema/download-attempt.js +17 -9
  222. package/dist/user-data-rights/schema/download-token.d.ts +8 -0
  223. package/dist/user-data-rights/schema/export-job.d.ts +16 -8
  224. package/dist/user-profile/handlers/change-email.write.js +8 -2
  225. package/dist/waitlist/entity.d.ts +22 -6
  226. package/dist/workflow-runner/event-trigger.d.ts +1 -0
  227. package/dist/workflow-runner/event-trigger.js +63 -15
  228. package/dist/workflow-runner/handlers/resume-run.write.js +8 -2
  229. package/dist/workflow-runner/runner.d.ts +2 -1
  230. package/dist/workflow-runner/runner.js +5 -1
  231. package/package.json +35 -10
  232. package/src/agent-tools/changes.json +12 -0
  233. package/src/audit/changes.json +6 -0
  234. package/src/auth-email-password/changes.json +24 -0
  235. package/src/auth-mfa/changes.json +7 -0
  236. package/src/billing-foundation/changes.json +6 -0
  237. package/src/channel-in-app/changes.json +8 -1
  238. package/src/crypto-shredding/changes.json +12 -0
  239. package/src/delivery/changes.json +6 -0
  240. package/src/files/changes.json +24 -0
  241. package/src/files-tenant-data/changes.json +5 -0
  242. package/src/folders/changes.json +6 -0
  243. package/src/form-draft/changes.json +7 -1
  244. package/src/inbound-provider-imap/changes.json +6 -0
  245. package/src/jobs/changes.json +30 -0
  246. package/src/ledger/changes.json +23 -0
  247. package/src/managed-pages/changes.json +6 -0
  248. package/src/notes-history/changes.json +13 -0
  249. package/src/seo/changes.json +5 -0
  250. package/src/template-resolver/changes.json +5 -0
  251. package/src/tenant/changes.json +19 -0
  252. package/src/tenant-handover/changes.json +12 -0
  253. package/src/tenant-lifecycle/changes.json +22 -0
  254. package/src/tier-engine/changes.json +5 -0
  255. package/src/user/changes.json +15 -0
  256. package/src/user-data-rights/changes.json +6 -0
  257. package/src/workflow-runner/changes.json +5 -0
  258. package/dist/shared/has-where-rule.d.ts +0 -2
  259. package/dist/shared/has-where-rule.js +0 -10
@@ -4,6 +4,7 @@ export declare const AgentDocGapKinds: {
4
4
  readonly handlerWithoutDescription: "handler-without-description";
5
5
  readonly customScreenWithoutDescription: "custom-screen-without-description";
6
6
  readonly exposedEntityWithoutDescription: "exposed-entity-without-description";
7
+ readonly handlerWithoutTranslation: "handler-without-translation";
7
8
  };
8
9
  export type AgentDocGapKind = (typeof AgentDocGapKinds)[keyof typeof AgentDocGapKinds];
9
10
  export type AgentDocGap = {
@@ -13,4 +14,5 @@ export type AgentDocGap = {
13
14
  readonly message: string;
14
15
  };
15
16
  export declare function findAgentDocGaps(features: readonly FeatureDefinition[]): readonly AgentDocGap[];
17
+ export declare function findHandlerTranslationGaps(features: readonly FeatureDefinition[]): readonly AgentDocGap[];
16
18
  export declare function formatAgentDocGap(gap: AgentDocGap): string;
@@ -1,10 +1,12 @@
1
- import { isAgentVisibleScreen, QnTypes, qn, resolveAgentExposure, toKebab, } from "@cosmicdrift/kumiko-framework/engine";
1
+ import { handlerFieldLabelKey, handlerTitleKey, isAgentVisibleScreen, QnTypes, qn, resolveAgentExposure, toKebab, } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import * as z from "zod";
3
+ import { systemOnlyPropertyNames } from "./agent-manifest.js";
3
4
  export const AgentDocGapKinds = {
4
5
  handlerSchemaNotExpressible: "handler-schema-not-expressible",
5
6
  handlerWithoutDescription: "handler-without-description",
6
7
  customScreenWithoutDescription: "custom-screen-without-description",
7
8
  exposedEntityWithoutDescription: "exposed-entity-without-description",
9
+ handlerWithoutTranslation: "handler-without-translation",
8
10
  };
9
11
  // No shared screen-type union exists in the codebase to import (ScreenDefinition
10
12
  // is a discriminated union keyed by inline string literals) — name the one
@@ -107,6 +109,57 @@ function exposedEntityDocGaps(feature) {
107
109
  }
108
110
  return gaps;
109
111
  }
112
+ function actionFormHandlerQns(features) {
113
+ const handlerQns = new Set();
114
+ for (const feature of features) {
115
+ for (const screen of Object.values(feature.screens ?? {})) {
116
+ if (screen.type === "actionForm")
117
+ handlerQns.add(screen.handler);
118
+ }
119
+ }
120
+ return handlerQns;
121
+ }
122
+ function agentVisibleInputFieldNames(schema) {
123
+ if (!(schema instanceof z.ZodObject))
124
+ return [];
125
+ const fieldNames = Object.keys(schema.shape);
126
+ try {
127
+ const systemOnly = new Set(systemOnlyPropertyNames(z.toJSONSchema(schema, { io: "input" })));
128
+ return fieldNames.filter((field) => !systemOnly.has(field));
129
+ }
130
+ catch {
131
+ // skip: a schema without JSON Schema form is reported as its own gap kind
132
+ return fieldNames;
133
+ }
134
+ }
135
+ function translationGapsOfFeature(feature, coveredHandlerQns) {
136
+ const gaps = [];
137
+ // The client resolves translation keys verbatim (no feature prefix), so only
138
+ // fully spelled-out keys count.
139
+ const translated = new Set(Object.keys(feature.translations ?? {}));
140
+ for (const [name, def] of Object.entries(feature.writeHandlers ?? {})) {
141
+ if (!resolveAgentExposure(def, "write").expose)
142
+ continue;
143
+ if (feature.handlerEntityMappings?.[name] !== undefined)
144
+ continue;
145
+ const handlerQn = qn(toKebab(feature.name), QnTypes.write, toKebab(name));
146
+ if (coveredHandlerQns.has(handlerQn))
147
+ continue;
148
+ const missing = [
149
+ handlerTitleKey(handlerQn),
150
+ ...agentVisibleInputFieldNames(def.schema).map((field) => handlerFieldLabelKey(handlerQn, field)),
151
+ ].filter((key) => !translated.has(key));
152
+ if (missing.length === 0)
153
+ continue;
154
+ gaps.push({
155
+ qn: handlerQn,
156
+ feature: feature.name,
157
+ kind: AgentDocGapKinds.handlerWithoutTranslation,
158
+ message: `This write handler has no entity and no actionForm screen, so its title and field labels can only come from translations — missing keys: ${missing.join(", ")}.`,
159
+ });
160
+ }
161
+ return gaps;
162
+ }
110
163
  export function findAgentDocGaps(features) {
111
164
  const gaps = [];
112
165
  for (const feature of features) {
@@ -119,6 +172,13 @@ export function findAgentDocGaps(features) {
119
172
  // depend on it (mirrors agent-manifest.ts's sortedByKey rationale).
120
173
  return [...gaps].sort((a, b) => compareByCodePoint(a.qn, b.qn));
121
174
  }
175
+ // Kept out of findAgentDocGaps on purpose: the published agent-manifest guard
176
+ // fails on every gap that function returns, and these are advisory.
177
+ export function findHandlerTranslationGaps(features) {
178
+ const coveredHandlerQns = actionFormHandlerQns(features);
179
+ const gaps = features.flatMap((feature) => translationGapsOfFeature(feature, coveredHandlerQns));
180
+ return [...gaps].sort((a, b) => compareByCodePoint(a.qn, b.qn));
181
+ }
122
182
  export function formatAgentDocGap(gap) {
123
183
  return `${gap.qn} — ${gap.message}`;
124
184
  }
@@ -1,4 +1,5 @@
1
1
  import type { AgentManifest, AgentManifestOptions, RegistryManifestView } from "./types.js";
2
+ export declare function systemOnlyPropertyNames(jsonSchema: Record<string, unknown>): readonly string[];
2
3
  /** Registry snapshot → AI-agent manifest. Pure, deterministic, no I/O — mirrors
3
4
  * `buildToolCatalog`'s contract, just describing the app's shape instead of
4
5
  * generating callable tools from it. */
@@ -1,6 +1,7 @@
1
1
  import { hasAccess, isAgentVisibleScreen, resolveAgentExposure, SYSTEM_ONLY_JSON_SCHEMA_KEY, } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import { translationValueOtherText } from "@cosmicdrift/kumiko-framework/ui-types";
3
3
  import * as z from "zod";
4
+ import { recordManifestDenyQns } from "./manifest-deny-qns.js";
4
5
  /** The manifest surfaces one string per locale — a plural entry reads as its
5
6
  * CLDR `other` form since the manifest has no `count` to select a category. */
6
7
  function entryAsLabels(entry) {
@@ -118,13 +119,19 @@ function buildFeatures(features) {
118
119
  }
119
120
  // Agent dispatch never runs as a system identity, so properties the engine only
120
121
  // honors for system callers would be silently dropped — hide them from the tool.
121
- function withoutSystemOnlyProperties(jsonSchema) {
122
+ export function systemOnlyPropertyNames(jsonSchema) {
122
123
  const properties = jsonSchema["properties"];
123
124
  if (typeof properties !== "object" || properties === null)
124
- return jsonSchema;
125
- const hidden = Object.entries(properties)
125
+ return [];
126
+ return Object.entries(properties)
126
127
  .filter(([, property]) => property[SYSTEM_ONLY_JSON_SCHEMA_KEY] === true)
127
128
  .map(([name]) => name);
129
+ }
130
+ function withoutSystemOnlyProperties(jsonSchema) {
131
+ const properties = jsonSchema["properties"];
132
+ if (typeof properties !== "object" || properties === null)
133
+ return jsonSchema;
134
+ const hidden = systemOnlyPropertyNames(jsonSchema);
128
135
  if (hidden.length === 0)
129
136
  return jsonSchema;
130
137
  const required = jsonSchema["required"];
@@ -232,6 +239,9 @@ function lastSegment(qualifiedId) {
232
239
  function detailIdParam(screen) {
233
240
  return screen.type === "projectionDetail" ? (screen.idParam ?? "id") : "id";
234
241
  }
242
+ function screenTitles(translations, screenId, shortId) {
243
+ return entryAsLabels(translations[`${screenId}.title`] ?? translations[`screen:${shortId}.title`] ?? {});
244
+ }
235
245
  function buildScreens(screens, navs, workspaces, translations, roles, visibleNavIds) {
236
246
  const allNavs = [...navs.values()];
237
247
  const result = [];
@@ -257,7 +267,7 @@ function buildScreens(screens, navs, workspaces, translations, roles, visibleNav
257
267
  result.push({
258
268
  id: screen.id,
259
269
  type: screen.type,
260
- titles: labelsForSuffix(translations, `screen:${shortId}.title`),
270
+ titles: screenTitles(translations, screen.id, shortId),
261
271
  // `screen.description` may be a raw i18n key (`registry.getAllTranslations()`
262
272
  // double-prefixes it, see build-app-schema.ts:77-81) — match on suffix like
263
273
  // `labelsForSuffix` does for entity/field labels, else fall back to the
@@ -304,7 +314,7 @@ export function buildAgentManifest(registry, options) {
304
314
  const navs = buildNavs(navMap, workspaceMap, translations, roles, screenMap);
305
315
  const screens = buildScreens(screenMap, navMap, workspaceMap, translations, roles, new Set(navs.map((nav) => nav.id)));
306
316
  const workspaces = buildWorkspaces(workspaceMap, translations, roles);
307
- return {
317
+ const manifest = {
308
318
  builtForRoles: [...roles],
309
319
  features: sortedByKey(features, (f) => f.name),
310
320
  entities: sortedByKey(entities, (e) => e.name),
@@ -317,4 +327,6 @@ export function buildAgentManifest(registry, options) {
317
327
  ...(options.currency && { currency: options.currency }),
318
328
  },
319
329
  };
330
+ recordManifestDenyQns(manifest, new Set(options.denyQns ?? []));
331
+ return manifest;
320
332
  }
@@ -1,5 +1,5 @@
1
1
  import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
2
- import { findAgentDocGaps, formatAgentDocGap } from "./agent-doc-lint.js";
2
+ import { findAgentDocGaps, findHandlerTranslationGaps, formatAgentDocGap, } from "./agent-doc-lint.js";
3
3
  export const AGENT_TOOLS_FEATURE_NAME = "agent-tools";
4
4
  export function createAgentToolsFeature() {
5
5
  return defineFeature(AGENT_TOOLS_FEATURE_NAME, (r) => {
@@ -14,8 +14,13 @@ export function createAgentToolsFeature() {
14
14
  // would turn every incrementally-documented app into an outage.
15
15
  r.bootCheck(({ features }) => {
16
16
  const gaps = findAgentDocGaps(features);
17
+ const warningCount = findHandlerTranslationGaps(features).length;
18
+ if (warningCount > 0) {
19
+ // biome-ignore lint/suspicious/noConsole: operator-visibility at boot for agent-doc gaps
20
+ console.warn(`[agent-tools] ${warningCount} agent-exposed write handler(s) lack title/field-label translations — run \`kumiko agent lint\` for details.`);
21
+ }
17
22
  if (gaps.length === 0) {
18
- // skip: no documentation gaps, nothing to warn about
23
+ // skip: no blocking documentation gaps, nothing more to warn about
19
24
  return;
20
25
  }
21
26
  // biome-ignore lint/suspicious/noConsole: operator-visibility at boot for agent-doc gaps
@@ -1,5 +1,5 @@
1
1
  export type { AgentDocGap, AgentDocGapKind } from "./agent-doc-lint.js";
2
- export { AgentDocGapKinds, findAgentDocGaps, formatAgentDocGap } from "./agent-doc-lint.js";
2
+ export { AgentDocGapKinds, findAgentDocGaps, findHandlerTranslationGaps, formatAgentDocGap, } from "./agent-doc-lint.js";
3
3
  export { buildAgentManifest } from "./agent-manifest.js";
4
4
  export { AGENT_TOOLS_FEATURE_NAME, createAgentToolsFeature } from "./feature.js";
5
5
  export { buildToolCatalog, OPEN_FORM_TOOL_NAME, toolNameForQn } from "./tool-catalog.js";
@@ -1,4 +1,4 @@
1
- export { AgentDocGapKinds, findAgentDocGaps, formatAgentDocGap } from "./agent-doc-lint.js";
1
+ export { AgentDocGapKinds, findAgentDocGaps, findHandlerTranslationGaps, formatAgentDocGap, } from "./agent-doc-lint.js";
2
2
  export { buildAgentManifest } from "./agent-manifest.js";
3
3
  export { AGENT_TOOLS_FEATURE_NAME, createAgentToolsFeature } from "./feature.js";
4
4
  export { buildToolCatalog, OPEN_FORM_TOOL_NAME, toolNameForQn } from "./tool-catalog.js";
@@ -0,0 +1,3 @@
1
+ import type { AgentManifest } from "./types.js";
2
+ export declare function recordManifestDenyQns(manifest: AgentManifest, denyQns: ReadonlySet<string>): void;
3
+ export declare function assertCatalogMirrorsManifestDenyQns(manifest: AgentManifest, catalogDenyQns: ReadonlySet<string>): void;
@@ -0,0 +1,17 @@
1
+ // The deny list is deliberately not a manifest field (the manifest is prompt
2
+ // payload), so the catalog builder learns it through this side channel to
3
+ // detect a manifest cut that the catalog would silently not mirror.
4
+ const denyQnsByManifest = new WeakMap();
5
+ export function recordManifestDenyQns(manifest, denyQns) {
6
+ denyQnsByManifest.set(manifest, denyQns);
7
+ }
8
+ export function assertCatalogMirrorsManifestDenyQns(manifest, catalogDenyQns) {
9
+ const manifestDenyQns = denyQnsByManifest.get(manifest);
10
+ // skip: manifest has no registered deny list, nothing to mirror
11
+ if (!manifestDenyQns)
12
+ return;
13
+ const missing = [...manifestDenyQns].filter((qn) => !catalogDenyQns.has(qn));
14
+ if (missing.length > 0) {
15
+ throw new Error(`agent denyQns passed to buildAgentManifest but not to buildToolCatalog: ${missing.join(", ")} — the catalog would still expose their entity CRUD tools`);
16
+ }
17
+ }
@@ -24,5 +24,6 @@ export declare const OPEN_FORM_TOOL_NAME = "open_form";
24
24
  * to drop an entity CRUD tool, which no `agent.expose` on the manifest side can reach. It is
25
25
  * NOT read off the manifest on purpose — the manifest is prompt payload, and a list of the
26
26
  * handlers the model may not call has no business travelling to the provider. Pass the same
27
- * list to `buildAgentManifest` so the manifest stops describing what the catalog withholds. */
27
+ * list to `buildAgentManifest` so the manifest stops describing what the catalog withholds;
28
+ * a manifest built with `denyQns` that the catalog lacks throws. */
28
29
  export declare function buildToolCatalog(registry: RegistrySearchView, manifest: AgentManifest, options: ToolCatalogOptions): ToolCatalog;
@@ -1,5 +1,6 @@
1
1
  import { hasAccess, normalizeAccessEntry } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import { FILTER_OPS, isRecord } from "./filter-ops.js";
3
+ import { assertCatalogMirrorsManifestDenyQns } from "./manifest-deny-qns.js";
3
4
  const MAX_TOOL_NAME_LENGTH = 64;
4
5
  /** Field types that declare `filterable` (per `packages/framework/src/engine/types/fields.ts`).
5
6
  * Mapped to the JSON-Schema type an LLM tool-call argument should use. `undefined` = field type
@@ -466,13 +467,15 @@ function addClientTools(manifest, mode, sink) {
466
467
  * to drop an entity CRUD tool, which no `agent.expose` on the manifest side can reach. It is
467
468
  * NOT read off the manifest on purpose — the manifest is prompt payload, and a list of the
468
469
  * handlers the model may not call has no business travelling to the provider. Pass the same
469
- * list to `buildAgentManifest` so the manifest stops describing what the catalog withholds. */
470
+ * list to `buildAgentManifest` so the manifest stops describing what the catalog withholds;
471
+ * a manifest built with `denyQns` that the catalog lacks throws. */
470
472
  export function buildToolCatalog(registry, manifest, options) {
471
473
  const sink = { tools: [], dispatchTable: new Map(), usedNames: new Set() };
472
474
  const roleFilter = { roles: manifest.builtForRoles };
473
475
  const locale = manifest.tenantSettings.locale;
474
476
  const entityByName = new Map(manifest.entities.map((entity) => [entity.name, entity]));
475
477
  const denyQns = new Set(options.denyQns ?? []);
478
+ assertCatalogMirrorsManifestDenyQns(manifest, denyQns);
476
479
  addRegistrySearchTools(registry, roleFilter, denyQns, sink);
477
480
  const { detailQnByEntity, listQnByEntity, entityListDetailQns } = collectEntityHandlerQns(registry, roleFilter, denyQns);
478
481
  addGetTools(registry, detailQnByEntity, entityByName, locale, sink);
@@ -1,4 +1,4 @@
1
- import { runAsDirectCallEntry } from "@cosmicdrift/kumiko-framework/api";
1
+ import { runAsDirectCallEntry } from "@cosmicdrift/kumiko-framework/internal/request-context";
2
2
  import { FILTER_OPS, isRecord } from "./filter-ops.js";
3
3
  const SEARCH_RESULT_LIMIT = 10;
4
4
  const DEFAULT_LIST_LIMIT = 10;
@@ -34,7 +34,7 @@ function buildDateRange(from, to) {
34
34
  function escapeLikePattern(value) {
35
35
  return value.replace(/[\\%_]/g, (m) => `\\${m}`);
36
36
  }
37
- function buildAuditWhere(tenantId, p) {
37
+ function buildAuditWhere(tenantId, p, direction) {
38
38
  const where = { tenantId };
39
39
  if (p.aggregateType)
40
40
  where["aggregateType"] = p.aggregateType;
@@ -50,8 +50,10 @@ function buildAuditWhere(tenantId, p) {
50
50
  if (range)
51
51
  where["createdAt"] = range;
52
52
  const cursor = p.cursor ?? p.before;
53
+ // The cursor is an event id, so it only walks pages in id order: ascending
54
+ // pages continue past it, descending pages stop before it.
53
55
  if (cursor)
54
- where["id"] = { lt: BigInt(cursor) };
56
+ where["id"] = direction === "asc" ? { gt: BigInt(cursor) } : { lt: BigInt(cursor) };
55
57
  return where;
56
58
  }
57
59
  export const listQuery = defineQueryHandler({
@@ -81,15 +83,17 @@ export const listQuery = defineQueryHandler({
81
83
  handler: async (query, ctx) => {
82
84
  const p = query.payload;
83
85
  const scopeFilter = resolveAuditScopeFilter(query.user, p.scope);
86
+ const direction = p.sortDirection ?? "desc";
84
87
  const where = buildAuditWhere(scopeFilter.tenantId, {
85
88
  ...p,
86
89
  aggregateType: scopeFilter.aggregateType ?? p.aggregateType,
87
- });
90
+ }, direction);
88
91
  const rows = await selectMany(ctx.db, eventsTable, where, {
89
- orderBy: {
90
- col: query.payload.sort === "type" ? "type" : "createdAt",
91
- direction: query.payload.sortDirection ?? "desc",
92
- },
92
+ // `id` as tie-breaker keeps the order total, so pages never overlap.
93
+ orderBy: [
94
+ { col: p.sort === "type" ? "type" : "createdAt", direction },
95
+ { col: "id", direction },
96
+ ],
93
97
  limit: p.limit,
94
98
  });
95
99
  const serialised = rows.map((r) => ({
@@ -104,7 +108,8 @@ export const listQuery = defineQueryHandler({
104
108
  createdBy: r.createdBy,
105
109
  }));
106
110
  const last = serialised[serialised.length - 1];
107
- const nextId = serialised.length === p.limit && last ? last.id : null;
111
+ // An id cursor cannot resume a type-ordered listing, so that sort is one page.
112
+ const nextId = serialised.length === p.limit && last && p.sort !== "type" ? last.id : null;
108
113
  return {
109
114
  rows: serialised,
110
115
  nextCursor: nextId,
@@ -16,6 +16,7 @@ export const changePasswordWrite = defineWriteHandler({
16
16
  }),
17
17
  access: { roles: access.authenticated },
18
18
  escapeHatch: {
19
+ grants: ["systemIdentity"],
19
20
  reason: "Reads the caller's own passwordHash (privileged-only field) via ctx.queryAs(SYSTEM, ...) " +
20
21
  "and writes the new hash via ctx.writeAs(SYSTEM, user:update) — the caller's own " +
21
22
  "identity can't reach either, field-access on passwordHash is privileged-only.",
@@ -29,7 +29,7 @@ import { decryptStoredPii, sessionLocaleField, sessionTimezoneField } from "../.
29
29
  // kumiko-lint-ignore cross-feature-import invite-flow
30
30
  import { INVITATION_STATUS, tenantInvitationEntity, tenantInvitationsTable, } from "../../tenant/invitation-table.js";
31
31
  // kumiko-lint-ignore cross-feature-import membership grant for a privileged cross-tenant add
32
- import { grantInvitedMembershipRole, invitationIssuedAt } from "../../tenant/invited-membership.js";
32
+ import { grantInvitedMembershipRole } from "../../tenant/invited-membership.js";
33
33
  // kumiko-lint-ignore cross-feature-import global roles granted on invite accept live on the user row
34
34
  import { grantInvitedGlobalRoles, parseInvitedGlobalRoles, } from "../../user/invited-global-roles.js";
35
35
  // kumiko-lint-ignore cross-feature-import login-style password-check
@@ -118,7 +118,7 @@ export function createInviteAcceptWithLoginHandler(opts = {}) {
118
118
  userId,
119
119
  tenantId: invitationTenantId,
120
120
  role: invitationRole,
121
- invitationIssuedAt: invitationIssuedAt(invitation),
121
+ invitation,
122
122
  });
123
123
  if (!grant.isSuccess)
124
124
  return grant;
@@ -23,7 +23,7 @@ import { decryptStoredPii } from "../../shared/index.js";
23
23
  // kumiko-lint-ignore cross-feature-import invite-flow lebt in auth-email-password (Magic-Link), DB-row-owner ist tenant-feature
24
24
  import { INVITATION_STATUS, tenantInvitationEntity, tenantInvitationsTable, } from "../../tenant/invitation-table.js";
25
25
  // kumiko-lint-ignore cross-feature-import membership grant for a privileged cross-tenant add (like provisionSignupAccount)
26
- import { grantInvitedMembershipRole, invitationIssuedAt } from "../../tenant/invited-membership.js";
26
+ import { grantInvitedMembershipRole } from "../../tenant/invited-membership.js";
27
27
  // kumiko-lint-ignore cross-feature-import global roles granted on invite accept live on the user row
28
28
  import { grantInvitedGlobalRoles, parseInvitedGlobalRoles, } from "../../user/invited-global-roles.js";
29
29
  // kumiko-lint-ignore cross-feature-import auth handler reads user-row für email-match
@@ -93,7 +93,7 @@ export function createInviteAcceptHandler() {
93
93
  userId: event.user.id,
94
94
  tenantId: invitationTenantId,
95
95
  role: invitationRole,
96
- invitationIssuedAt: invitationIssuedAt(invitation),
96
+ invitation,
97
97
  });
98
98
  if (!grant.isSuccess)
99
99
  return grant;
@@ -24,10 +24,14 @@ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
24
24
  import * as z from "zod";
25
25
  // kumiko-lint-ignore cross-feature-import invite-flow lebt in auth-email-password (Magic-Link-Pattern), DB-row-owner ist tenant-feature
26
26
  import { INVITATION_STATUS, tenantInvitationEntity, tenantInvitationsTable, } from "../../tenant/invitation-table.js";
27
+ // kumiko-lint-ignore cross-feature-import membership version read is owned by the tenant feature
28
+ import { membershipVersionOf } from "../../tenant/invited-membership.js";
27
29
  // kumiko-lint-ignore cross-feature-import membership-role validation owned by tenant-feature
28
30
  import { findForbiddenMembershipRole, findForbiddenRoleAssignment, reservedMembershipRoleError, unassignableMembershipRoleError, } from "../../tenant/membership-roles.js";
29
31
  // kumiko-lint-ignore cross-feature-import global-role options are owned by the user feature
30
32
  import { GLOBAL_ROLE_OPTIONS } from "../../user/constants.js";
33
+ // kumiko-lint-ignore cross-feature-import invitee lookup by email reads the user row
34
+ import { userTable } from "../../user/schema/user.js";
31
35
  import { AUTH_INVITE_DEFAULT_TTL_MINUTES } from "../constants.js";
32
36
  import { renderInviteEmail } from "../email-templates.js";
33
37
  import { inviteAlreadyAccepted } from "../errors.js";
@@ -61,6 +65,16 @@ function checkAssignableInviteRole(opts, inviterRoles, role, ctx) {
61
65
  }
62
66
  return undefined;
63
67
  }
68
+ // 0 = the invitee has no membership in the tenant yet (or no account at all).
69
+ async function currentMembershipVersion(ctx, tenantId, email) {
70
+ const user = await ctx.db.global(userTable).fetchOne({
71
+ email,
72
+ isDeleted: false,
73
+ });
74
+ if (!user)
75
+ return 0;
76
+ return membershipVersionOf(ctx.db, tenantId, user.id);
77
+ }
64
78
  async function issueInvitation(opts, ttlSeconds, request, inviter, ctx, refuseAccepted = false) {
65
79
  if (!ctx.redis) {
66
80
  return writeFailure(new InternalError({ message: "invite-create requires ctx.redis for token store" }));
@@ -72,6 +86,7 @@ async function issueInvitation(opts, ttlSeconds, request, inviter, ctx, refuseAc
72
86
  // The unique index allows one row per (tenantId, email). Whatever its
73
87
  // status, a re-invite resets it to pending with a fresh token.
74
88
  const existing = await ctx.db.fetchOne(tenantInvitationsTable, { tenantId, email });
89
+ const membershipVersion = await currentMembershipVersion(ctx, tenantId, email);
75
90
  let invitationId;
76
91
  if (existing) {
77
92
  // Provisioning callers (waitlist) must not flip an accepted invite back to
@@ -96,6 +111,7 @@ async function issueInvitation(opts, ttlSeconds, request, inviter, ctx, refuseAc
96
111
  globalRoles: [...request.globalRoles],
97
112
  status: INVITATION_STATUS.pending,
98
113
  invitedBy: inviter.id,
114
+ membershipVersion,
99
115
  expiresAt,
100
116
  },
101
117
  }, inviter, ctx.db);
@@ -109,6 +125,7 @@ async function issueInvitation(opts, ttlSeconds, request, inviter, ctx, refuseAc
109
125
  globalRoles: [...request.globalRoles],
110
126
  status: INVITATION_STATUS.pending,
111
127
  invitedBy: inviter.id,
128
+ membershipVersion,
112
129
  expiresAt,
113
130
  }, inviter, ctx.db);
114
131
  if (!createResult.isSuccess)
@@ -21,7 +21,7 @@ import { createEventStoreExecutor, createTenantDb, } from "@cosmicdrift/kumiko-f
21
21
  import { buildSessionRoles, createSystemUser, defineWriteHandler, } from "@cosmicdrift/kumiko-framework/engine";
22
22
  import { InternalError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
23
23
  import * as z from "zod";
24
- import { decryptStoredPii } from "../../shared/index.js";
24
+ import { decryptStoredPii, registrationLocaleFromRequest } from "../../shared/index.js";
25
25
  // kumiko-lint-ignore cross-feature-import invite-flow
26
26
  import { INVITATION_STATUS, tenantInvitationEntity, tenantInvitationsTable, } from "../../tenant/invitation-table.js";
27
27
  // kumiko-lint-ignore cross-feature-import reserved-role check owned by tenant-feature
@@ -92,11 +92,13 @@ export function createInviteSignupCompleteHandler(opts = {}) {
92
92
  // @cast-boundary db-runner — helpers use only the query API that
93
93
  // DbConnection and DbTx share.
94
94
  const dbConn = ctx.db.unsafeRaw();
95
+ const registrationLocale = registrationLocaleFromRequest();
95
96
  const { id: userId } = await seedUserWithPassword(dbConn, {
96
97
  email: invitationEmail,
97
98
  password: event.payload.password,
98
99
  displayName: invitationEmail.split("@")[0] ?? invitationEmail,
99
100
  emailVerified: true,
101
+ ...registrationLocale,
100
102
  ...(invitationGlobalRoles.length > 0 && { roles: invitationGlobalRoles }),
101
103
  });
102
104
  // Membership-Add via seed-helper (gleiches Pattern wie
@@ -136,6 +138,7 @@ export function createInviteSignupCompleteHandler(opts = {}) {
136
138
  id: userId,
137
139
  tenantId: invitationTenantId,
138
140
  roles: mergedRoles,
141
+ ...registrationLocale,
139
142
  };
140
143
  committed = true;
141
144
  return {
@@ -151,6 +151,7 @@ export function createLoginHandler(opts = {}) {
151
151
  access: { roles: ["anonymous"] },
152
152
  rateLimit: { per: "ip+handler", limit: 20, windowSeconds: 60 },
153
153
  escapeHatch: {
154
+ grants: ["systemIdentity", "unsafeRaw"],
154
155
  reason: "Unauthenticated login has no caller identity yet — it looks up the user row by " +
155
156
  "email via ctx.queryAs(SYSTEM, ...) and resolves tenant membership via " +
156
157
  "ctx.resolveActiveMembership before a session exists. It also reads the MFA " +
@@ -23,6 +23,7 @@ export function createResetPasswordHandler(opts) {
23
23
  access: { roles: ["anonymous"] },
24
24
  rateLimit: { per: "ip+handler", limit: 20, windowSeconds: 60 },
25
25
  escapeHatch: {
26
+ grants: ["systemIdentity"],
26
27
  reason: "Anonymous token holder has no session — runConfirmTokenFlow loads and updates the " +
27
28
  "target user via ctx.writeAs(SYSTEM, user:update) to set the new password hash.",
28
29
  },
@@ -24,7 +24,7 @@ import { ConflictError, InternalError, reraiseAsKumikoError, writeFailure, } fro
24
24
  import { generateUniqueName } from "@cosmicdrift/kumiko-framework/random";
25
25
  import { generateId } from "@cosmicdrift/kumiko-framework/utils";
26
26
  import * as z from "zod";
27
- import { findSignupHandoverProvider, SIGNUP_HANDOVER_BENIGN_CLAIM_REJECTION_CODE, } from "../../shared/index.js";
27
+ import { findSignupHandoverProvider, registrationLocaleFromRequest, SIGNUP_HANDOVER_BENIGN_CLAIM_REJECTION_CODE, } from "../../shared/index.js";
28
28
  // kumiko-lint-ignore cross-feature-import signup-confirm reads tenants.key for slug-uniqueness check (TOCTOU + DB-unique-index zusammen)
29
29
  import { tenantTable } from "../../tenant/schema/tenant.js";
30
30
  import { invalidSignupToken, signupEmailAlreadyRegistered } from "../errors.js";
@@ -116,6 +116,7 @@ export function createSignupConfirmHandler(opts = {}) {
116
116
  // Display-Name aus email-prefix als sinnvolles Default; User kann
117
117
  // den Tenant-Namen + sein eigenes displayName später ändern.
118
118
  const displayName = email.split("@")[0] ?? email;
119
+ const registrationLocale = registrationLocaleFromRequest();
119
120
  let provisioned;
120
121
  try {
121
122
  provisioned = await provisionSignupAccount(dbConn, {
@@ -124,9 +125,9 @@ export function createSignupConfirmHandler(opts = {}) {
124
125
  displayName,
125
126
  tenantId,
126
127
  tenantKey,
127
- // Tenant-Display-Name als Default = Email. User wechselt das im
128
- // Settings-Screen. Konzept "Tenant" leakt nicht in die Signup-UI.
129
- tenantName: email,
128
+ // Generated slug as default name: no personal data (the email) in the tenant name.
129
+ tenantName: tenantKey,
130
+ ...registrationLocale,
130
131
  },
131
132
  // #1463: seedTenant's postSave hooks (tier-engine's auto-default-
132
133
  // tier, an app's auto-default-compliance) must fire on self-signup
@@ -151,6 +152,7 @@ export function createSignupConfirmHandler(opts = {}) {
151
152
  id: provisioned.userId,
152
153
  tenantId: provisioned.tenantId,
153
154
  roles: [...INITIAL_SIGNUP_ROLES],
155
+ ...registrationLocale,
154
156
  };
155
157
  // Claim BEFORE spending the signup token or the binding below and
156
158
  // before `committed` flips: if the claim throws, the `finally` still