@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
@@ -1,5 +1,6 @@
1
1
  import { UnprocessableError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
2
2
  import * as z from "zod";
3
+ import { hostRowIsGone } from "../../shared/index.js";
3
4
  import { DEFAULT_FOLDER_ACCESS } from "../constants.js";
4
5
  import { folderAssignmentExecutor, folderExecutor } from "../executor.js";
5
6
  const deleteFolderPayloadSchema = z.object({ id: z.uuid() });
@@ -10,18 +11,44 @@ const deleteFolderPayloadSchema = z.object({ id: z.uuid() });
10
11
  // see 658/1). Block server-side instead: this is the same "referential
11
12
  // integrity has no FK here" reasoning set-folder.write.ts already applies to
12
13
  // folderId on assignment-creation.
14
+ //
15
+ // Assignments whose host is gone (hard-deleted, soft-deleted or unregistered)
16
+ // are cleaned up here instead of blocking forever: clear-folder can't reach
17
+ // them (it requires a visible host). A host-delete hook would break restore,
18
+ // so the cleanup runs lazily at folder:delete.
19
+ const ASSIGNMENT_PAGE_SIZE = 200;
13
20
  export function createDeleteFolderHandler(access = DEFAULT_FOLDER_ACCESS) {
14
21
  return {
15
22
  name: "folder:delete",
16
23
  schema: deleteFolderPayloadSchema,
17
24
  access,
18
- description: "Deletes a folder from the tenant catalog and refuses while any entity is still filed in it; use it once that folder's contents have been unfiled or moved elsewhere.",
25
+ description: "Deletes a folder from the tenant catalog and refuses while any live entity is still filed in it (assignments of deleted hosts are cleaned up); use it once that folder's contents have been unfiled or moved elsewhere.",
19
26
  agent: { risk: "high" },
20
27
  handler: async (event, ctx) => {
21
28
  const payload = event.payload; // @cast-boundary engine-payload
22
- const assigned = await folderAssignmentExecutor.list({ filter: { field: "folderId", op: "eq", value: payload.id }, limit: 1 }, event.user, ctx.db);
23
- if (assigned.rows.length > 0) {
24
- return writeFailure(new UnprocessableError("folder_has_assignments", { details: { folderId: payload.id } }));
29
+ const orphanAssignmentIds = [];
30
+ let cursor;
31
+ do {
32
+ const page = await folderAssignmentExecutor.list({
33
+ filter: { field: "folderId", op: "eq", value: payload.id },
34
+ limit: ASSIGNMENT_PAGE_SIZE,
35
+ cursor,
36
+ }, event.user, ctx.db);
37
+ for (const row of page.rows) {
38
+ const gone = await hostRowIsGone(ctx.registry, String(row["entityType"]), String(row["entityId"]), ctx.db);
39
+ if (!gone) {
40
+ return writeFailure(new UnprocessableError("folder_has_assignments", {
41
+ details: { folderId: payload.id },
42
+ }));
43
+ }
44
+ orphanAssignmentIds.push(String(row["id"]));
45
+ }
46
+ cursor = page.nextCursor ?? undefined;
47
+ } while (cursor !== undefined);
48
+ for (const id of orphanAssignmentIds) {
49
+ const removed = await folderAssignmentExecutor.delete({ id }, event.user, ctx.db);
50
+ if (!removed.isSuccess)
51
+ return removed;
25
52
  }
26
53
  return folderExecutor.delete({ id: payload.id }, event.user, ctx.db);
27
54
  },
@@ -12,4 +12,4 @@ export type StaleDraftRow = {
12
12
  };
13
13
  export declare function selectStaleDraftsBatch(db: DbConnection, olderThanDays: number, batchSize: number): Promise<readonly StaleDraftRow[]>;
14
14
  /** Re-check staleness after the select→release window so a freshly saved draft is not deleted. */
15
- export declare function isDraftStillStale(db: DbConnection, id: string, olderThanDays: number): Promise<boolean>;
15
+ export declare function isDraftStillStale(db: DbConnection, tenantId: TenantId, id: string, olderThanDays: number): Promise<boolean>;
@@ -26,10 +26,11 @@ export async function selectStaleDraftsBatch(db, olderThanDays, batchSize) {
26
26
  }));
27
27
  }
28
28
  /** Re-check staleness after the select→release window so a freshly saved draft is not deleted. */
29
- export async function isDraftStillStale(db, id, olderThanDays) {
29
+ export async function isDraftStillStale(db, tenantId, id, olderThanDays) {
30
30
  const rows = (await unsafeReadRetrying(db, `SELECT 1 FROM "read_form_drafts"
31
31
  WHERE "id" = $1
32
- AND COALESCE("modified_at", "inserted_at") < now() - ($2::int * interval '1 day')
33
- LIMIT 1`, [id, olderThanDays]));
32
+ AND "tenant_id" = $2
33
+ AND COALESCE("modified_at", "inserted_at") < now() - ($3::int * interval '1 day')
34
+ LIMIT 1`, [id, tenantId, olderThanDays]));
34
35
  return rows.length > 0;
35
36
  }
@@ -3,6 +3,7 @@ export declare const formDraftEntity: import("@cosmicdrift/kumiko-types/fields")
3
3
  readonly type: "text";
4
4
  readonly description?: string;
5
5
  readonly maxLength?: number;
6
+ readonly minLength?: number;
6
7
  readonly required?: boolean;
7
8
  readonly searchable?: boolean;
8
9
  readonly sortable?: boolean;
@@ -24,6 +25,7 @@ export declare const formDraftEntity: import("@cosmicdrift/kumiko-types/fields")
24
25
  readonly type: "text";
25
26
  readonly description?: string;
26
27
  readonly maxLength?: number;
28
+ readonly minLength?: number;
27
29
  readonly required?: boolean;
28
30
  readonly searchable?: boolean;
29
31
  readonly sortable?: boolean;
@@ -1,8 +1,12 @@
1
1
  import { type DbConnection } from "@cosmicdrift/kumiko-framework/db";
2
- import { type ConfigKeyDefinition, type JobContext, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { type AppContext, type ConfigKeyDefinition, type JobContext, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
3
3
  import { type StaleDraftRow } from "../db/queries/cleanup.js";
4
4
  export declare const FORM_DRAFT_RETENTION_DAYS_CONFIG_KEY = "form-draft:config:retention-days";
5
5
  export declare const FORM_DRAFT_DEFAULT_RETENTION_DAYS = 30;
6
6
  export declare const formDraftRetentionDaysConfig: ConfigKeyDefinition<"number">;
7
7
  export declare function groupStaleDraftIdsByTenant(batch: readonly StaleDraftRow[]): ReadonlyMap<TenantId, readonly string[]>;
8
+ export declare function deleteStaleDraftsBatch(batch: readonly StaleDraftRow[], db: DbConnection, retentionDays: number, log: AppContext["log"]): Promise<{
9
+ readonly deletedRows: readonly StaleDraftRow[];
10
+ readonly skippedCount: number;
11
+ }>;
8
12
  export declare function cleanupDraftsJob(_payload: Record<string, unknown>, ctx: JobContext, db: DbConnection): Promise<void>;
@@ -72,16 +72,19 @@ export function groupStaleDraftIdsByTenant(batch) {
72
72
  // resurrects, PII included. formDraftExecutor.delete() is the event-sourced
73
73
  // path; it needs a tenant-scoped db + a SessionUser, so rows are grouped by
74
74
  // tenant first (see groupStaleDraftIdsByTenant above).
75
- async function deleteStaleDraftsBatch(batch, db, retentionDays, log) {
75
+ export async function deleteStaleDraftsBatch(batch, db, retentionDays, log) {
76
76
  const deletedRows = [];
77
+ let skippedCount = 0;
77
78
  const byId = new Map(batch.map((row) => [row.id, row]));
78
79
  for (const [tenantId, ids] of groupStaleDraftIdsByTenant(batch)) {
79
80
  const systemUser = createSystemUser(tenantId);
80
81
  const tenantDb = createTenantDb(db, tenantId, "system");
81
82
  for (const id of ids) {
82
83
  // Race: user may have saved between select and here — skip if no longer stale.
83
- if (!(await isDraftStillStale(db, id, retentionDays)))
84
+ if (!(await isDraftStillStale(db, tenantId, id, retentionDays))) {
85
+ skippedCount++;
84
86
  continue;
87
+ }
85
88
  const result = await formDraftExecutor.delete({ id }, systemUser, tenantDb);
86
89
  if (result.isSuccess) {
87
90
  const row = byId.get(id);
@@ -93,7 +96,7 @@ async function deleteStaleDraftsBatch(batch, db, retentionDays, log) {
93
96
  }
94
97
  }
95
98
  }
96
- return deletedRows;
99
+ return { deletedRows, skippedCount };
97
100
  }
98
101
  export async function cleanupDraftsJob(_payload, ctx, db) {
99
102
  const resolved = ctx.configResolver
@@ -122,12 +125,15 @@ export async function cleanupDraftsJob(_payload, ctx, db) {
122
125
  // entirely for rows with no FileRefs in the blob, the common case.
123
126
  // Delete first (with staleness re-check), then release binaries from rows
124
127
  // that actually disappeared — never release a draft the user just saved.
125
- const deletedRows = await deleteStaleDraftsBatch(batch, db, retentionDays, ctx.log);
128
+ const { deletedRows, skippedCount } = await deleteStaleDraftsBatch(batch, db, retentionDays, ctx.log);
126
129
  for (const row of deletedRows) {
127
130
  await releaseRowFileRefs(row, db, fileProviderResolver, ctx.log);
128
131
  }
129
132
  deleted += deletedRows.length;
130
- if (deletedRows.length === 0 || batch.length < DEFAULT_BATCH_SIZE)
133
+ // A skipped row is no longer stale, so the next select drops it — a fully
134
+ // skipped batch still makes progress. Only a batch with neither deletes nor
135
+ // skips (all failed) would reselect the same rows forever.
136
+ if (deletedRows.length + skippedCount === 0 || batch.length < DEFAULT_BATCH_SIZE)
131
137
  break;
132
138
  }
133
139
  ctx.log?.info?.(`[form-draft:cleanup] deleted=${deleted} retentionDays=${retentionDays}`);
@@ -1,3 +1,3 @@
1
1
  export { requireDefined, requireNonEmpty, requireSecretSet } from "./config-helpers.js";
2
2
  export { isHostAllowlisted, readHostAllowlistFromEnv } from "./host-allowlist.js";
3
- export { BlockedHostError, HostResolutionError, MAIL_ALLOWED_PRIVATE_HOSTS_ENV_VAR, type MailConnectTarget, type MailHostGuardOptions, readAllowedPrivateMailHostsFromEnv, resolveMailConnectTarget, } from "./mail-host-policy.js";
3
+ export { BlockedHostError, HostResolutionError, MAIL_ALLOWED_PRIVATE_HOSTS_ENV_VAR, type MailConnectTarget, type MailHostGuardOptions, mailAllowedPrivateHostsEnvSchema, readAllowedPrivateMailHostsFromEnv, resolveMailConnectTarget, } from "./mail-host-policy.js";
@@ -2,4 +2,4 @@
2
2
  // Foundation packages (ai-foundation, mail-foundation, file-foundation).
3
3
  export { requireDefined, requireNonEmpty, requireSecretSet } from "./config-helpers.js";
4
4
  export { isHostAllowlisted, readHostAllowlistFromEnv } from "./host-allowlist.js";
5
- export { BlockedHostError, HostResolutionError, MAIL_ALLOWED_PRIVATE_HOSTS_ENV_VAR, readAllowedPrivateMailHostsFromEnv, resolveMailConnectTarget, } from "./mail-host-policy.js";
5
+ export { BlockedHostError, HostResolutionError, MAIL_ALLOWED_PRIVATE_HOSTS_ENV_VAR, mailAllowedPrivateHostsEnvSchema, readAllowedPrivateMailHostsFromEnv, resolveMailConnectTarget, } from "./mail-host-policy.js";
@@ -1,5 +1,9 @@
1
1
  import type { lookup } from "node:dns/promises";
2
+ import * as z from "zod";
2
3
  export declare const MAIL_ALLOWED_PRIVATE_HOSTS_ENV_VAR = "KUMIKO_MAIL_ALLOWED_PRIVATE_HOSTS";
4
+ export declare const mailAllowedPrivateHostsEnvSchema: z.ZodObject<{
5
+ KUMIKO_MAIL_ALLOWED_PRIVATE_HOSTS: z.ZodOptional<z.ZodString>;
6
+ }, z.core.$strip>;
3
7
  export declare function readAllowedPrivateMailHostsFromEnv(env?: Readonly<Record<string, string | undefined>>): readonly string[];
4
8
  export type MailHostGuardOptions = {
5
9
  readonly allowedPrivateMailHosts?: readonly string[];
@@ -10,14 +10,23 @@
10
10
  // internal relay or a dev/test server (mailpit, greenmail) — deliberately
11
11
  // an operator env var (KUMIKO_MAIL_ALLOWED_PRIVATE_HOSTS, read via
12
12
  // readAllowedPrivateMailHostsFromEnv), never a tenant-config key, so a
13
- // tenant can never grant themselves the bypass. Shared by both features
14
- // since a deploy commonly mounts both; declared once in mail-transport-
15
- // smtp's envSchema (see its feature.ts) to avoid the framework's env-var-
16
- // conflict check tripping when both features are mounted together.
13
+ // tenant can never grant themselves the bypass. Both features register the
14
+ // shared mailAllowedPrivateHostsEnvSchema below.
17
15
  import { isIP } from "node:net";
18
16
  import { resolvePublicHostname } from "@cosmicdrift/kumiko-framework/http";
17
+ import * as z from "zod";
19
18
  import { isHostAllowlisted, readHostAllowlistFromEnv } from "./host-allowlist.js";
20
19
  export const MAIL_ALLOWED_PRIVATE_HOSTS_ENV_VAR = "KUMIKO_MAIL_ALLOWED_PRIVATE_HOSTS";
20
+ // One schema instance, registered by every feature that reads the variable:
21
+ // composeEnvSchema accepts the same field instance from several features, so
22
+ // an app mounting only one of them still gets the variable validated and
23
+ // documented.
24
+ export const mailAllowedPrivateHostsEnvSchema = z.object({
25
+ [MAIL_ALLOWED_PRIVATE_HOSTS_ENV_VAR]: z
26
+ .string()
27
+ .optional()
28
+ .describe("Comma-separated operator allowlist of private/internal hosts (e.g. a local mailpit/greenmail dev server) that bypass the public-address check for SMTP and IMAP host config. Shared by mail-transport-smtp and inbound-provider-imap — never a tenant-config value."),
29
+ });
21
30
  export function readAllowedPrivateMailHostsFromEnv(env = process.env) {
22
31
  return readHostAllowlistFromEnv(MAIL_ALLOWED_PRIVATE_HOSTS_ENV_VAR, env);
23
32
  }
@@ -5,6 +5,7 @@ export declare const mailAccountEntity: import("@cosmicdrift/kumiko-types/fields
5
5
  readonly type: "text";
6
6
  readonly description?: string;
7
7
  readonly maxLength?: number;
8
+ readonly minLength?: number;
8
9
  readonly required?: boolean;
9
10
  readonly searchable?: boolean;
10
11
  readonly sortable?: boolean;
@@ -26,6 +27,7 @@ export declare const mailAccountEntity: import("@cosmicdrift/kumiko-types/fields
26
27
  readonly type: "text";
27
28
  readonly description?: string;
28
29
  readonly maxLength?: number;
30
+ readonly minLength?: number;
29
31
  readonly required?: boolean;
30
32
  readonly searchable?: boolean;
31
33
  readonly sortable?: boolean;
@@ -47,6 +49,7 @@ export declare const mailAccountEntity: import("@cosmicdrift/kumiko-types/fields
47
49
  readonly type: "text";
48
50
  readonly description?: string;
49
51
  readonly maxLength?: number;
52
+ readonly minLength?: number;
50
53
  readonly required?: boolean;
51
54
  readonly searchable?: boolean;
52
55
  readonly sortable?: boolean;
@@ -68,6 +71,7 @@ export declare const mailAccountEntity: import("@cosmicdrift/kumiko-types/fields
68
71
  readonly type: "text";
69
72
  readonly description?: string;
70
73
  readonly maxLength?: number;
74
+ readonly minLength?: number;
71
75
  readonly required?: boolean;
72
76
  readonly searchable?: boolean;
73
77
  readonly sortable?: boolean;
@@ -89,6 +93,7 @@ export declare const mailAccountEntity: import("@cosmicdrift/kumiko-types/fields
89
93
  readonly type: "text";
90
94
  readonly description?: string;
91
95
  readonly maxLength?: number;
96
+ readonly minLength?: number;
92
97
  readonly required?: boolean;
93
98
  readonly searchable?: boolean;
94
99
  readonly sortable?: boolean;
@@ -110,6 +115,7 @@ export declare const mailAccountEntity: import("@cosmicdrift/kumiko-types/fields
110
115
  readonly type: "text";
111
116
  readonly description?: string;
112
117
  readonly maxLength?: number;
118
+ readonly minLength?: number;
113
119
  readonly required?: boolean;
114
120
  readonly searchable?: boolean;
115
121
  readonly sortable?: boolean;
@@ -131,6 +137,7 @@ export declare const mailAccountEntity: import("@cosmicdrift/kumiko-types/fields
131
137
  readonly type: "text";
132
138
  readonly description?: string;
133
139
  readonly maxLength?: number;
140
+ readonly minLength?: number;
134
141
  readonly required?: boolean;
135
142
  readonly searchable?: boolean;
136
143
  readonly sortable?: boolean;
@@ -172,6 +179,7 @@ export declare const inboundMessageEntity: import("@cosmicdrift/kumiko-types/fie
172
179
  readonly type: "text";
173
180
  readonly description?: string;
174
181
  readonly maxLength?: number;
182
+ readonly minLength?: number;
175
183
  readonly required?: boolean;
176
184
  readonly searchable?: boolean;
177
185
  readonly sortable?: boolean;
@@ -193,6 +201,7 @@ export declare const inboundMessageEntity: import("@cosmicdrift/kumiko-types/fie
193
201
  readonly type: "text";
194
202
  readonly description?: string;
195
203
  readonly maxLength?: number;
204
+ readonly minLength?: number;
196
205
  readonly required?: boolean;
197
206
  readonly searchable?: boolean;
198
207
  readonly sortable?: boolean;
@@ -214,6 +223,7 @@ export declare const inboundMessageEntity: import("@cosmicdrift/kumiko-types/fie
214
223
  readonly type: "text";
215
224
  readonly description?: string;
216
225
  readonly maxLength?: number;
226
+ readonly minLength?: number;
217
227
  readonly required?: boolean;
218
228
  readonly searchable?: boolean;
219
229
  readonly sortable?: boolean;
@@ -235,6 +245,7 @@ export declare const inboundMessageEntity: import("@cosmicdrift/kumiko-types/fie
235
245
  readonly type: "text";
236
246
  readonly description?: string;
237
247
  readonly maxLength?: number;
248
+ readonly minLength?: number;
238
249
  readonly required?: boolean;
239
250
  readonly searchable?: boolean;
240
251
  readonly sortable?: boolean;
@@ -256,6 +267,7 @@ export declare const inboundMessageEntity: import("@cosmicdrift/kumiko-types/fie
256
267
  readonly type: "text";
257
268
  readonly description?: string;
258
269
  readonly maxLength?: number;
270
+ readonly minLength?: number;
259
271
  readonly required?: boolean;
260
272
  readonly searchable?: boolean;
261
273
  readonly sortable?: boolean;
@@ -277,6 +289,7 @@ export declare const inboundMessageEntity: import("@cosmicdrift/kumiko-types/fie
277
289
  readonly type: "text";
278
290
  readonly description?: string;
279
291
  readonly maxLength?: number;
292
+ readonly minLength?: number;
280
293
  readonly required?: boolean;
281
294
  readonly searchable?: boolean;
282
295
  readonly sortable?: boolean;
@@ -298,6 +311,7 @@ export declare const inboundMessageEntity: import("@cosmicdrift/kumiko-types/fie
298
311
  readonly type: "text";
299
312
  readonly description?: string;
300
313
  readonly maxLength?: number;
314
+ readonly minLength?: number;
301
315
  readonly required?: boolean;
302
316
  readonly searchable?: boolean;
303
317
  readonly sortable?: boolean;
@@ -319,6 +333,7 @@ export declare const inboundMessageEntity: import("@cosmicdrift/kumiko-types/fie
319
333
  readonly type: "text";
320
334
  readonly description?: string;
321
335
  readonly maxLength?: number;
336
+ readonly minLength?: number;
322
337
  readonly required?: boolean;
323
338
  readonly searchable?: boolean;
324
339
  readonly sortable?: boolean;
@@ -340,6 +355,7 @@ export declare const inboundMessageEntity: import("@cosmicdrift/kumiko-types/fie
340
355
  readonly type: "text";
341
356
  readonly description?: string;
342
357
  readonly maxLength?: number;
358
+ readonly minLength?: number;
343
359
  readonly required?: boolean;
344
360
  readonly searchable?: boolean;
345
361
  readonly sortable?: boolean;
@@ -376,6 +392,7 @@ export declare const inboundMessageEntity: import("@cosmicdrift/kumiko-types/fie
376
392
  readonly type: "text";
377
393
  readonly description?: string;
378
394
  readonly maxLength?: number;
395
+ readonly minLength?: number;
379
396
  readonly required?: boolean;
380
397
  readonly searchable?: boolean;
381
398
  readonly sortable?: boolean;
@@ -397,6 +414,7 @@ export declare const inboundMessageEntity: import("@cosmicdrift/kumiko-types/fie
397
414
  readonly type: "text";
398
415
  readonly description?: string;
399
416
  readonly maxLength?: number;
417
+ readonly minLength?: number;
400
418
  readonly required?: boolean;
401
419
  readonly searchable?: boolean;
402
420
  readonly sortable?: boolean;
@@ -422,6 +440,7 @@ export declare const mailThreadEntity: import("@cosmicdrift/kumiko-types/fields"
422
440
  readonly type: "text";
423
441
  readonly description?: string;
424
442
  readonly maxLength?: number;
443
+ readonly minLength?: number;
425
444
  readonly required?: boolean;
426
445
  readonly searchable?: boolean;
427
446
  readonly sortable?: boolean;
@@ -443,6 +462,7 @@ export declare const mailThreadEntity: import("@cosmicdrift/kumiko-types/fields"
443
462
  readonly type: "text";
444
463
  readonly description?: string;
445
464
  readonly maxLength?: number;
465
+ readonly minLength?: number;
446
466
  readonly required?: boolean;
447
467
  readonly searchable?: boolean;
448
468
  readonly sortable?: boolean;
@@ -507,6 +527,7 @@ export declare const syncCursorEntity: import("@cosmicdrift/kumiko-types/fields"
507
527
  readonly type: "text";
508
528
  readonly description?: string;
509
529
  readonly maxLength?: number;
530
+ readonly minLength?: number;
510
531
  readonly required?: boolean;
511
532
  readonly searchable?: boolean;
512
533
  readonly sortable?: boolean;
@@ -528,6 +549,7 @@ export declare const syncCursorEntity: import("@cosmicdrift/kumiko-types/fields"
528
549
  readonly type: "text";
529
550
  readonly description?: string;
530
551
  readonly maxLength?: number;
552
+ readonly minLength?: number;
531
553
  readonly required?: boolean;
532
554
  readonly searchable?: boolean;
533
555
  readonly sortable?: boolean;
@@ -549,6 +571,7 @@ export declare const syncCursorEntity: import("@cosmicdrift/kumiko-types/fields"
549
571
  readonly type: "text";
550
572
  readonly description?: string;
551
573
  readonly maxLength?: number;
574
+ readonly minLength?: number;
552
575
  readonly required?: boolean;
553
576
  readonly searchable?: boolean;
554
577
  readonly sortable?: boolean;
@@ -591,6 +614,7 @@ export declare const seenMessageEntity: import("@cosmicdrift/kumiko-types/fields
591
614
  readonly type: "text";
592
615
  readonly description?: string;
593
616
  readonly maxLength?: number;
617
+ readonly minLength?: number;
594
618
  readonly required?: boolean;
595
619
  readonly searchable?: boolean;
596
620
  readonly sortable?: boolean;
@@ -612,6 +636,7 @@ export declare const seenMessageEntity: import("@cosmicdrift/kumiko-types/fields
612
636
  readonly type: "text";
613
637
  readonly description?: string;
614
638
  readonly maxLength?: number;
639
+ readonly minLength?: number;
615
640
  readonly required?: boolean;
616
641
  readonly searchable?: boolean;
617
642
  readonly sortable?: boolean;
@@ -3,6 +3,7 @@ export declare const mailAccountsProjectionTable: import("@cosmicdrift/kumiko-fr
3
3
  readonly type: "text";
4
4
  readonly description?: string;
5
5
  readonly maxLength?: number;
6
+ readonly minLength?: number;
6
7
  readonly required?: boolean;
7
8
  readonly searchable?: boolean;
8
9
  readonly sortable?: boolean;
@@ -24,6 +25,7 @@ export declare const mailAccountsProjectionTable: import("@cosmicdrift/kumiko-fr
24
25
  readonly type: "text";
25
26
  readonly description?: string;
26
27
  readonly maxLength?: number;
28
+ readonly minLength?: number;
27
29
  readonly required?: boolean;
28
30
  readonly searchable?: boolean;
29
31
  readonly sortable?: boolean;
@@ -45,6 +47,7 @@ export declare const mailAccountsProjectionTable: import("@cosmicdrift/kumiko-fr
45
47
  readonly type: "text";
46
48
  readonly description?: string;
47
49
  readonly maxLength?: number;
50
+ readonly minLength?: number;
48
51
  readonly required?: boolean;
49
52
  readonly searchable?: boolean;
50
53
  readonly sortable?: boolean;
@@ -66,6 +69,7 @@ export declare const mailAccountsProjectionTable: import("@cosmicdrift/kumiko-fr
66
69
  readonly type: "text";
67
70
  readonly description?: string;
68
71
  readonly maxLength?: number;
72
+ readonly minLength?: number;
69
73
  readonly required?: boolean;
70
74
  readonly searchable?: boolean;
71
75
  readonly sortable?: boolean;
@@ -87,6 +91,7 @@ export declare const mailAccountsProjectionTable: import("@cosmicdrift/kumiko-fr
87
91
  readonly type: "text";
88
92
  readonly description?: string;
89
93
  readonly maxLength?: number;
94
+ readonly minLength?: number;
90
95
  readonly required?: boolean;
91
96
  readonly searchable?: boolean;
92
97
  readonly sortable?: boolean;
@@ -108,6 +113,7 @@ export declare const mailAccountsProjectionTable: import("@cosmicdrift/kumiko-fr
108
113
  readonly type: "text";
109
114
  readonly description?: string;
110
115
  readonly maxLength?: number;
116
+ readonly minLength?: number;
111
117
  readonly required?: boolean;
112
118
  readonly searchable?: boolean;
113
119
  readonly sortable?: boolean;
@@ -129,6 +135,7 @@ export declare const mailAccountsProjectionTable: import("@cosmicdrift/kumiko-fr
129
135
  readonly type: "text";
130
136
  readonly description?: string;
131
137
  readonly maxLength?: number;
138
+ readonly minLength?: number;
132
139
  readonly required?: boolean;
133
140
  readonly searchable?: boolean;
134
141
  readonly sortable?: boolean;
@@ -167,6 +174,7 @@ export declare const inboundMessagesProjectionTable: import("@cosmicdrift/kumiko
167
174
  readonly type: "text";
168
175
  readonly description?: string;
169
176
  readonly maxLength?: number;
177
+ readonly minLength?: number;
170
178
  readonly required?: boolean;
171
179
  readonly searchable?: boolean;
172
180
  readonly sortable?: boolean;
@@ -188,6 +196,7 @@ export declare const inboundMessagesProjectionTable: import("@cosmicdrift/kumiko
188
196
  readonly type: "text";
189
197
  readonly description?: string;
190
198
  readonly maxLength?: number;
199
+ readonly minLength?: number;
191
200
  readonly required?: boolean;
192
201
  readonly searchable?: boolean;
193
202
  readonly sortable?: boolean;
@@ -209,6 +218,7 @@ export declare const inboundMessagesProjectionTable: import("@cosmicdrift/kumiko
209
218
  readonly type: "text";
210
219
  readonly description?: string;
211
220
  readonly maxLength?: number;
221
+ readonly minLength?: number;
212
222
  readonly required?: boolean;
213
223
  readonly searchable?: boolean;
214
224
  readonly sortable?: boolean;
@@ -230,6 +240,7 @@ export declare const inboundMessagesProjectionTable: import("@cosmicdrift/kumiko
230
240
  readonly type: "text";
231
241
  readonly description?: string;
232
242
  readonly maxLength?: number;
243
+ readonly minLength?: number;
233
244
  readonly required?: boolean;
234
245
  readonly searchable?: boolean;
235
246
  readonly sortable?: boolean;
@@ -251,6 +262,7 @@ export declare const inboundMessagesProjectionTable: import("@cosmicdrift/kumiko
251
262
  readonly type: "text";
252
263
  readonly description?: string;
253
264
  readonly maxLength?: number;
265
+ readonly minLength?: number;
254
266
  readonly required?: boolean;
255
267
  readonly searchable?: boolean;
256
268
  readonly sortable?: boolean;
@@ -272,6 +284,7 @@ export declare const inboundMessagesProjectionTable: import("@cosmicdrift/kumiko
272
284
  readonly type: "text";
273
285
  readonly description?: string;
274
286
  readonly maxLength?: number;
287
+ readonly minLength?: number;
275
288
  readonly required?: boolean;
276
289
  readonly searchable?: boolean;
277
290
  readonly sortable?: boolean;
@@ -293,6 +306,7 @@ export declare const inboundMessagesProjectionTable: import("@cosmicdrift/kumiko
293
306
  readonly type: "text";
294
307
  readonly description?: string;
295
308
  readonly maxLength?: number;
309
+ readonly minLength?: number;
296
310
  readonly required?: boolean;
297
311
  readonly searchable?: boolean;
298
312
  readonly sortable?: boolean;
@@ -314,6 +328,7 @@ export declare const inboundMessagesProjectionTable: import("@cosmicdrift/kumiko
314
328
  readonly type: "text";
315
329
  readonly description?: string;
316
330
  readonly maxLength?: number;
331
+ readonly minLength?: number;
317
332
  readonly required?: boolean;
318
333
  readonly searchable?: boolean;
319
334
  readonly sortable?: boolean;
@@ -335,6 +350,7 @@ export declare const inboundMessagesProjectionTable: import("@cosmicdrift/kumiko
335
350
  readonly type: "text";
336
351
  readonly description?: string;
337
352
  readonly maxLength?: number;
353
+ readonly minLength?: number;
338
354
  readonly required?: boolean;
339
355
  readonly searchable?: boolean;
340
356
  readonly sortable?: boolean;
@@ -371,6 +387,7 @@ export declare const inboundMessagesProjectionTable: import("@cosmicdrift/kumiko
371
387
  readonly type: "text";
372
388
  readonly description?: string;
373
389
  readonly maxLength?: number;
390
+ readonly minLength?: number;
374
391
  readonly required?: boolean;
375
392
  readonly searchable?: boolean;
376
393
  readonly sortable?: boolean;
@@ -392,6 +409,7 @@ export declare const inboundMessagesProjectionTable: import("@cosmicdrift/kumiko
392
409
  readonly type: "text";
393
410
  readonly description?: string;
394
411
  readonly maxLength?: number;
412
+ readonly minLength?: number;
395
413
  readonly required?: boolean;
396
414
  readonly searchable?: boolean;
397
415
  readonly sortable?: boolean;
@@ -415,6 +433,7 @@ export declare const mailThreadsProjectionTable: import("@cosmicdrift/kumiko-fra
415
433
  readonly type: "text";
416
434
  readonly description?: string;
417
435
  readonly maxLength?: number;
436
+ readonly minLength?: number;
418
437
  readonly required?: boolean;
419
438
  readonly searchable?: boolean;
420
439
  readonly sortable?: boolean;
@@ -436,6 +455,7 @@ export declare const mailThreadsProjectionTable: import("@cosmicdrift/kumiko-fra
436
455
  readonly type: "text";
437
456
  readonly description?: string;
438
457
  readonly maxLength?: number;
458
+ readonly minLength?: number;
439
459
  readonly required?: boolean;
440
460
  readonly searchable?: boolean;
441
461
  readonly sortable?: boolean;
@@ -22,6 +22,7 @@
22
22
  // Credentials: JSON-Dokument im per-Account-Secret-Slot
23
23
  // (credential-document.ts) — kein Tenant-Config, weil mehrere Accounts
24
24
  // pro Tenant verschiedene Hosts haben.
25
+ import { mailAllowedPrivateHostsEnvSchema } from "@cosmicdrift/kumiko-bundled-features/foundation-shared";
25
26
  import { INBOUND_MAIL_PROVIDER_EXTENSION, InboundAuthError, inboundCredentialSecretKey, } from "@cosmicdrift/kumiko-bundled-features/inbound-mail-foundation";
26
27
  import { requireSecretsContext } from "@cosmicdrift/kumiko-bundled-features/secrets";
27
28
  import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
@@ -225,5 +226,6 @@ export const inboundProviderImapFeature = defineFeature(FEATURE_NAME, (r) => {
225
226
  });
226
227
  r.requires("inbound-mail-foundation");
227
228
  r.requires("secrets");
229
+ r.envSchema(mailAllowedPrivateHostsEnvSchema);
228
230
  r.useExtension(INBOUND_MAIL_PROVIDER_EXTENSION, IMAP_PROVIDER_KEY, imapInboundMailPlugin);
229
231
  });
@@ -21,10 +21,9 @@ const IMAP_HOST_UNREACHABLE_MESSAGE = "IMAP host is not reachable or not allowed
21
21
  // =============================================================================
22
22
  // Operator escape hatch for an internal relay or a dev/test IMAP server
23
23
  // (greenmail): KUMIKO_MAIL_ALLOWED_PRIVATE_HOSTS, the same operator env
24
- // var mail-transport-smtp declares (see its envSchema — composeEnvSchema
25
- // rejects two features declaring the same key, so this feature reads it
26
- // without redeclaring it), never a tenant-config value, so a tenant can
27
- // never grant themselves the private-host bypass.
24
+ // var mail-transport-smtp registers (both share one schema instance, see
25
+ // foundation-shared), never a tenant-config value, so a tenant can never
26
+ // grant themselves the private-host bypass.
28
27
  //
29
28
  // mailHostLookup is a test-only DNS seam — production leaves it undefined,
30
29
  // so resolveMailConnectTarget uses the real resolver. Module-global state:
@@ -8,6 +8,7 @@ export declare const JobQueries: {
8
8
  readonly details: "jobs:query:details";
9
9
  readonly catalog: "jobs:query:catalog";
10
10
  readonly failures: "jobs:query:failures";
11
+ readonly tenantRuns: "jobs:query:tenant-runs";
11
12
  };
12
13
  export declare const JobErrors: {
13
14
  readonly unknownJob: "unknown_job";
@@ -12,6 +12,7 @@ export const JobQueries = {
12
12
  details: "jobs:query:details",
13
13
  catalog: "jobs:query:catalog",
14
14
  failures: "jobs:query:failures",
15
+ tenantRuns: "jobs:query:tenant-runs",
15
16
  };
16
17
  // Error codes
17
18
  export const JobErrors = {
@@ -3,5 +3,6 @@ export type JobRunRetentionResult = {
3
3
  readonly runsDeleted: number;
4
4
  readonly logsDeleted: number;
5
5
  readonly tenantFailuresDeleted: number;
6
+ readonly tenantRunsDeleted: number;
6
7
  };
7
8
  export declare function deleteStaleJobRuns(db: DbConnection, retentionDays: number): Promise<JobRunRetentionResult>;
@@ -12,6 +12,7 @@
12
12
  import { deleteManyBatched } from "@cosmicdrift/kumiko-framework/bun-db";
13
13
  import { jobRunLogsTable, jobRunsTable } from "../../job-run-table.js";
14
14
  import { tenantJobFailuresTable } from "../../tenant-job-failure-table.js";
15
+ import { tenantJobRunsTable } from "../../tenant-job-run-table.js";
15
16
  const RETENTION_DELETE_BATCH_SIZE = 500;
16
17
  export async function deleteStaleJobRuns(db, retentionDays) {
17
18
  const cutoff = Temporal.Now.instant().subtract({ hours: retentionDays * 24 });
@@ -21,9 +22,13 @@ export async function deleteStaleJobRuns(db, retentionDays) {
21
22
  // they point at is gone by now, and a job whose subject never ran again
22
23
  // would otherwise keep its record forever.
23
24
  const tenantFailuresResult = await deleteManyBatched(db, tenantJobFailuresTable, { failedAt: { lt: cutoff } }, { limit: RETENTION_DELETE_BATCH_SIZE });
25
+ // Only finished rows: active ones are the stale sweep's business, and a
26
+ // still-running job older than the window must not vanish from the tenant.
27
+ const tenantRunsResult = await deleteManyBatched(db, tenantJobRunsTable, { status: { in: ["completed", "failed"] }, updatedAt: { lt: cutoff } }, { limit: RETENTION_DELETE_BATCH_SIZE });
24
28
  return {
25
29
  runsDeleted: runsResult.deleted,
26
30
  logsDeleted: logsResult.deleted,
27
31
  tenantFailuresDeleted: tenantFailuresResult.deleted,
32
+ tenantRunsDeleted: tenantRunsResult.deleted,
28
33
  };
29
34
  }
@@ -2,5 +2,7 @@ import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
2
2
  export declare const STALE_JOB_RUN_ERROR = "job run exceeded the stale-run timeout without a completion signal (likely a crashed worker process)";
3
3
  export type StaleRunSweepResult = {
4
4
  readonly runsMarkedFailed: number;
5
+ readonly tenantRunsMarkedFailed: number;
6
+ readonly tenantQueuedDropped: number;
5
7
  };
6
8
  export declare function markStaleJobRunsFailed(db: DbConnection, timeoutHours: number): Promise<StaleRunSweepResult>;