@cosmicdrift/kumiko-bundled-features 0.352.0 → 0.353.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 (105) hide show
  1. package/dist/agent-tools/agent-manifest.js +8 -2
  2. package/dist/agent-tools/manifest-deny-qns.d.ts +3 -0
  3. package/dist/agent-tools/manifest-deny-qns.js +17 -0
  4. package/dist/agent-tools/tool-catalog.d.ts +2 -1
  5. package/dist/agent-tools/tool-catalog.js +4 -1
  6. package/dist/audit/handlers/list.query.js +13 -8
  7. package/dist/auth-email-password/handlers/signup-confirm.write.js +2 -3
  8. package/dist/auth-mfa/schema/user-mfa.d.ts +6 -0
  9. package/dist/auth-mfa/screens.js +2 -2
  10. package/dist/billing-foundation/checkout-core.d.ts +7 -0
  11. package/dist/billing-foundation/checkout-core.js +7 -2
  12. package/dist/billing-foundation/entities.d.ts +8 -0
  13. package/dist/billing-foundation/handlers/start-plan-checkout.write.js +7 -1
  14. package/dist/billing-foundation/projection.d.ts +8 -0
  15. package/dist/cap-counter/entity.d.ts +1 -0
  16. package/dist/cap-overview/access.js +2 -9
  17. package/dist/cap-overview/handlers/tenant-caps-list.query.js +2 -2
  18. package/dist/cap-overview/handlers/tenant-options.query.js +4 -5
  19. package/dist/compliance-profiles/schema/profile-selection.d.ts +8 -8
  20. package/dist/compliance-profiles-ops/handlers/tenants-missing-profile.query.js +2 -4
  21. package/dist/config/handlers/values.query.d.ts +3 -1
  22. package/dist/config/handlers/values.query.js +7 -2
  23. package/dist/config/table.d.ts +6 -0
  24. package/dist/custom-fields/entity.d.ts +4 -0
  25. package/dist/data-retention/schema/tenant-retention-override.d.ts +4 -0
  26. package/dist/delivery/public-names.js +1 -1
  27. package/dist/delivery/tables.d.ts +6 -0
  28. package/dist/document-ingest-foundation/entity.d.ts +4 -0
  29. package/dist/files-tenant-data/hooks.js +3 -2
  30. package/dist/folders/entity.d.ts +8 -0
  31. package/dist/folders/feature.js +2 -10
  32. package/dist/form-draft/db/queries/cleanup.d.ts +1 -1
  33. package/dist/form-draft/db/queries/cleanup.js +4 -3
  34. package/dist/form-draft/entity.d.ts +2 -0
  35. package/dist/form-draft/handlers/cleanup.job.d.ts +5 -1
  36. package/dist/form-draft/handlers/cleanup.job.js +11 -5
  37. package/dist/inbound-mail-foundation/entities.d.ts +25 -0
  38. package/dist/inbound-mail-foundation/projection.d.ts +20 -0
  39. package/dist/jobs/handlers/list.query.d.ts +2 -1
  40. package/dist/jobs/handlers/list.query.js +39 -14
  41. package/dist/jobs/i18n.js +1 -0
  42. package/dist/jobs/job-run-table.d.ts +6 -0
  43. package/dist/ledger/entity.d.ts +21 -8
  44. package/dist/ledger/entity.js +4 -0
  45. package/dist/ledger/schemas.js +8 -2
  46. package/dist/managed-pages/feature.js +28 -10
  47. package/dist/managed-pages/table.d.ts +12 -0
  48. package/dist/notes-history/entity.d.ts +12 -0
  49. package/dist/notes-history/feature.js +2 -10
  50. package/dist/personal-access-tokens/schema/api-token.d.ts +6 -0
  51. package/dist/personal-access-tokens/screens.js +14 -3
  52. package/dist/secrets/table.d.ts +1 -0
  53. package/dist/seo/feature.js +28 -6
  54. package/dist/sessions/schema/user-session.d.ts +4 -0
  55. package/dist/shared/assert-no-where-rule-in-ownership-write.d.ts +2 -0
  56. package/dist/shared/assert-no-where-rule-in-ownership-write.js +18 -0
  57. package/dist/shared/index.d.ts +2 -1
  58. package/dist/shared/index.js +2 -1
  59. package/dist/shared/rate-limited-text-response.d.ts +2 -0
  60. package/dist/shared/rate-limited-text-response.js +9 -0
  61. package/dist/tags/entity.d.ts +9 -0
  62. package/dist/tags/feature.js +2 -10
  63. package/dist/template-resolver/table.d.ts +40 -30
  64. package/dist/template-resolver/user-content-table.d.ts +30 -20
  65. package/dist/tenant/decrypt-tenant-names.d.ts +3 -0
  66. package/dist/tenant/decrypt-tenant-names.js +10 -0
  67. package/dist/tenant/handlers/me.query.js +5 -1
  68. package/dist/tenant/handlers/memberships.query.js +3 -2
  69. package/dist/tenant/handlers/tenant-directory.query.js +9 -3
  70. package/dist/tenant/index.d.ts +1 -0
  71. package/dist/tenant/index.js +1 -0
  72. package/dist/tenant/invitation-table.d.ts +14 -8
  73. package/dist/tenant/membership-table.d.ts +4 -0
  74. package/dist/tenant/schema/tenant.d.ts +16 -10
  75. package/dist/tenant/schema/tenant.js +4 -6
  76. package/dist/tenant/screens.js +1 -1
  77. package/dist/tenant-handover/move-entity-graph.js +11 -18
  78. package/dist/tenant-lifecycle/feature.js +1 -0
  79. package/dist/tenant-lifecycle/run-tenant-destroy.d.ts +3 -0
  80. package/dist/tenant-lifecycle/run-tenant-destroy.js +2 -0
  81. package/dist/tenant-lifecycle/stages.d.ts +2 -0
  82. package/dist/tenant-lifecycle/stages.js +7 -1
  83. package/dist/tier-engine/entity.d.ts +2 -0
  84. package/dist/tier-engine/tier-resolver.d.ts +2 -0
  85. package/dist/user/handlers/list.query.js +2 -2
  86. package/dist/user/schema/user.d.ts +20 -8
  87. package/dist/user-data-rights/boot-checks.js +5 -0
  88. package/dist/user-data-rights/handlers/confirm-deletion-by-token.write.js +2 -2
  89. package/dist/user-data-rights/schema/download-attempt.d.ts +14 -0
  90. package/dist/user-data-rights/schema/download-token.d.ts +8 -0
  91. package/dist/user-data-rights/schema/export-job.d.ts +14 -8
  92. package/dist/waitlist/entity.d.ts +20 -6
  93. package/dist/workflow-runner/event-trigger.js +6 -3
  94. package/dist/workflow-runner/handlers/resume-run.write.js +3 -0
  95. package/package.json +9 -9
  96. package/src/agent-tools/changes.json +12 -0
  97. package/src/audit/changes.json +6 -0
  98. package/src/form-draft/changes.json +7 -1
  99. package/src/managed-pages/changes.json +6 -0
  100. package/src/seo/changes.json +5 -0
  101. package/src/tenant/changes.json +7 -0
  102. package/src/tenant-handover/changes.json +6 -0
  103. package/src/workflow-runner/changes.json +5 -0
  104. package/dist/shared/has-where-rule.d.ts +0 -2
  105. package/dist/shared/has-where-rule.js +0 -10
@@ -3,6 +3,7 @@ export declare const fieldDefinitionEntity: import("@cosmicdrift/kumiko-types/fi
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 fieldDefinitionEntity: import("@cosmicdrift/kumiko-types/fi
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 fieldDefinitionEntity: import("@cosmicdrift/kumiko-types/fi
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;
@@ -91,6 +94,7 @@ export declare const fieldDefinitionEntity: import("@cosmicdrift/kumiko-types/fi
91
94
  readonly type: "text";
92
95
  readonly description?: string;
93
96
  readonly maxLength?: number;
97
+ readonly minLength?: number;
94
98
  readonly required?: boolean;
95
99
  readonly searchable?: boolean;
96
100
  readonly sortable?: boolean;
@@ -3,6 +3,7 @@ export declare const tenantRetentionOverrideEntity: import("@cosmicdrift/kumiko-
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;
@@ -40,6 +41,7 @@ export declare const tenantRetentionOverrideEntity: import("@cosmicdrift/kumiko-
40
41
  readonly type: "text";
41
42
  readonly description?: string;
42
43
  readonly maxLength?: number;
44
+ readonly minLength?: number;
43
45
  readonly required?: boolean;
44
46
  readonly searchable?: boolean;
45
47
  readonly sortable?: boolean;
@@ -63,6 +65,7 @@ export declare const tenantRetentionOverrideTable: import("@cosmicdrift/kumiko-f
63
65
  readonly type: "text";
64
66
  readonly description?: string;
65
67
  readonly maxLength?: number;
68
+ readonly minLength?: number;
66
69
  readonly required?: boolean;
67
70
  readonly searchable?: boolean;
68
71
  readonly sortable?: boolean;
@@ -100,6 +103,7 @@ export declare const tenantRetentionOverrideTable: import("@cosmicdrift/kumiko-f
100
103
  readonly type: "text";
101
104
  readonly description?: string;
102
105
  readonly maxLength?: number;
106
+ readonly minLength?: number;
103
107
  readonly required?: boolean;
104
108
  readonly searchable?: boolean;
105
109
  readonly sortable?: boolean;
@@ -8,7 +8,7 @@ export const DeliveryHandlers = {
8
8
  resubscribeAddress: "delivery:write:resubscribe-address",
9
9
  resubscribeUser: "delivery:write:resubscribe-user",
10
10
  };
11
- // Fixed so links mailed out today keep working — the unsubscribe route is
11
+ // Stable path: links signed against it keep working. The unsubscribe route is
12
12
  // mounted at this exact path via `extraRoutes: [...createUnsubscribeRoutes(...)]`.
13
13
  export const DELIVERY_UNSUBSCRIBE_PATH = "/api/delivery/unsubscribe";
14
14
  // Mounted alongside the unsubscribe route by the same
@@ -6,6 +6,7 @@ export declare const notificationPreferenceEntity: import("@cosmicdrift/kumiko-t
6
6
  readonly type: "text";
7
7
  readonly description?: string;
8
8
  readonly maxLength?: number;
9
+ readonly minLength?: number;
9
10
  readonly required?: boolean;
10
11
  readonly searchable?: boolean;
11
12
  readonly sortable?: boolean;
@@ -27,6 +28,7 @@ export declare const notificationPreferenceEntity: import("@cosmicdrift/kumiko-t
27
28
  readonly type: "text";
28
29
  readonly description?: string;
29
30
  readonly maxLength?: number;
31
+ readonly minLength?: number;
30
32
  readonly required?: boolean;
31
33
  readonly searchable?: boolean;
32
34
  readonly sortable?: boolean;
@@ -48,6 +50,7 @@ export declare const notificationPreferenceEntity: import("@cosmicdrift/kumiko-t
48
50
  readonly type: "text";
49
51
  readonly description?: string;
50
52
  readonly maxLength?: number;
53
+ readonly minLength?: number;
51
54
  readonly required?: boolean;
52
55
  readonly searchable?: boolean;
53
56
  readonly sortable?: boolean;
@@ -75,6 +78,7 @@ export declare const notificationAddressOptOutEntity: import("@cosmicdrift/kumik
75
78
  readonly type: "text";
76
79
  readonly description?: string;
77
80
  readonly maxLength?: number;
81
+ readonly minLength?: number;
78
82
  readonly required?: boolean;
79
83
  readonly searchable?: boolean;
80
84
  readonly sortable?: boolean;
@@ -96,6 +100,7 @@ export declare const notificationAddressOptOutEntity: import("@cosmicdrift/kumik
96
100
  readonly type: "text";
97
101
  readonly description?: string;
98
102
  readonly maxLength?: number;
103
+ readonly minLength?: number;
99
104
  readonly required?: boolean;
100
105
  readonly searchable?: boolean;
101
106
  readonly sortable?: boolean;
@@ -117,6 +122,7 @@ export declare const notificationAddressOptOutEntity: import("@cosmicdrift/kumik
117
122
  readonly type: "text";
118
123
  readonly description?: string;
119
124
  readonly maxLength?: number;
125
+ readonly minLength?: number;
120
126
  readonly required?: boolean;
121
127
  readonly searchable?: boolean;
122
128
  readonly sortable?: boolean;
@@ -14,6 +14,7 @@ export declare const documentExtractEntity: import("@cosmicdrift/kumiko-types/fi
14
14
  readonly type: "text";
15
15
  readonly description?: string;
16
16
  readonly maxLength?: number;
17
+ readonly minLength?: number;
17
18
  readonly required?: boolean;
18
19
  readonly searchable?: boolean;
19
20
  readonly sortable?: boolean;
@@ -35,6 +36,7 @@ export declare const documentExtractEntity: import("@cosmicdrift/kumiko-types/fi
35
36
  readonly type: "text";
36
37
  readonly description?: string;
37
38
  readonly maxLength?: number;
39
+ readonly minLength?: number;
38
40
  readonly required?: boolean;
39
41
  readonly searchable?: boolean;
40
42
  readonly sortable?: boolean;
@@ -75,6 +77,7 @@ export declare const documentExtractsTable: import("@cosmicdrift/kumiko-framewor
75
77
  readonly type: "text";
76
78
  readonly description?: string;
77
79
  readonly maxLength?: number;
80
+ readonly minLength?: number;
78
81
  readonly required?: boolean;
79
82
  readonly searchable?: boolean;
80
83
  readonly sortable?: boolean;
@@ -96,6 +99,7 @@ export declare const documentExtractsTable: import("@cosmicdrift/kumiko-framewor
96
99
  readonly type: "text";
97
100
  readonly description?: string;
98
101
  readonly maxLength?: number;
102
+ readonly minLength?: number;
99
103
  readonly required?: boolean;
100
104
  readonly searchable?: boolean;
101
105
  readonly sortable?: boolean;
@@ -137,8 +137,9 @@ export const fileRefStorageDestroyHook = async (tenantId, ctx) => {
137
137
  }
138
138
  // Provider resolved — a list()/delete() failure from here IS fail-closed:
139
139
  // the "files" stage throws, tenant-lifecycle's retry/abandon handling sees
140
- // it, and the next sweep tick retries (list+delete are idempotent, so this
141
- // converges rather than double-deleting or erroring on a missing key).
140
+ // it and retries up to the stage's attempt cap before abandoning the destroy
141
+ // (list+delete are idempotent, so a retry converges rather than
142
+ // double-deleting or erroring on a missing key).
142
143
  for (const prefix of tenantStoragePrefixes(tenantId)) {
143
144
  const keys = await provider.list(prefix);
144
145
  const keptStemPrefixes = await survivorStemPrefixes(ctx.db, keys);
@@ -4,6 +4,7 @@ export declare const folderEntity: EntityDefinition<{
4
4
  readonly type: "text";
5
5
  readonly description?: string;
6
6
  readonly maxLength?: number;
7
+ readonly minLength?: number;
7
8
  readonly required?: boolean;
8
9
  readonly searchable?: boolean;
9
10
  readonly sortable?: boolean;
@@ -25,6 +26,7 @@ export declare const folderEntity: EntityDefinition<{
25
26
  readonly type: "text";
26
27
  readonly description?: string;
27
28
  readonly maxLength?: number;
29
+ readonly minLength?: number;
28
30
  readonly required?: boolean;
29
31
  readonly searchable?: boolean;
30
32
  readonly sortable?: boolean;
@@ -48,6 +50,7 @@ export declare function createFolderAssignmentEntity(access?: EntityDefinition["
48
50
  readonly type: "text";
49
51
  readonly description?: string;
50
52
  readonly maxLength?: number;
53
+ readonly minLength?: number;
51
54
  readonly required?: boolean;
52
55
  readonly searchable?: boolean;
53
56
  readonly sortable?: boolean;
@@ -69,6 +72,7 @@ export declare function createFolderAssignmentEntity(access?: EntityDefinition["
69
72
  readonly type: "text";
70
73
  readonly description?: string;
71
74
  readonly maxLength?: number;
75
+ readonly minLength?: number;
72
76
  readonly required?: boolean;
73
77
  readonly searchable?: boolean;
74
78
  readonly sortable?: boolean;
@@ -90,6 +94,7 @@ export declare function createFolderAssignmentEntity(access?: EntityDefinition["
90
94
  readonly type: "text";
91
95
  readonly description?: string;
92
96
  readonly maxLength?: number;
97
+ readonly minLength?: number;
93
98
  readonly required?: boolean;
94
99
  readonly searchable?: boolean;
95
100
  readonly sortable?: boolean;
@@ -113,6 +118,7 @@ export declare const folderAssignmentEntity: EntityDefinition<{
113
118
  readonly type: "text";
114
119
  readonly description?: string;
115
120
  readonly maxLength?: number;
121
+ readonly minLength?: number;
116
122
  readonly required?: boolean;
117
123
  readonly searchable?: boolean;
118
124
  readonly sortable?: boolean;
@@ -134,6 +140,7 @@ export declare const folderAssignmentEntity: EntityDefinition<{
134
140
  readonly type: "text";
135
141
  readonly description?: string;
136
142
  readonly maxLength?: number;
143
+ readonly minLength?: number;
137
144
  readonly required?: boolean;
138
145
  readonly searchable?: boolean;
139
146
  readonly sortable?: boolean;
@@ -155,6 +162,7 @@ export declare const folderAssignmentEntity: EntityDefinition<{
155
162
  readonly type: "text";
156
163
  readonly description?: string;
157
164
  readonly maxLength?: number;
165
+ readonly minLength?: number;
158
166
  readonly required?: boolean;
159
167
  readonly searchable?: boolean;
160
168
  readonly sortable?: boolean;
@@ -15,7 +15,7 @@
15
15
  // Deferred: reparenting with cycle-check (folder:update CAN change parentId, but
16
16
  // no UI exposes it in v1 — folders-view guards cycles defensively).
17
17
  import { defineEntityCreateHandler, defineEntityDetailHandler, defineEntityListHandler, defineEntityUpdateHandler, defineFeature, } from "@cosmicdrift/kumiko-framework/engine";
18
- import { hasWhereRule } from "../shared/index.js";
18
+ import { assertNoWhereRuleInOwnershipWrite } from "../shared/index.js";
19
19
  import { DEFAULT_FOLDER_ACCESS, FOLDERS_FEATURE_NAME } from "./constants.js";
20
20
  import { createFolderAssignmentEntity, folderEntity } from "./entity.js";
21
21
  import { createClearFolderHandler } from "./handlers/clear-folder.write.js";
@@ -80,15 +80,7 @@ export function createFoldersFeature(opts = {}) {
80
80
  opts.parents === undefined) {
81
81
  return foldersFeature;
82
82
  }
83
- if (hasWhereRule(opts.ownership?.write)) {
84
- throw new Error("createFoldersFeature({ ownership }): ownership.write must not contain a " +
85
- '`{ kind: "where" }` rule — where-rules are evaluated only at the SQL ' +
86
- "layer (the read path, via buildOwnershipClause). Write paths that " +
87
- "consult access.write (userCanCreateFieldRow/userCanWriteFieldRow) can't " +
88
- "evaluate them, so such a rule can only ever deny — boot validation " +
89
- "rejects it too (fw#2626). Use a `from()` rule for ownership.write, or " +
90
- "leave it unset.");
91
- }
83
+ assertNoWhereRuleInOwnershipWrite("createFoldersFeature", opts.ownership?.write);
92
84
  if (opts.parents !== undefined && opts.parents.length === 0) {
93
85
  throw new Error("createFoldersFeature({ parents }): parents must not be an empty array — " +
94
86
  "an empty allowlist rejects every set-folder call and hides every " +
@@ -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}`);
@@ -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;