@cosmicdrift/kumiko-samples 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 (186) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +18 -2
  3. package/packages/bundled-features/src/agent-tools/agent-doc-lint.ts +66 -0
  4. package/packages/bundled-features/src/agent-tools/agent-manifest.ts +26 -7
  5. package/packages/bundled-features/src/agent-tools/changes.json +12 -0
  6. package/packages/bundled-features/src/agent-tools/feature.ts +13 -2
  7. package/packages/bundled-features/src/agent-tools/index.ts +6 -1
  8. package/packages/bundled-features/src/agent-tools/manifest-deny-qns.ts +25 -0
  9. package/packages/bundled-features/src/agent-tools/tool-catalog.ts +4 -1
  10. package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +1 -1
  11. package/packages/bundled-features/src/audit/changes.json +6 -0
  12. package/packages/bundled-features/src/audit/handlers/list.query.ts +20 -10
  13. package/packages/bundled-features/src/auth-email-password/changes.json +24 -0
  14. package/packages/bundled-features/src/auth-email-password/handlers/change-password.write.ts +1 -0
  15. package/packages/bundled-features/src/auth-email-password/handlers/invite-accept-with-login.write.ts +3 -2
  16. package/packages/bundled-features/src/auth-email-password/handlers/invite-accept.write.ts +3 -2
  17. package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +21 -0
  18. package/packages/bundled-features/src/auth-email-password/handlers/invite-signup-complete.write.ts +4 -1
  19. package/packages/bundled-features/src/auth-email-password/handlers/login.write.ts +1 -0
  20. package/packages/bundled-features/src/auth-email-password/handlers/reset-password.write.ts +1 -0
  21. package/packages/bundled-features/src/auth-email-password/handlers/signup-confirm.write.ts +7 -3
  22. package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +53 -96
  23. package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +1 -0
  24. package/packages/bundled-features/src/auth-email-password/handlers/verify-email.write.ts +1 -0
  25. package/packages/bundled-features/src/auth-email-password/index.ts +12 -0
  26. package/packages/bundled-features/src/auth-email-password/seeding.ts +38 -7
  27. package/packages/bundled-features/src/auth-email-password/signup-activation.ts +83 -0
  28. package/packages/bundled-features/src/auth-email-password/signup-token-store.ts +9 -3
  29. package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +7 -7
  30. package/packages/bundled-features/src/auth-email-password/web/auth-redirect.ts +15 -0
  31. package/packages/bundled-features/src/auth-email-password/web/login-screen.tsx +2 -2
  32. package/packages/bundled-features/src/auth-email-password/web/user-menu.tsx +1 -1
  33. package/packages/bundled-features/src/auth-mfa/changes.json +7 -0
  34. package/packages/bundled-features/src/auth-mfa/feature.ts +5 -0
  35. package/packages/bundled-features/src/auth-mfa/handlers/enable-confirm-preauth.write.ts +1 -0
  36. package/packages/bundled-features/src/auth-mfa/handlers/verify.write.ts +1 -0
  37. package/packages/bundled-features/src/auth-mfa/screens.ts +2 -2
  38. package/packages/bundled-features/src/auth-mfa/web/mfa-recovery-codes-reveal.tsx +6 -8
  39. package/packages/bundled-features/src/auth-mfa/web/mfa-setup-preauth-screen.tsx +18 -21
  40. package/packages/bundled-features/src/auth-mfa/web/mfa-verify-screen.tsx +2 -2
  41. package/packages/bundled-features/src/billing-foundation/billing-info-query.ts +7 -8
  42. package/packages/bundled-features/src/billing-foundation/changes.json +6 -0
  43. package/packages/bundled-features/src/billing-foundation/checkout-core.ts +14 -2
  44. package/packages/bundled-features/src/billing-foundation/handlers/start-plan-checkout.write.ts +7 -1
  45. package/packages/bundled-features/src/cap-overview/access.ts +2 -9
  46. package/packages/bundled-features/src/cap-overview/handlers/tenant-caps-list.query.ts +5 -2
  47. package/packages/bundled-features/src/cap-overview/handlers/tenant-options.query.ts +6 -9
  48. package/packages/bundled-features/src/channel-in-app/changes.json +8 -1
  49. package/packages/bundled-features/src/channel-in-app/in-app-channel.ts +1 -1
  50. package/packages/bundled-features/src/channel-in-app/tables.ts +18 -12
  51. package/packages/bundled-features/src/compliance-profiles-ops/handlers/tenants-missing-profile.query.ts +5 -4
  52. package/packages/bundled-features/src/config/handlers/values.query.ts +7 -2
  53. package/packages/bundled-features/src/crypto-shredding/changes.json +12 -0
  54. package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +154 -69
  55. package/packages/bundled-features/src/delivery/changes.json +6 -0
  56. package/packages/bundled-features/src/delivery/db/queries/address-opt-outs.ts +18 -12
  57. package/packages/bundled-features/src/delivery/delivery-service.ts +103 -16
  58. package/packages/bundled-features/src/delivery/public-names.ts +1 -1
  59. package/packages/bundled-features/src/delivery/unsubscribe.ts +81 -13
  60. package/packages/bundled-features/src/feature-toggles/feature.ts +2 -3
  61. package/packages/bundled-features/src/feature-toggles/toggle-runtime.ts +34 -5
  62. package/packages/bundled-features/src/feature-toggles/toggle-sync-signal.ts +31 -8
  63. package/packages/bundled-features/src/file-derivatives/handlers/public-variant.query.ts +10 -1
  64. package/packages/bundled-features/src/files/changes.json +24 -0
  65. package/packages/bundled-features/src/files-tenant-data/changes.json +5 -0
  66. package/packages/bundled-features/src/files-tenant-data/handlers/sweep-orphaned-derivatives.job.ts +4 -0
  67. package/packages/bundled-features/src/files-tenant-data/hooks.ts +75 -25
  68. package/packages/bundled-features/src/folders/changes.json +6 -0
  69. package/packages/bundled-features/src/folders/feature.ts +2 -12
  70. package/packages/bundled-features/src/folders/handlers/delete-folder.write.ts +41 -9
  71. package/packages/bundled-features/src/form-draft/changes.json +7 -1
  72. package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +4 -2
  73. package/packages/bundled-features/src/form-draft/handlers/cleanup.job.ts +18 -6
  74. package/packages/bundled-features/src/foundation-shared/index.ts +1 -0
  75. package/packages/bundled-features/src/foundation-shared/mail-host-policy.ts +16 -4
  76. package/packages/bundled-features/src/inbound-provider-imap/changes.json +6 -0
  77. package/packages/bundled-features/src/inbound-provider-imap/feature.ts +2 -0
  78. package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +3 -4
  79. package/packages/bundled-features/src/jobs/changes.json +30 -0
  80. package/packages/bundled-features/src/jobs/constants.ts +1 -0
  81. package/packages/bundled-features/src/jobs/db/queries/retention.ts +12 -0
  82. package/packages/bundled-features/src/jobs/db/queries/stale-run-sweep.ts +26 -2
  83. package/packages/bundled-features/src/jobs/feature.ts +7 -1
  84. package/packages/bundled-features/src/jobs/handlers/list.query.ts +45 -12
  85. package/packages/bundled-features/src/jobs/handlers/tenant-failures.query.ts +1 -1
  86. package/packages/bundled-features/src/jobs/handlers/tenant-runs.query.ts +94 -0
  87. package/packages/bundled-features/src/jobs/i18n.ts +1 -0
  88. package/packages/bundled-features/src/jobs/index.ts +2 -0
  89. package/packages/bundled-features/src/jobs/job-run-logger.ts +177 -10
  90. package/packages/bundled-features/src/jobs/tenant-job-failure-table.ts +27 -8
  91. package/packages/bundled-features/src/jobs/tenant-job-run-table.ts +55 -0
  92. package/packages/bundled-features/src/ledger/changes.json +23 -0
  93. package/packages/bundled-features/src/ledger/entity.ts +17 -5
  94. package/packages/bundled-features/src/ledger/feature.ts +3 -0
  95. package/packages/bundled-features/src/ledger/handlers/confirm-schedule-period.write.ts +15 -3
  96. package/packages/bundled-features/src/ledger/handlers/create-transaction.write.ts +1 -0
  97. package/packages/bundled-features/src/ledger/handlers/reverse-transaction.write.ts +1 -0
  98. package/packages/bundled-features/src/ledger/schedule-subject-pair.ts +27 -0
  99. package/packages/bundled-features/src/ledger/schemas.ts +8 -2
  100. package/packages/bundled-features/src/mail-transport-smtp/feature.ts +4 -15
  101. package/packages/bundled-features/src/managed-pages/changes.json +6 -0
  102. package/packages/bundled-features/src/managed-pages/feature.ts +24 -11
  103. package/packages/bundled-features/src/notes-history/changes.json +13 -0
  104. package/packages/bundled-features/src/notes-history/entity.ts +4 -0
  105. package/packages/bundled-features/src/notes-history/feature.ts +2 -12
  106. package/packages/bundled-features/src/personal-access-tokens/handlers/create.write.ts +1 -0
  107. package/packages/bundled-features/src/personal-access-tokens/screens.ts +24 -3
  108. package/packages/bundled-features/src/seo/changes.json +5 -0
  109. package/packages/bundled-features/src/seo/feature.ts +23 -7
  110. package/packages/bundled-features/src/sessions/feature.ts +21 -8
  111. package/packages/bundled-features/src/sessions/handlers/detail.query.ts +34 -33
  112. package/packages/bundled-features/src/sessions/handlers/list.query.ts +68 -65
  113. package/packages/bundled-features/src/sessions/screens.ts +10 -8
  114. package/packages/bundled-features/src/shared/assert-no-where-rule-in-ownership-write.ts +25 -0
  115. package/packages/bundled-features/src/shared/encrypt-for-direct-write.ts +15 -8
  116. package/packages/bundled-features/src/shared/index.ts +4 -2
  117. package/packages/bundled-features/src/shared/parent-visibility.ts +31 -1
  118. package/packages/bundled-features/src/shared/rate-limited-text-response.ts +11 -0
  119. package/packages/bundled-features/src/shared/row-bound-grant.ts +18 -2
  120. package/packages/bundled-features/src/shared/session-locale-field.ts +7 -0
  121. package/packages/bundled-features/src/tags/feature.ts +2 -12
  122. package/packages/bundled-features/src/template-resolver/api.ts +3 -3
  123. package/packages/bundled-features/src/template-resolver/changes.json +5 -0
  124. package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +2 -2
  125. package/packages/bundled-features/src/tenant/changes.json +19 -0
  126. package/packages/bundled-features/src/tenant/decrypt-tenant-names.ts +17 -0
  127. package/packages/bundled-features/src/tenant/feature.ts +8 -1
  128. package/packages/bundled-features/src/tenant/handlers/me.query.ts +4 -1
  129. package/packages/bundled-features/src/tenant/handlers/memberships.query.ts +7 -3
  130. package/packages/bundled-features/src/tenant/handlers/tenant-directory.query.ts +11 -5
  131. package/packages/bundled-features/src/tenant/handlers/toggle-enabled.write.ts +2 -0
  132. package/packages/bundled-features/src/tenant/handlers/update-member-roles.write.ts +1 -0
  133. package/packages/bundled-features/src/tenant/index.ts +1 -0
  134. package/packages/bundled-features/src/tenant/invitation-table.ts +6 -0
  135. package/packages/bundled-features/src/tenant/invited-membership.ts +45 -12
  136. package/packages/bundled-features/src/tenant/schema/tenant.ts +4 -6
  137. package/packages/bundled-features/src/tenant/screens.ts +1 -1
  138. package/packages/bundled-features/src/tenant-handover/changes.json +12 -0
  139. package/packages/bundled-features/src/tenant-handover/move-entity-graph.ts +12 -19
  140. package/packages/bundled-features/src/tenant-handover/transfer-graph.ts +14 -96
  141. package/packages/bundled-features/src/tenant-lifecycle/boot-checks.ts +16 -0
  142. package/packages/bundled-features/src/tenant-lifecycle/changes.json +22 -0
  143. package/packages/bundled-features/src/tenant-lifecycle/constants.ts +8 -0
  144. package/packages/bundled-features/src/tenant-lifecycle/events.ts +6 -0
  145. package/packages/bundled-features/src/tenant-lifecycle/feature.ts +14 -1
  146. package/packages/bundled-features/src/tenant-lifecycle/run-tenant-destroy.ts +105 -29
  147. package/packages/bundled-features/src/tenant-lifecycle/stages.ts +64 -10
  148. package/packages/bundled-features/src/tenant-lifecycle/testing.ts +58 -0
  149. package/packages/bundled-features/src/tier-engine/changes.json +5 -0
  150. package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +1 -0
  151. package/packages/bundled-features/src/user/changes.json +15 -0
  152. package/packages/bundled-features/src/user/handlers/detail.query.ts +1 -0
  153. package/packages/bundled-features/src/user/handlers/list.query.ts +8 -2
  154. package/packages/bundled-features/src/user/handlers/update.write.ts +1 -0
  155. package/packages/bundled-features/src/user/seeding.ts +34 -0
  156. package/packages/bundled-features/src/user-data-rights/boot-checks.ts +7 -0
  157. package/packages/bundled-features/src/user-data-rights/changes.json +6 -0
  158. package/packages/bundled-features/src/user-data-rights/deletion-token.ts +1 -1
  159. package/packages/bundled-features/src/user-data-rights/handlers/confirm-deletion-by-token.write.ts +2 -2
  160. package/packages/bundled-features/src/user-data-rights/handlers/request-deletion.write.ts +1 -0
  161. package/packages/bundled-features/src/user-data-rights/index.ts +1 -0
  162. package/packages/bundled-features/src/user-data-rights/schema/download-attempt.ts +17 -9
  163. package/packages/bundled-features/src/user-profile/handlers/change-email.write.ts +8 -2
  164. package/packages/bundled-features/src/workflow-runner/changes.json +5 -0
  165. package/packages/bundled-features/src/workflow-runner/event-trigger.ts +80 -14
  166. package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +11 -1
  167. package/packages/bundled-features/src/workflow-runner/runner.ts +8 -2
  168. package/samples/apps/showcase/src/features/items/feature.ts +2 -1
  169. package/samples/apps/showcase/src/features/items/schema/item.ts +0 -1
  170. package/samples/apps/styleguide/src/features/widgets/demo-locale.ts +47 -0
  171. package/samples/apps/styleguide/src/features/widgets/feature.ts +131 -15
  172. package/samples/apps/styleguide/src/features/widgets/i18n.ts +40 -0
  173. package/samples/apps/styleguide/src/features/widgets/web/DashboardFilterEcho.tsx +5 -4
  174. package/samples/apps/styleguide/src/features/widgets/web/Widgets.tsx +4 -0
  175. package/samples/apps/ui-walkthrough/package.json +3 -0
  176. package/samples/apps/use-all-bundled/README.md +1 -2
  177. package/samples/apps/use-all-bundled/feature-manifest.json +1 -1
  178. package/samples/apps/user-data-rights-demo/README.md +1 -3
  179. package/samples/recipes/legal-pages/README.md +3 -4
  180. package/samples/recipes/managed-pages/README.md +2 -2
  181. package/samples/recipes/public-share-token/README.md +4 -5
  182. package/samples/recipes/public-share-token/src/feature.ts +1 -1
  183. package/samples/recipes/security-baseline/README.md +6 -0
  184. package/samples/recipes/seed-migration/README.md +0 -2
  185. package/samples/recipes/tenant-settings/README.md +1 -1
  186. package/packages/bundled-features/src/shared/has-where-rule.ts +0 -11
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-samples",
3
- "version": "0.352.0",
3
+ "version": "0.354.0",
4
4
  "description": "Source trees of the Kumiko sample recipes, sample apps and bundled features in repo layout, for tooling such as few-shot corpus builds.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-bundled-features",
3
- "version": "0.352.0",
3
+ "version": "0.354.0",
4
4
  "description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -17,7 +17,8 @@
17
17
  "kumiko": {
18
18
  "runtime": "runtime",
19
19
  "bunOnlyExports": [
20
- "./template-resolver/testing"
20
+ "./template-resolver/testing",
21
+ "./tenant-lifecycle/testing"
21
22
  ]
22
23
  },
23
24
  "exports": {
@@ -81,12 +82,14 @@
81
82
  "./files-tenant-data": "./src/files-tenant-data/index.ts",
82
83
  "./user-data-rights": "./src/user-data-rights/index.ts",
83
84
  "./user-data-rights/web": "./src/user-data-rights/web/index.ts",
85
+ "./user-data-rights/email-templates": "./src/user-data-rights/email-templates.ts",
84
86
  "./user-data-rights-defaults": "./src/user-data-rights-defaults/index.ts",
85
87
  "./tenant": "./src/tenant/index.ts",
86
88
  "./tenant/constants": "./src/tenant/constants.ts",
87
89
  "./tenant/web": "./src/tenant/web/index.ts",
88
90
  "./tenant/seeding": "./src/tenant/seeding.ts",
89
91
  "./tenant-lifecycle": "./src/tenant-lifecycle/index.ts",
92
+ "./tenant-lifecycle/testing": "./src/tenant-lifecycle/testing.ts",
90
93
  "./tenant-handover": "./src/tenant-handover/index.ts",
91
94
  "./tenant/testing": "./src/tenant/testing.ts",
92
95
  "./user": "./src/user/index.ts",
@@ -96,6 +99,7 @@
96
99
  "./user-profile/web": "./src/user-profile/web/index.ts",
97
100
  "./auth-email-password": "./src/auth-email-password/index.ts",
98
101
  "./auth-email-password/constants": "./src/auth-email-password/constants.ts",
102
+ "./auth-email-password/email-templates": "./src/auth-email-password/email-templates.ts",
99
103
  "./auth-email-password/seeding": "./src/auth-email-password/seeding.ts",
100
104
  "./auth-email-password/testing": "./src/auth-email-password/testing.ts",
101
105
  "./auth-email-password/web": "./src/auth-email-password/web/index.ts",
@@ -411,6 +415,10 @@
411
415
  "types": "./dist/user-data-rights/index.d.ts",
412
416
  "default": "./dist/user-data-rights/index.js"
413
417
  },
418
+ "./user-data-rights/email-templates": {
419
+ "types": "./dist/user-data-rights/email-templates.d.ts",
420
+ "default": "./dist/user-data-rights/email-templates.js"
421
+ },
414
422
  "./user-data-rights/web": {
415
423
  "types": "./dist/user-data-rights/web/index.d.ts",
416
424
  "default": "./dist/user-data-rights/web/index.js"
@@ -439,6 +447,10 @@
439
447
  "types": "./dist/tenant-lifecycle/index.d.ts",
440
448
  "default": "./dist/tenant-lifecycle/index.js"
441
449
  },
450
+ "./tenant-lifecycle/testing": {
451
+ "types": "./dist/tenant-lifecycle/testing.d.ts",
452
+ "default": "./dist/tenant-lifecycle/testing.js"
453
+ },
442
454
  "./tenant-handover": {
443
455
  "types": "./dist/tenant-handover/index.d.ts",
444
456
  "default": "./dist/tenant-handover/index.js"
@@ -471,6 +483,10 @@
471
483
  "types": "./dist/auth-email-password/index.d.ts",
472
484
  "default": "./dist/auth-email-password/index.js"
473
485
  },
486
+ "./auth-email-password/email-templates": {
487
+ "types": "./dist/auth-email-password/email-templates.d.ts",
488
+ "default": "./dist/auth-email-password/email-templates.js"
489
+ },
474
490
  "./auth-email-password/constants": {
475
491
  "types": "./dist/auth-email-password/constants.d.ts",
476
492
  "default": "./dist/auth-email-password/constants.js"
@@ -4,6 +4,8 @@ import type {
4
4
  WriteHandlerDef,
5
5
  } from "@cosmicdrift/kumiko-framework/engine";
6
6
  import {
7
+ handlerFieldLabelKey,
8
+ handlerTitleKey,
7
9
  isAgentVisibleScreen,
8
10
  QnTypes,
9
11
  qn,
@@ -11,12 +13,14 @@ import {
11
13
  toKebab,
12
14
  } from "@cosmicdrift/kumiko-framework/engine";
13
15
  import * as z from "zod";
16
+ import { systemOnlyPropertyNames } from "./agent-manifest.js";
14
17
 
15
18
  export const AgentDocGapKinds = {
16
19
  handlerSchemaNotExpressible: "handler-schema-not-expressible",
17
20
  handlerWithoutDescription: "handler-without-description",
18
21
  customScreenWithoutDescription: "custom-screen-without-description",
19
22
  exposedEntityWithoutDescription: "exposed-entity-without-description",
23
+ handlerWithoutTranslation: "handler-without-translation",
20
24
  } as const;
21
25
 
22
26
  export type AgentDocGapKind = (typeof AgentDocGapKinds)[keyof typeof AgentDocGapKinds];
@@ -144,6 +148,58 @@ function exposedEntityDocGaps(feature: FeatureDefinition): readonly AgentDocGap[
144
148
  return gaps;
145
149
  }
146
150
 
151
+ function actionFormHandlerQns(features: readonly FeatureDefinition[]): ReadonlySet<string> {
152
+ const handlerQns = new Set<string>();
153
+ for (const feature of features) {
154
+ for (const screen of Object.values(feature.screens ?? {})) {
155
+ if (screen.type === "actionForm") handlerQns.add(screen.handler);
156
+ }
157
+ }
158
+ return handlerQns;
159
+ }
160
+
161
+ function agentVisibleInputFieldNames(schema: z.ZodType): readonly string[] {
162
+ if (!(schema instanceof z.ZodObject)) return [];
163
+ const fieldNames = Object.keys(schema.shape);
164
+ try {
165
+ const systemOnly = new Set(systemOnlyPropertyNames(z.toJSONSchema(schema, { io: "input" })));
166
+ return fieldNames.filter((field) => !systemOnly.has(field));
167
+ } catch {
168
+ // skip: a schema without JSON Schema form is reported as its own gap kind
169
+ return fieldNames;
170
+ }
171
+ }
172
+
173
+ function translationGapsOfFeature(
174
+ feature: FeatureDefinition,
175
+ coveredHandlerQns: ReadonlySet<string>,
176
+ ): readonly AgentDocGap[] {
177
+ const gaps: AgentDocGap[] = [];
178
+ // The client resolves translation keys verbatim (no feature prefix), so only
179
+ // fully spelled-out keys count.
180
+ const translated = new Set(Object.keys(feature.translations ?? {}));
181
+ for (const [name, def] of Object.entries(feature.writeHandlers ?? {})) {
182
+ if (!resolveAgentExposure(def, "write").expose) continue;
183
+ if (feature.handlerEntityMappings?.[name] !== undefined) continue;
184
+ const handlerQn = qn(toKebab(feature.name), QnTypes.write, toKebab(name));
185
+ if (coveredHandlerQns.has(handlerQn)) continue;
186
+ const missing = [
187
+ handlerTitleKey(handlerQn),
188
+ ...agentVisibleInputFieldNames(def.schema).map((field) =>
189
+ handlerFieldLabelKey(handlerQn, field),
190
+ ),
191
+ ].filter((key) => !translated.has(key));
192
+ if (missing.length === 0) continue;
193
+ gaps.push({
194
+ qn: handlerQn,
195
+ feature: feature.name,
196
+ kind: AgentDocGapKinds.handlerWithoutTranslation,
197
+ 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(", ")}.`,
198
+ });
199
+ }
200
+ return gaps;
201
+ }
202
+
147
203
  export function findAgentDocGaps(features: readonly FeatureDefinition[]): readonly AgentDocGap[] {
148
204
  const gaps: AgentDocGap[] = [];
149
205
  for (const feature of features) {
@@ -161,6 +217,16 @@ export function findAgentDocGaps(features: readonly FeatureDefinition[]): readon
161
217
  return [...gaps].sort((a, b) => compareByCodePoint(a.qn, b.qn));
162
218
  }
163
219
 
220
+ // Kept out of findAgentDocGaps on purpose: the published agent-manifest guard
221
+ // fails on every gap that function returns, and these are advisory.
222
+ export function findHandlerTranslationGaps(
223
+ features: readonly FeatureDefinition[],
224
+ ): readonly AgentDocGap[] {
225
+ const coveredHandlerQns = actionFormHandlerQns(features);
226
+ const gaps = features.flatMap((feature) => translationGapsOfFeature(feature, coveredHandlerQns));
227
+ return [...gaps].sort((a, b) => compareByCodePoint(a.qn, b.qn));
228
+ }
229
+
164
230
  export function formatAgentDocGap(gap: AgentDocGap): string {
165
231
  return `${gap.qn} — ${gap.message}`;
166
232
  }
@@ -19,6 +19,7 @@ import {
19
19
  } from "@cosmicdrift/kumiko-framework/engine";
20
20
  import { translationValueOtherText } from "@cosmicdrift/kumiko-framework/ui-types";
21
21
  import * as z from "zod";
22
+ import { recordManifestDenyQns } from "./manifest-deny-qns.js";
22
23
  import type {
23
24
  AgentManifest,
24
25
  AgentManifestEntity,
@@ -193,16 +194,22 @@ function buildFeatures(
193
194
 
194
195
  // Agent dispatch never runs as a system identity, so properties the engine only
195
196
  // honors for system callers would be silently dropped — hide them from the tool.
196
- function withoutSystemOnlyProperties(
197
- jsonSchema: Record<string, unknown>,
198
- ): Readonly<Record<string, unknown>> {
197
+ export function systemOnlyPropertyNames(jsonSchema: Record<string, unknown>): readonly string[] {
199
198
  const properties = jsonSchema["properties"];
200
- if (typeof properties !== "object" || properties === null) return jsonSchema;
201
- const hidden = Object.entries(properties)
199
+ if (typeof properties !== "object" || properties === null) return [];
200
+ return Object.entries(properties)
202
201
  .filter(
203
202
  ([, property]) => (property as Record<string, unknown>)[SYSTEM_ONLY_JSON_SCHEMA_KEY] === true,
204
203
  )
205
204
  .map(([name]) => name);
205
+ }
206
+
207
+ function withoutSystemOnlyProperties(
208
+ jsonSchema: Record<string, unknown>,
209
+ ): Readonly<Record<string, unknown>> {
210
+ const properties = jsonSchema["properties"];
211
+ if (typeof properties !== "object" || properties === null) return jsonSchema;
212
+ const hidden = systemOnlyPropertyNames(jsonSchema);
206
213
  if (hidden.length === 0) return jsonSchema;
207
214
  const required = jsonSchema["required"];
208
215
  return {
@@ -334,6 +341,16 @@ function detailIdParam(screen: ScreenDefinition): string {
334
341
  return screen.type === "projectionDetail" ? (screen.idParam ?? "id") : "id";
335
342
  }
336
343
 
344
+ function screenTitles(
345
+ translations: TranslationKeys,
346
+ screenId: string,
347
+ shortId: string,
348
+ ): AgentManifestScreen["titles"] {
349
+ return entryAsLabels(
350
+ translations[`${screenId}.title`] ?? translations[`screen:${shortId}.title`] ?? {},
351
+ );
352
+ }
353
+
337
354
  function buildScreens(
338
355
  screens: ReadonlyMap<string, ScreenDefinition>,
339
356
  navs: ReadonlyMap<string, NavDefinition>,
@@ -372,7 +389,7 @@ function buildScreens(
372
389
  result.push({
373
390
  id: screen.id,
374
391
  type: screen.type,
375
- titles: labelsForSuffix(translations, `screen:${shortId}.title`),
392
+ titles: screenTitles(translations, screen.id, shortId),
376
393
  // `screen.description` may be a raw i18n key (`registry.getAllTranslations()`
377
394
  // double-prefixes it, see build-app-schema.ts:77-81) — match on suffix like
378
395
  // `labelsForSuffix` does for entity/field labels, else fall back to the
@@ -444,7 +461,7 @@ export function buildAgentManifest(
444
461
  );
445
462
  const workspaces = buildWorkspaces(workspaceMap, translations, roles);
446
463
 
447
- return {
464
+ const manifest: AgentManifest = {
448
465
  builtForRoles: [...roles],
449
466
  features: sortedByKey(features, (f) => f.name),
450
467
  entities: sortedByKey(entities, (e) => e.name),
@@ -457,4 +474,6 @@ export function buildAgentManifest(
457
474
  ...(options.currency && { currency: options.currency }),
458
475
  },
459
476
  };
477
+ recordManifestDenyQns(manifest, new Set(options.denyQns ?? []));
478
+ return manifest;
460
479
  }
@@ -1,4 +1,16 @@
1
1
  [
2
+ {
3
+ "version": "0.353.0",
4
+ "type": "fix",
5
+ "title": "buildToolCatalog throws when the manifest was built with denyQns the catalog options lack",
6
+ "migration": "Pass the same denyQns list to buildAgentManifest and buildToolCatalog."
7
+ },
8
+ {
9
+ "version": "0.353.0",
10
+ "type": "fix",
11
+ "title": "Agent manifest screen titles resolve per feature instead of by mount order",
12
+ "detail": "Agent manifest screen titles no longer depend on feature mount order\nTwo features with a screen of the same short id used to get the title of whichever feature was mounted first. The manifest now looks up the screen's own feature-prefixed title key first and falls back to the unprefixed key."
13
+ },
2
14
  {
3
15
  "version": "0.338.0",
4
16
  "type": "improvement",
@@ -1,5 +1,9 @@
1
1
  import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
2
- import { findAgentDocGaps, formatAgentDocGap } from "./agent-doc-lint.js";
2
+ import {
3
+ findAgentDocGaps,
4
+ findHandlerTranslationGaps,
5
+ formatAgentDocGap,
6
+ } from "./agent-doc-lint.js";
3
7
 
4
8
  export const AGENT_TOOLS_FEATURE_NAME = "agent-tools";
5
9
 
@@ -19,8 +23,15 @@ export function createAgentToolsFeature(): FeatureDefinition {
19
23
  // would turn every incrementally-documented app into an outage.
20
24
  r.bootCheck(({ features }) => {
21
25
  const gaps = findAgentDocGaps(features);
26
+ const warningCount = findHandlerTranslationGaps(features).length;
27
+ if (warningCount > 0) {
28
+ // biome-ignore lint/suspicious/noConsole: operator-visibility at boot for agent-doc gaps
29
+ console.warn(
30
+ `[agent-tools] ${warningCount} agent-exposed write handler(s) lack title/field-label translations — run \`kumiko agent lint\` for details.`,
31
+ );
32
+ }
22
33
  if (gaps.length === 0) {
23
- // skip: no documentation gaps, nothing to warn about
34
+ // skip: no blocking documentation gaps, nothing more to warn about
24
35
  return;
25
36
  }
26
37
  // biome-ignore lint/suspicious/noConsole: operator-visibility at boot for agent-doc gaps
@@ -1,5 +1,10 @@
1
1
  export type { AgentDocGap, AgentDocGapKind } from "./agent-doc-lint.js";
2
- export { AgentDocGapKinds, findAgentDocGaps, formatAgentDocGap } from "./agent-doc-lint.js";
2
+ export {
3
+ AgentDocGapKinds,
4
+ findAgentDocGaps,
5
+ findHandlerTranslationGaps,
6
+ formatAgentDocGap,
7
+ } from "./agent-doc-lint.js";
3
8
  export { buildAgentManifest } from "./agent-manifest.js";
4
9
  export { AGENT_TOOLS_FEATURE_NAME, createAgentToolsFeature } from "./feature.js";
5
10
  export { buildToolCatalog, OPEN_FORM_TOOL_NAME, toolNameForQn } from "./tool-catalog.js";
@@ -0,0 +1,25 @@
1
+ import type { AgentManifest } from "./types.js";
2
+
3
+ // The deny list is deliberately not a manifest field (the manifest is prompt
4
+ // payload), so the catalog builder learns it through this side channel to
5
+ // detect a manifest cut that the catalog would silently not mirror.
6
+ const denyQnsByManifest = new WeakMap<AgentManifest, ReadonlySet<string>>();
7
+
8
+ export function recordManifestDenyQns(manifest: AgentManifest, denyQns: ReadonlySet<string>): void {
9
+ denyQnsByManifest.set(manifest, denyQns);
10
+ }
11
+
12
+ export function assertCatalogMirrorsManifestDenyQns(
13
+ manifest: AgentManifest,
14
+ catalogDenyQns: ReadonlySet<string>,
15
+ ): void {
16
+ const manifestDenyQns = denyQnsByManifest.get(manifest);
17
+ // skip: manifest has no registered deny list, nothing to mirror
18
+ if (!manifestDenyQns) return;
19
+ const missing = [...manifestDenyQns].filter((qn) => !catalogDenyQns.has(qn));
20
+ if (missing.length > 0) {
21
+ throw new Error(
22
+ `agent denyQns passed to buildAgentManifest but not to buildToolCatalog: ${missing.join(", ")} — the catalog would still expose their entity CRUD tools`,
23
+ );
24
+ }
25
+ }
@@ -6,6 +6,7 @@ import type {
6
6
  } from "@cosmicdrift/kumiko-framework/engine";
7
7
  import { hasAccess, normalizeAccessEntry } from "@cosmicdrift/kumiko-framework/engine";
8
8
  import { FILTER_OPS, isRecord } from "./filter-ops.js";
9
+ import { assertCatalogMirrorsManifestDenyQns } from "./manifest-deny-qns.js";
9
10
  import type {
10
11
  AgentManifest,
11
12
  AgentManifestEntity,
@@ -628,7 +629,8 @@ function addClientTools(manifest: AgentManifest, mode: AgentToolMode, sink: Cata
628
629
  * to drop an entity CRUD tool, which no `agent.expose` on the manifest side can reach. It is
629
630
  * NOT read off the manifest on purpose — the manifest is prompt payload, and a list of the
630
631
  * handlers the model may not call has no business travelling to the provider. Pass the same
631
- * list to `buildAgentManifest` so the manifest stops describing what the catalog withholds. */
632
+ * list to `buildAgentManifest` so the manifest stops describing what the catalog withholds;
633
+ * a manifest built with `denyQns` that the catalog lacks throws. */
632
634
  export function buildToolCatalog(
633
635
  registry: RegistrySearchView,
634
636
  manifest: AgentManifest,
@@ -639,6 +641,7 @@ export function buildToolCatalog(
639
641
  const locale = manifest.tenantSettings.locale;
640
642
  const entityByName = new Map(manifest.entities.map((entity) => [entity.name, entity]));
641
643
  const denyQns = new Set(options.denyQns ?? []);
644
+ assertCatalogMirrorsManifestDenyQns(manifest, denyQns);
642
645
 
643
646
  addRegistrySearchTools(registry, roleFilter, denyQns, sink);
644
647
  const { detailQnByEntity, listQnByEntity, entityListDetailQns } = collectEntityHandlerQns(
@@ -1,5 +1,5 @@
1
- import { runAsDirectCallEntry } from "@cosmicdrift/kumiko-framework/api";
2
1
  import type { SessionUser, WriteResult } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { runAsDirectCallEntry } from "@cosmicdrift/kumiko-framework/internal/request-context";
3
3
  import { FILTER_OPS, isRecord } from "./filter-ops.js";
4
4
  import type { ToolDispatchDescriptor } from "./types.js";
5
5
 
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.353.0",
4
+ "type": "fix",
5
+ "title": "Audit list paging no longer repeats rows with ascending sort",
6
+ "detail": "Audit list pages forward without repeats when sorted ascending\n`audit:query:list` with `sortDirection: \"asc\"` returned the same rows again on the next page because the cursor always meant \"older than\". The cursor now follows the sort direction, and ties are broken by event id. Sorting by `type` returns a single page without `nextCursor`, since an event id cannot resume a type-ordered listing."
7
+ },
2
8
  {
3
9
  "version": "0.346.0",
4
10
  "type": "improvement",
@@ -52,6 +52,7 @@ function buildAuditWhere(
52
52
  cursor?: string;
53
53
  before?: string;
54
54
  },
55
+ direction: "asc" | "desc",
55
56
  ): WhereObject {
56
57
  const where: WhereObject = { tenantId };
57
58
  if (p.aggregateType) where["aggregateType"] = p.aggregateType;
@@ -62,7 +63,9 @@ function buildAuditWhere(
62
63
  const range = buildDateRange(p.from, p.to);
63
64
  if (range) where["createdAt"] = range;
64
65
  const cursor = p.cursor ?? p.before;
65
- if (cursor) where["id"] = { lt: BigInt(cursor) };
66
+ // The cursor is an event id, so it only walks pages in id order: ascending
67
+ // pages continue past it, descending pages stop before it.
68
+ if (cursor) where["id"] = direction === "asc" ? { gt: BigInt(cursor) } : { lt: BigInt(cursor) };
66
69
  return where;
67
70
  }
68
71
 
@@ -94,10 +97,15 @@ export const listQuery = defineQueryHandler({
94
97
  handler: async (query, ctx) => {
95
98
  const p = query.payload;
96
99
  const scopeFilter = resolveAuditScopeFilter(query.user, p.scope);
97
- const where = buildAuditWhere(scopeFilter.tenantId, {
98
- ...p,
99
- aggregateType: scopeFilter.aggregateType ?? p.aggregateType,
100
- });
100
+ const direction = p.sortDirection ?? "desc";
101
+ const where = buildAuditWhere(
102
+ scopeFilter.tenantId,
103
+ {
104
+ ...p,
105
+ aggregateType: scopeFilter.aggregateType ?? p.aggregateType,
106
+ },
107
+ direction,
108
+ );
101
109
 
102
110
  const rows = await selectMany<{
103
111
  id: bigint;
@@ -110,10 +118,11 @@ export const listQuery = defineQueryHandler({
110
118
  createdAt: unknown;
111
119
  createdBy: string;
112
120
  }>(ctx.db, eventsTable, where, {
113
- orderBy: {
114
- col: query.payload.sort === "type" ? "type" : "createdAt",
115
- direction: query.payload.sortDirection ?? "desc",
116
- },
121
+ // `id` as tie-breaker keeps the order total, so pages never overlap.
122
+ orderBy: [
123
+ { col: p.sort === "type" ? "type" : "createdAt", direction },
124
+ { col: "id", direction },
125
+ ],
117
126
  limit: p.limit,
118
127
  });
119
128
 
@@ -129,7 +138,8 @@ export const listQuery = defineQueryHandler({
129
138
  createdBy: r.createdBy,
130
139
  }));
131
140
  const last = serialised[serialised.length - 1];
132
- const nextId = serialised.length === p.limit && last ? last.id : null;
141
+ // An id cursor cannot resume a type-ordered listing, so that sort is one page.
142
+ const nextId = serialised.length === p.limit && last && p.sort !== "type" ? last.id : null;
133
143
  return {
134
144
  rows: serialised,
135
145
  nextCursor: nextId,
@@ -1,4 +1,28 @@
1
1
  [
2
+ {
3
+ "version": "0.354.0",
4
+ "type": "breaking",
5
+ "title": "Invitations pin the invitee's membership version; superseded accept no longer relies on timestamps",
6
+ "migration": "New nullable column read_tenant_invitations.membership_version: run `kumiko schema generate` and apply the migration before deploying. Invitations issued before the migration keep the timestamp comparison."
7
+ },
8
+ {
9
+ "version": "0.354.0",
10
+ "type": "fix",
11
+ "title": "Login, MFA verify and MFA preauth setup follow the server landingPath when no valid next parameter is present",
12
+ "detail": "AuthGate onSuccess receives the landing path. A validated next parameter still wins; a landing path equal to the current path is not followed, so the screen does not reload itself."
13
+ },
14
+ {
15
+ "version": "0.354.0",
16
+ "type": "fix",
17
+ "title": "Signup-confirm and invite-signup-complete store the request locale on the new user and in the session"
18
+ },
19
+ {
20
+ "version": "0.354.0",
21
+ "type": "fix",
22
+ "title": "INITIAL_SIGNUP_ROLES stays [\"TenantAdmin\", \"Admin\"] as a transition",
23
+ "detail": "New self-signup owners hold TenantAdmin plus the legacy Admin literal, because handlers that gate on the Admin literal would otherwise lock them out. Both roles show in Team, Members until those handlers gate on access.admin. This supersedes the earlier note that the owner role is only TenantAdmin.",
24
+ "migration": "Move your own handlers that gate on the Admin literal to access.admin; after that the Admin entry can be dropped from the constant."
25
+ },
2
26
  {
3
27
  "version": "0.348.0",
4
28
  "type": "fix",
@@ -22,6 +22,7 @@ export const changePasswordWrite = defineWriteHandler({
22
22
  }),
23
23
  access: { roles: access.authenticated },
24
24
  escapeHatch: {
25
+ grants: ["systemIdentity"],
25
26
  reason:
26
27
  "Reads the caller's own passwordHash (privileged-only field) via ctx.queryAs(SYSTEM, ...) " +
27
28
  "and writes the new hash via ctx.writeAs(SYSTEM, user:update) — the caller's own " +
@@ -41,7 +41,7 @@ import {
41
41
  tenantInvitationsTable,
42
42
  } from "../../tenant/invitation-table.js";
43
43
  // kumiko-lint-ignore cross-feature-import membership grant for a privileged cross-tenant add
44
- import { grantInvitedMembershipRole, invitationIssuedAt } from "../../tenant/invited-membership.js";
44
+ import { grantInvitedMembershipRole } from "../../tenant/invited-membership.js";
45
45
  // kumiko-lint-ignore cross-feature-import global roles granted on invite accept live on the user row
46
46
  import {
47
47
  grantInvitedGlobalRoles,
@@ -150,6 +150,7 @@ export function createInviteAcceptWithLoginHandler(opts: InviteAcceptWithLoginOp
150
150
  readonly role: string;
151
151
  readonly globalRoles: unknown;
152
152
  readonly version: number;
153
+ readonly membershipVersion: number | null;
153
154
  readonly insertedAt: Temporal.Instant;
154
155
  readonly modifiedAt: Temporal.Instant | null;
155
156
  };
@@ -224,7 +225,7 @@ export function createInviteAcceptWithLoginHandler(opts: InviteAcceptWithLoginOp
224
225
  userId,
225
226
  tenantId: invitationTenantId,
226
227
  role: invitationRole,
227
- invitationIssuedAt: invitationIssuedAt(invitation),
228
+ invitation,
228
229
  });
229
230
  if (!grant.isSuccess) return grant;
230
231
 
@@ -33,7 +33,7 @@ import {
33
33
  tenantInvitationsTable,
34
34
  } from "../../tenant/invitation-table.js";
35
35
  // kumiko-lint-ignore cross-feature-import membership grant for a privileged cross-tenant add (like provisionSignupAccount)
36
- import { grantInvitedMembershipRole, invitationIssuedAt } from "../../tenant/invited-membership.js";
36
+ import { grantInvitedMembershipRole } from "../../tenant/invited-membership.js";
37
37
  // kumiko-lint-ignore cross-feature-import global roles granted on invite accept live on the user row
38
38
  import {
39
39
  grantInvitedGlobalRoles,
@@ -110,6 +110,7 @@ export function createInviteAcceptHandler() {
110
110
  readonly role: string;
111
111
  readonly globalRoles: unknown;
112
112
  readonly version: number;
113
+ readonly membershipVersion: number | null;
113
114
  readonly insertedAt: Temporal.Instant;
114
115
  readonly modifiedAt: Temporal.Instant | null;
115
116
  };
@@ -154,7 +155,7 @@ export function createInviteAcceptHandler() {
154
155
  userId: event.user.id,
155
156
  tenantId: invitationTenantId,
156
157
  role: invitationRole,
157
- invitationIssuedAt: invitationIssuedAt(invitation),
158
+ invitation,
158
159
  });
159
160
  if (!grant.isSuccess) return grant;
160
161
  const { alreadyMember } = grant.data;
@@ -40,6 +40,8 @@ import {
40
40
  tenantInvitationEntity,
41
41
  tenantInvitationsTable,
42
42
  } from "../../tenant/invitation-table.js";
43
+ // kumiko-lint-ignore cross-feature-import membership version read is owned by the tenant feature
44
+ import { membershipVersionOf } from "../../tenant/invited-membership.js";
43
45
  // kumiko-lint-ignore cross-feature-import membership-role validation owned by tenant-feature
44
46
  import {
45
47
  findForbiddenMembershipRole,
@@ -49,6 +51,8 @@ import {
49
51
  } from "../../tenant/membership-roles.js";
50
52
  // kumiko-lint-ignore cross-feature-import global-role options are owned by the user feature
51
53
  import { GLOBAL_ROLE_OPTIONS } from "../../user/constants.js";
54
+ // kumiko-lint-ignore cross-feature-import invitee lookup by email reads the user row
55
+ import { userTable } from "../../user/schema/user.js";
52
56
  import { AUTH_INVITE_DEFAULT_TTL_MINUTES } from "../constants.js";
53
57
  import type { AuthMailLocale } from "../email-templates.js";
54
58
  import { renderInviteEmail } from "../email-templates.js";
@@ -144,6 +148,20 @@ function checkAssignableInviteRole(
144
148
  return undefined;
145
149
  }
146
150
 
151
+ // 0 = the invitee has no membership in the tenant yet (or no account at all).
152
+ async function currentMembershipVersion(
153
+ ctx: HandlerContext,
154
+ tenantId: string,
155
+ email: string,
156
+ ): Promise<number> {
157
+ const user = await ctx.db.global(userTable).fetchOne<{ readonly id: string }>({
158
+ email,
159
+ isDeleted: false,
160
+ });
161
+ if (!user) return 0;
162
+ return membershipVersionOf(ctx.db, tenantId, user.id);
163
+ }
164
+
147
165
  async function issueInvitation(
148
166
  opts: InviteCreateOptions,
149
167
  ttlSeconds: number,
@@ -166,6 +184,7 @@ async function issueInvitation(
166
184
  // The unique index allows one row per (tenantId, email). Whatever its
167
185
  // status, a re-invite resets it to pending with a fresh token.
168
186
  const existing = await ctx.db.fetchOne(tenantInvitationsTable, { tenantId, email });
187
+ const membershipVersion = await currentMembershipVersion(ctx, tenantId, email);
169
188
 
170
189
  let invitationId: string;
171
190
  if (existing) {
@@ -193,6 +212,7 @@ async function issueInvitation(
193
212
  globalRoles: [...request.globalRoles],
194
213
  status: INVITATION_STATUS.pending,
195
214
  invitedBy: inviter.id,
215
+ membershipVersion,
196
216
  expiresAt,
197
217
  },
198
218
  },
@@ -208,6 +228,7 @@ async function issueInvitation(
208
228
  globalRoles: [...request.globalRoles],
209
229
  status: INVITATION_STATUS.pending,
210
230
  invitedBy: inviter.id,
231
+ membershipVersion,
211
232
  expiresAt,
212
233
  },
213
234
  inviter,
@@ -32,7 +32,7 @@ import {
32
32
  } from "@cosmicdrift/kumiko-framework/engine";
33
33
  import { InternalError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
34
34
  import * as z from "zod";
35
- import { decryptStoredPii } from "../../shared/index.js";
35
+ import { decryptStoredPii, registrationLocaleFromRequest } from "../../shared/index.js";
36
36
  // kumiko-lint-ignore cross-feature-import invite-flow
37
37
  import {
38
38
  INVITATION_STATUS,
@@ -161,11 +161,13 @@ export function createInviteSignupCompleteHandler(opts: InviteSignupCompleteOpti
161
161
  // @cast-boundary db-runner — helpers use only the query API that
162
162
  // DbConnection and DbTx share.
163
163
  const dbConn = ctx.db.unsafeRaw() as DbConnection;
164
+ const registrationLocale = registrationLocaleFromRequest();
164
165
  const { id: userId } = await seedUserWithPassword(dbConn, {
165
166
  email: invitationEmail,
166
167
  password: event.payload.password,
167
168
  displayName: invitationEmail.split("@")[0] ?? invitationEmail,
168
169
  emailVerified: true,
170
+ ...registrationLocale,
169
171
  ...(invitationGlobalRoles.length > 0 && { roles: invitationGlobalRoles }),
170
172
  });
171
173
 
@@ -221,6 +223,7 @@ export function createInviteSignupCompleteHandler(opts: InviteSignupCompleteOpti
221
223
  id: userId,
222
224
  tenantId: invitationTenantId,
223
225
  roles: mergedRoles,
226
+ ...registrationLocale,
224
227
  };
225
228
 
226
229
  committed = true;
@@ -277,6 +277,7 @@ export function createLoginHandler(opts: LoginHandlerOptions = {}) {
277
277
  access: { roles: ["anonymous"] },
278
278
  rateLimit: { per: "ip+handler", limit: 20, windowSeconds: 60 },
279
279
  escapeHatch: {
280
+ grants: ["systemIdentity", "unsafeRaw"],
280
281
  reason:
281
282
  "Unauthenticated login has no caller identity yet — it looks up the user row by " +
282
283
  "email via ctx.queryAs(SYSTEM, ...) and resolves tenant membership via " +